1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2010, Moloch
4 * Copyright (c) 2018, Ketmar Dark
6 * This file is part of Spelunky.
8 * You can redistribute and/or modify Spelunky, including its source code, under
9 * the terms of the Spelunky User License.
11 * Spelunky is distributed in the hope that it will be entertaining and useful,
12 * but WITHOUT WARRANTY. Please see the Spelunky User License for more details.
14 * The Spelunky User License should be available in "Game Information", which
15 * can be found in the Resource Explorer, or as an external file called COPYING.
16 * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
18 **********************************************************************************/
19 class EnemyMonkey['oMonkey'] : MapEnemy;
32 const int minDist = 80;
52 override bool initialize () {
53 if (!::initialize()) return false;
54 setSprite('sMonkeyLeft');
55 dir = global.randOther(0, 1);
61 if (poopCount < 20) canPoop = true;
62 else if (poopCount < 30 && global.randOther(1, 3) == 1) canPoop = true;
63 else if (global.randOther(1, 8) == 1) canPoop = true;
64 else poopTimer = global.randOther(30, 60);
65 //writeln("POOP TIMER: ", poopTimer, "; canPoop=", canPoop);
69 // ////////////////////////////////////////////////////////////////////////// //
70 // for dead players too
71 // first, the code will call `onObjectTouched()` for player
72 // if it returned `false`, the code will call this handler
73 // note that player's handler is called *after* its frame thinker,
74 // but object handler is called *before* frame thinker for the object
75 // return `true` to skip thinker on this frame
76 override bool onTouchedByPlayer (PlayerPawn plr) {
77 if (plr.dead || status == DEAD || dead) return false;
78 if ((stunned || status == STUNNED) && !global.hasSpikeShoes) return false;
80 if (fabs(plr.ix-(ix+8)) > 4 || status == GRAB || !plr.visible) {
82 } else if (!plr.dead && (plr.status == JUMPING || plr.status == FALLING) && plr.iy < iy+2 && !plr.swimming) {
83 plr.yVel = -6-0.2*plr.yVel;
85 //if (!invincible) hp -= 1;
86 plr.playSound('sndHit');
87 } else if (!meGoldMonkey && !plr.invincible && grabCounter == 0) {
88 if (iy+8 > plr.iy+2) flty = plr.iy+2-8;
89 if (iy+8 < plr.iy-2) flty = plr.iy-2-8;
95 counter = global.randOther(40, 80);
102 // ////////////////////////////////////////////////////////////////////////// //
103 bool onItemWalkBy (MapItem o) {
104 //if (meGoldMonkey) return true; // do nothing
105 if (throwCounter == 0 && status != GRAB && o.active && !o.heldBy) {
106 auto rope = ItemRopeThrow(o);
109 rope.xVel = (dir == Dir.Right ? 5 : -5);
111 if (!level.isSolidAtPoint(rope.ix, rope.iy)) rope.flty -= 2;
114 counter = global.randOther(20, 60);
118 writeln("monkey kicked item '", GetClassName(o.Class), "'");
119 o.xVel = (dir == Dir.Right ? 5 : -5);
121 if (!level.isSolidAtPoint(o.ix, o.iy-2)) o.flty -= 2;
124 counter = global.randOther(20, 60);
125 auto idol = ItemGoldIdol(o);
126 if (idol && idol.trigger) {
127 idol.sacrificeAllowed = true;
129 idol.trigger = false;
130 level.scrTriggerIdolAltar(stolenIdol:false);
139 // ////////////////////////////////////////////////////////////////////////// //
140 override void thinkFrame () {
142 if (!isInstanceAlive) return;
146 if (status != HANG && status != CLIMB && status != GRAB) yVel += myGrav;
147 yVel = fmin(yVel, yVelLimit);
153 auto plr = level.player;
157 if (!heldBy && (plr.dead || status != GRAB) && level.isSolidAtPoint(x+8, y+8)) hp = -999;
159 if (level.isWaterAtPoint(x+8, y+8)) {
161 level.MakeMapObject(x+8, y, 'oSplash');
163 playSound('sndSplash');
171 if (countsAsKill) level.addKill(objName);
177 if (--poopTimer == 0) doPoopTimer();
180 if (meGoldMonkey && canPoop) {
181 // I'm ready to poop!
182 //writeln("poop=", poop);
184 // I'm going to poop right now!
188 if (global.randOther(1, 50) == 1) obj = level.MakeMapObject(x+8, y+10, 'oRuby');
189 else if (global.randOther(1, 40) == 1) obj = level.MakeMapObject(x+8, y+10, 'oGoldNugget');
190 else if (global.randOther(1, 30) == 1) obj = level.MakeMapObject(x+8, y+10, 'oSapphire');
191 else if (global.randOther(1, 20) == 1) obj = level.MakeMapObject(x+8, y+10, 'oEmerald');
192 else obj = level.MakeMapObject(x+8, y+10, 'oGoldChunk');
194 obj.xVel = global.randOther(3, 5);
195 if (dir == Dir.Right) obj.xVel *= -1;
196 obj.yVel = -global.randOther(2, 4);
200 poopTimer = global.randOther(60, 120); // Perhaps I will poop again soon.
203 bool colBot = !!isCollisionBottom(1);
205 if (grabCounter > 0) grabCounter -= 1;
206 if (vineCounter > 0) vineCounter -= 1;
207 if (throwCounter > 0) throwCounter -= 1;
208 if (invincible > 0) invincible -= 1;
211 //if (status == HANG) status == IDLE;
212 if (/*status == IDLE &&*/ global.randOther(1, 140) == 1) poop = true;
213 //writeln("HOLD; status==IDLE:", status == IDLE, "; status=", status);
214 //setSprite('sMonkeyWalkL');
216 if (isCollisionRight(1)) xVel = -1;
217 if (isCollisionLeft(1)) xVel = 1;
219 auto dist = distanceToEntityCenter(plr);
220 if (!plr.visible) dist = 64;
222 if (status == IDLE) {
224 if (counter > 0) --counter; else status = WALK;
225 if (dist < 64) status = BOUNCE;
226 else if (meGoldMonkey && global.randOther(1, 140) == 1) poop = true;
227 //if (status == BOUNCE) playSound(global.sndFrog); // in the original
228 } else if (status == WALK) {
229 if (isCollisionLeft(1) || isCollisionRight(1)) {
232 xVel = (dir == Dir.Left ? -2 : 2);
233 if (global.randOther(1, 100) == 1) {
235 counter = global.randOther(20, 50);
236 if (meGoldMonkey && global.randOther(1, 6) == 1) poop = true;
238 } else if (global.randOther(1, 140) == 1) {
240 if (meGoldMonkey && global.randOther(1, 10) == 1) poop = true;
242 } else if (status == RECOVER) {
247 counter = global.randOther(10, 40);
248 } else if (isCollisionLadder()) {
249 if (vineCounter == 0) {
250 MapTile obj = isCollisionLadder();
257 counter = global.randOther(10, 40);
261 } else if (status == BOUNCE) {
263 yVel = -global.randOther(4, 5);
273 playSound('sndMonkey');
275 } else if (status == HANG) {
276 if (!isCollisionLadder()) status = IDLE;
285 } else if (status == CLIMB) {
287 if (vdir == VDir.Up) {
289 //if (not collision_point(x+8, y, oVine, 0, 0))
290 if (!level.isLadderOrPlatformAtPoint(x+8, y)) {
293 counter = global.randOther(10, 40);
297 //if (not collision_point(x+8, y+22, oVine, 0, 0))
298 if (!level.isLadderOrPlatformAtPoint(x+8, y+22)) {
301 counter = global.randOther(10, 40);
304 ///if (dist < 64 and oCharacter.y > y)
305 if (dist < 64 && plr.iy > y+8) {
308 yVel = -global.randOther(2, 4);
317 } else if (status == GRAB) {
321 fltx = plr.fltx+grabX;
322 flty = plr.flty+grabY;
323 // grid and interpolation fix start
324 prevFltX = plr.prevFltX+grabX;
325 prevFltY = plr.prevFltY+grabY;
327 // grid and interpolation fix end
334 int n = 500+trunc(ceil(500.0/4.0)*global.levelType);
335 if (global.randOther(1, 4) == 1) {
337 plr.xVel = (plr.dir == Dir.Left ? -3 : 3);
342 plr.scrDropItem(LostCause.Monkey, 0, 0);
343 plr.playSound('sndHit');
344 } else if (level.stats.money >= n && global.randOther(1, 10) <= 8) {
346 level.stats.takeMoney(n);
347 auto obj = ItemTreasure(level.MakeMapObject(x, y, 'oGoldNugget'));
349 obj.canCollect = false;
350 obj.collectRestoreTimer = 20;
351 obj.xVel = global.randOther(1, 3)-global.randOther(1, 3);
352 obj.yVel = -global.randOther(3, 4);
354 playSound('sndThrow');
355 } else if (global.randOther(1, 2) == 1 && global.rope > 0) {
359 if (global.rope == 0 && plr.isHoldingRope()) {
360 auto it = plr.holdItem;
364 auto obj = level.MakeMapObject(x, y, 'oRopeThrow');
366 obj.xVel = global.randOther(1, 3)-global.randOther(1, 3);
367 obj.yVel = -global.randOther(3, 4);
369 playSound('sndThrow');
370 } else if (global.bombs > 0) {
375 if (global.bombs == 0 && plr.isHoldingBomb()) {
376 auto it = plr.holdItem;
377 if (plr.isHoldingArmedBomb()) throwArmed = ItemBomb(it).alarmCount;
381 auto obj = ItemBomb(level.MakeMapObject(x, y, 'oBomb'));
383 obj.xVel = global.randOther(1, 3)-global.randOther(1, 3);
384 obj.yVel = -global.randOther(3, 4);
385 int boom = (global.config.bizarre ? 7 : 10);
386 if (throwArmed >= 0 || global.randOther(1, boom) == 1) {
388 obj.sprite_index = sBombArmed;
393 if (throwArmed < 0) throwArmed = 40;
394 obj.armIt(throwArmed);
397 playSound('sndThrow');
401 yVel = -global.randOther(2, 4);
410 invincible = 30; // prevent being hit with item thrown
412 } else if (status != DROWNED) {
417 if (status != GRAB && isCollisionTop(1)) yVel = 1;
420 if (status == HANG) setSprite('sMonkeyHangL');
421 else if (status == CLIMB || status == GRAB) setSprite('sMonkeyClimbL');
422 else if (!colBot) setSprite('sMonkeyJumpL');
423 else if (status == WALK) setSprite('sMonkeyWalkL');
424 else setSprite('sMonkeyLeft');
427 if (!meGoldMonkey && throwCounter == 0 && status != GRAB) {
428 level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
429 auto item = MapItem(o);
430 if (item && !item.heldBy && item.active) return onItemWalkBy(item);
440 desc2 = "An amazingly irritating primate that robs and stuns passerby.";
442 setCollisionBounds(4, 6, 12, 16);
447 yVelLimit = 6; // YASM 1.7
466 doBasicPhysics = false;
468 leavesBody = true; // we will do our own death sequence
469 checkInsideBlock = false;
470 allowWaterProcessing = false;
481 // ////////////////////////////////////////////////////////////////////////// //
482 class EnemyGoldMonkey['oMonkeyGold'] : EnemyMonkey;
485 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
486 //image_blend = make_color_rgb(255, 245, 104);
487 auto oclr = GLVideo.color;
488 GLVideo.color = 0xff_f5_68;
489 ::drawWithOfs(xpos, ypos, scale, currFrameDelta);
490 GLVideo.color = oclr;
497 desc = "Gold Monkey";
498 desc2 = "This monkey is SPECIAL.";