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 ObjBarrier['oBarrier'] : MapObject;
22 override bool initialize () {
23 if (!::initialize()) return false;
24 setSprite('sBarrier');
29 override bool collidesWith (MapEntity e, optional bool ignoreDims) {
30 if (!e || width < 1 || height < 1) return false;
31 if (e == self) return false; // never
32 return e.isRectHitSimple(x0, y0, width, height);
36 override void onBulletHit (ObjBullet bullet) {
37 level.MakeMapObject(bullet.ix, bullet.iy, 'oSmokePuff');
42 transient array!MapObject objhits;
44 override void thinkFrame () {
45 setCollisionBoundsFromFrame();
46 //writeln("barrier hitbox: (", hitboxX, ",", hitboxY, "); size:(", hitboxW, "x", hitboxH, ")");
49 auto plr = level.player;
50 //writeln("C0:", plr.isRectHitSimple(x0, y0, width, height), "; C1:", plr.collidesWith(self));
51 if (plr.collidesWith(self)) {
52 //writeln("barrier with player");
53 bool wasInvi = (plr.invincible == 0);
57 if (plr.ix < ix) plr.xVel = -6; else plr.xVel = 6;
58 if (!wasInvi && global.plife > 0) global.plife -= damage;
60 if (!plr.isHoldingBombOrRope() && plr.holdItem !isa PlayerWeapon) {
61 plr.scrDropItem(LostCause.Throw, -global.randOther(4, 6), -2);
63 auto it = plr.holdItem;
64 if (it && it !isa PlayerWeapon) {
66 it.xVel = -global.randOther(4, 6);
71 plr.playSound('sndHurt');
76 level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
77 if (o.heldBy) return false;
78 if (o isa ItemWebBall || o isa MapItem) {
79 if (o.collidesWith(self)) {
81 foreach (MapObject xo; objhits) if (xo == o) { found = true; break; }
82 if (!found) objhits[$] = o;
84 } else if (o isa MapEnemy) {
85 o.xVel = -global.randOther(4, 6);
91 foreach (MapObject o; objhits) {
92 if (!o || !o.isInstanceAlive) continue;
93 if (o isa ItemWebBall) { o.instanceRemove(); continue; }
94 auto bomb = ItemBomb(o);
95 if (bomb) bomb.armIt(global.randOther(4, 8));
96 o.xVel = -global.randOther(4, 6);
106 desc2 = "The business end of a beam turret.";
108 damage = 1; // damage amount to player on contact
109 setCollisionBounds(0, 0, 10, 10);
111 canBeHitByBullet = true;
117 // ////////////////////////////////////////////////////////////////////////// //
118 class ObjBarrierEmitter['oBarrierEmitter'] : MapObject;
123 override bool initialize () {
124 if (!::initialize()) return false;
125 setSprite('sBarrierEmitter');
130 override bool collidesWith (MapEntity e, optional bool ignoreDims) {
131 if (!e || width < 1 || height < 1) return false;
132 if (e == self) return false; // never
133 return e.isRectHitSimple(x0, y0, width, height);
137 void destroyBarrier () {
138 level.forEachObject(delegate bool (MapObject o) {
139 if (o isa ObjBarrier) o.instanceRemove();
145 override void onDestroy () {
147 auto obj = level.MakeMapObject(ix+2+global.randOther(0, 14), iy+2+global.randOther(0, 14), 'oFlareSpark');
148 if (obj) obj.yVel = global.randOther(1, 3);
151 playSound('sndSmallExplode');
157 override void onBulletHit (ObjBullet bullet) {
162 // return `true` if item hits the character
163 // this is called after item's event
165 bool onHitByItem (MapItem item) {
166 // are we invincible?
167 if (!barrierChecked) return false;
168 //if (invincible) return false;
169 if (fabs(item.xVel) > 2 || fabs(item.yVel) > 2) {
171 writeln("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
173 item.playSound('sndHit');
176 item.moveRel(-item.xVel, -item.yVel);
177 item.xVel = -item.xVel;
183 void moveObjectAwayHoriz (MapObject o) {
184 if (!o || !o.isInstanceAlive) return;
185 if (o.x0 > xCenter) {
193 transient array!MapObject objhits;
195 override void thinkFrame () {
196 if (!barrierChecked) {
197 barrierChecked = true;
198 auto bar = level.forEachObject(delegate bool (MapObject o) { return (o isa ObjBarrier); });
199 if (!bar) level.MakeMapObject(ix, iy+16, 'oBarrier');
202 setCollisionBoundsFromFrame();
203 //writeln("barrier hitbox: (", hitboxX, ",", hitboxY, "); size:(", hitboxW, "x", hitboxH, ")");
206 level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
207 if (o isa MapItem && fabs(o.xVel) > 2 || fabs(o.yVel) > 2) {
208 if (o.collidesWith(self)) {
209 writeln("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
212 o.playSound('sndHit');
220 if (!level.isSolidAtPoint(ix, iy-16)) { instanceRemove(); return; }
223 auto plr = level.player;
224 //writeln("C0:", plr.isRectHitSimple(x0, y0, width, height), "; C1:", plr.collidesWith(self));
225 if (plr.collidesWith(self)) {
226 //writeln("barrier with player");
229 plr.xVel = fabs(plr.xVel);
230 if (plr.ix >= ix) plr.xVel = -plr.xVel;
232 moveObjectAwayHoriz(plr);
237 level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
238 if (o.heldBy) return false;
239 if (o isa ItemWebBall) return false;
241 if (o.collidesWith(self)) {
243 foreach (MapObject xo; objhits) if (xo == o) { found = true; break; }
244 if (!found) objhits[$] = o;
246 } else if (o isa MapEnemy) {
247 //o.xVel = fabs(o.xVel);
248 //if (o.ix >= ix) o.xVel = -o.xVel;
249 moveObjectAwayHoriz(o);
254 foreach (MapObject o; objhits) {
255 if (!o || !o.isInstanceAlive) continue;
256 //if (invincible) return false;
257 if (fabs(o.xVel) > 2 || fabs(o.yVel) > 2) {
259 //writeln("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
261 o.playSound('sndHit');
265 //o.xVel = fabs(o.xVel);
266 //if (o.ix >= ix) o.xVel = -o.xVel;
267 moveObjectAwayHoriz(o);
274 objName = 'Barrier Emitter';
275 desc = "Beam Turret";
276 desc2 = "An alien defense mechanism that emits a short-range laser beam.";
279 canBeHitByBullet = true;
280 setCollisionBounds(0, 0, 10, 10);
286 // ////////////////////////////////////////////////////////////////////////// //
287 class EnemyAlienBoss['oAlienBoss'] : MapEnemy;
290 int attackRange = 96;
293 // ////////////////////////////////////////////////////////////////////////// //
294 override bool initialize () {
295 if (!::initialize()) return false;
296 setSprite('sAlienBoss');
297 //!if (isRoom("rOlmec2")) attackRange = 128; else attackRange = 96;
298 //dir = global.randOther(0, 1);
303 // ////////////////////////////////////////////////////////////////////////// //
304 void destroyBarrier () {
305 level.forEachObject(delegate bool (MapObject o) {
306 if (o isa ObjBarrierEmitter) o.instanceRemove();
312 // ////////////////////////////////////////////////////////////////////////// //
313 override void onAnimationLooped () {
314 if (spriteLName == 'sAlienBossDie') {
315 setSprite('sAlienBossDead');
317 } else if (spriteLName == 'sAlienBossHurt') {
318 setSprite('sAlienBoss');
323 // ////////////////////////////////////////////////////////////////////////// //
324 override bool onTouchedByPlayer (PlayerPawn plr) {
325 if (plr.dead || dead || status == DEAD) return false;
326 if ((stunned || status == STUNNED) && !global.hasSpikeShoes) return false;
329 int plrx = plr.ix, plry = plr.iy;
332 if ((plr.status == JUMPING || plr.status == FALLING) && plry < y+8 && !plr.swimming) {
333 if (status < STUNNED || global.hasSpikeShoes) {
334 plr.yVel = -6-0.2*plr.yVel;
336 if (global.hasSpikeShoes) {
337 hp -= 3*trunc(floor(plr.fallTimer/16)+1);
340 hp -= 1*trunc(floor(plr.fallTimer/16)+1);
344 spillBlood(amount:bam);
345 plr.playSound('sndHit');
347 } else if (plr.invincible == 0 && status != DEAD) {
350 if (plry < y) plr.yVel = -6;
351 if (plrx < x) plr.xVel = -6; else plr.xVel = 6;
352 //instance_create(plr.x, plr.y, oBlood);
353 if (global.plife > 0) {
354 global.plife -= damage;
355 if (global.plife <= 0 /*&& isRealLevel()*/) level.addDeath(objName);
358 plr.playSound('sndHurt');
361 return false; // don't skip thinker
365 // return `false` to do standard weapon processing
366 override bool onTouchedByPlayerWeapon (PlayerPawn plr, PlayerWeapon wpn) {
367 if (heldBy) return true;
368 if (status < STUNNED) {
372 spillBlood(amount:1);
373 plr.playSound('sndHit');
379 override void onBulletHit (ObjBullet bullet) {
380 if (status == DEAD) return;
382 ::onBulletHit(bullet);
386 // ////////////////////////////////////////////////////////////////////////// //
387 override void thinkFrame () {
390 if (level.isSolidAtPoint(x+8, y+8)) hp = 0;
392 if (hp < 1 && status != DEAD) {
394 setSprite('sAlienBossDie');
396 if (countsAsKill) level.addKill(objName);
397 if (false /*carries != ""*/) {
398 //!!!if (holds != "") scrMakeItem(carries, x+16, y+16, 1); else scrMakeItem(carries, x+16, y+16);
401 MapObject gem = none;
402 switch (global.randOther(1, 3)) {
403 case 1: gem = level.MakeMapObject(x+16, y+16, 'oEmeraldBig'); break;
404 case 2: gem = level.MakeMapObject(x+16, y+16, 'oSapphireBig'); break;
405 case 3: gem = level.MakeMapObject(x+16, y+16, 'oRubyBig'); break;
408 gem.xVel = global.randOther(0, 3)-global.randOther(0, 3);
413 //!with (oBossBlock) dying = true;
416 if (spriteLName == 'sAlienBossDie') {
417 if (global.randOther(1, 2) == 1) {
418 foreach (; 0..2) scrCreateBlood(ix+8, iy+global.randOther(14, 18), 2);
420 if (imageFrame >= 8) {
421 setSprite('sAlienBossDead');
426 yVel = fmin(yVel+myGrav, yVelLimit);
428 if (xVel > 0) xVel -= 0.1;
429 if (xVel < 0) xVel += 0.1;
430 if (fabs(xVel) < 0.5) xVel = 0;
432 if (isCollisionBottom(1) && status != STUNNED) yVel = 0;
434 if (status == IDLE) xVel = 0;
441 if (isCollision()) { flty = y-2; y = iy; }
443 auto plr = level.player;
444 auto dist = distanceToEntity(plr);
446 if (psychicRecover > 0) {
448 } else if (dist < attackRange && status != DEAD && !plr.dead && !plr.stunned && plr.invincible == 0) {
450 level.MakeMapObject(x+16+global.randOther(0, 32)-global.randOther(0, 32), y+16+global.randOther(0, 32)-global.randOther(0, 32), 'oPsychicCreate');
452 level.MakeMapObject(x+16, y+16, 'oPsychicWave');
453 //!if (isRoom("rOlmec2")) psychicRecover = 30; else psychicRecover = 100;
454 psychicRecover = 100;
455 playSound('sndPsychic');
458 if (spriteLName != 'sAlienBossHurt') imageSpeed = 0.25;
461 if (status != DEAD && spriteLName != 'sAlienBossHurt' && facing == LEFT) sprite_index = sAlienBoss;
462 if (status != DEAD && spriteLName != 'sAlienBossHurt' && facing == RIGHT) sprite_index = sAlienBoss;
464 if (status != DEAD && spriteLName != 'sAlienBossHurt') setSprite('sAlienBoss');
469 objName = 'Alien Boss';
471 desc2 = "This psychic alien subspecies operates the more complex mechanics of their starship and holds sway over the rest of the crew.";
473 setCollisionBounds(0, 0, 32, 32);
477 yVelLimit = 6; // YASM 1.7
493 canBeHitByBullet = true;
495 psychicRecover = 100;
498 doBasicPhysics = false;
500 leavesBody = false; // default processing
501 checkInsideBlock = true;
502 allowWaterProcessing = true;
511 // ////////////////////////////////////////////////////////////////////////// //
512 class ItemSfxPsychicCreateAlienBoss['oPsychicCreate'] : MapObject;
519 override bool initialize () {
520 if (!::initialize()) return false;
521 setSprite('sPsychicCreate');
526 override void onAnimationLooped () {
531 override void thinkFrame () {
534 auto aboss = level.forEachObject(delegate bool (MapObject o) { return (o isa EnemyAlienBoss); });
535 if (aboss) dirAngle = pointDirection(ix, iy, aboss.ix+16, aboss.iy+16);
537 shiftXY(2*cos(dirAngle), -2*sin(dirAngle));
542 objName = 'Psychic Wave';
551 // ////////////////////////////////////////////////////////////////////////// //
552 class ItemProjPsychicWaveAlienBoss['oPsychicWave'] : MapObject;
557 override bool initialize () {
558 if (!::initialize()) return false;
559 setSprite('sPsychicWave');
560 if (false /*isRoom("rOlmec2")*/) {
571 override void onAnimationLooped () {
576 override void thinkFrame () {
577 auto plr = level.player;
579 auto dirAngle = pointDirection(ix, iy, plr.ix, plr.iy);
580 shiftXY(dm*cos(dirAngle), -dm*sin(dirAngle));
582 if (isOutsideOfLevel()) { instanceRemove(); return; }
584 setCollisionBoundsFromFrame();
586 if (plr.visible && plr.invincible == 0 && plr.collidesWith(self)) {
589 //if (plry < y) plr.yVel = -6;
590 //if (plrx < x) plr.xVel = -6; else plr.xVel = 6;
591 //instance_create(plr.x, plr.y, oBlood);
592 plr.xVel = global.randOther(0, 2)-global.randOther(1, 2);
595 if (global.plife > 0) {
596 global.plife -= damage;
597 if (global.plife <= 0 /*&& isRealLevel()*/) level.addDeath(objName);
600 playSound('sndHurt');
603 level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
604 if (o isa EnemyAlienBoss) return false;
605 if (/*o.dead ||*/ o.invincible) return false;
607 auto scp = MonsterShopkeeper(o);
608 if (scp && !scp.angered) scp.status = ATTACK;
610 auto dms = MonsterDamsel(o);
612 if (dms.status == DEAD) return false;
613 dms.xVel = global.randOther(0, 2)-global.randOther(1, 2);
621 //dms.kissCount = min(1, dms.kissCount);
623 //playSound('sndDamsel');
625 instanceRemove(); //???
629 auto enemy = MapEnemy(o);
631 //if (enemy.status == DEAD) return false;
636 enemy.xVel = global.randOther(0, 2)-global.randOther(1, 2);
642 }, castClass:MapEnemy);
647 objName = 'Psychic Wave';