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 MonsterDamselKiss['oDamselKiss'] : MapObject;
24 override bool initialize () {
25 if (!::initialize()) return false;
26 setSprite(global.isDamsel ? 'sStandLeft' : 'sDamselLeft');
27 setCollisionBoundsFromFrame();
32 override void onAnimationLooped () {
33 auto spr = getSprite();
34 if (spr.Name == 'sDamselKissL' || spr.Name == 'sPKissL') {
35 setSprite(global.isDamsel ? 'sStandLeft' : 'sDamselLeft');
43 setSprite(global.isDamsel ? 'sPKissL' : 'sDamselKissL');
47 override void thinkFrame () {
48 //writeln("DKS is thinking...");
49 auto spr = getSprite();
50 if ((spr.Name == 'sDamselKissL' || spr.Name == 'sPKissL') && trunc(imageFrame) == 7) {
51 level.MakeMapObject(ix-8, iy-8, 'oHeart');
53 global.plife += level.damselSaved;
54 level.damselSaved = 0;
59 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
60 ::drawWithOfs(xpos, ypos, scale, currFrameDelta);
62 auto oclr = GLVideo.color;
63 GLVideo.color = 0xff_ff_00;
64 level.sprStore.loadFont('sFontSmall');
66 level.sprStore.renderText((ix-32)*scale-xpos, (iy-18)*scale-ypos, "MY HERO!", 3);
79 // ////////////////////////////////////////////////////////////////////////// //
80 // dunno, she is an item in the original code
81 class MonsterDamsel['oDamsel'] : ObjCharacter;
87 int kissCount = 2; // unhurt
103 override bool initialize () {
104 if (!::initialize()) return false;
105 setSprite('sDamselLeft');
107 if (!global.config.optDoubleKiss) kissCount = 1;
108 writeln("*** generated Damsel at tile (", ix/16, ",", iy/16, ")");
114 override void onDestroy () {
115 writeln("*** Damsel is dead");
120 final bool isStatusLessThanExit () {
129 final bool isExiting () {
130 auto spr = getSprite();
131 return (spr.Name == 'sDamselExit2' || spr.Name == 'sPExit');
135 final bool isKissing () {
136 auto spr = getSprite();
137 return (spr.Name == 'sDamselKissL' || spr.Name == 'sPKissL');
141 final bool isStunned () {
142 auto spr = getSprite();
143 return (spr.Name == 'sDamselStunL');
147 override bool onCanBePickedUp (PlayerPawn plr) {
152 // return `true` to stop player from holding it
153 override bool onTryPickup (PlayerPawn plr) {
154 if (forSale) level.osdMessage("YOU MUST BE IN LOVE!", 3.33);
159 override void onAnimationLooped () {
164 if (status == YELL) {
167 } else if (status == KISS) {
173 override bool onExplosionTouch (MapObject xplo) {
174 if (invincible) return false;
175 if (heldBy) heldBy.holdItem = none;
177 writeln("damsel touched by an explosion");
178 if (myGrav == 0) myGrav = 0.6; // spikes fix
180 kissCount = 0; // alas
182 if (xplo.fltx < fltx) xVel = global.randOther(4, 6); else xVel = -global.randOther(4, 6);
185 hp -= global.config.explosionDmg;
187 //if (hp <= 0 && !leavesBody) instanceRemove();
192 // return `false` to do standard weapon processing
193 override bool onTouchedByPlayerWeapon (PlayerPawn plr, PlayerWeapon wpn) {
194 if (heldBy) return true;
195 //writeln("damsel hit by weapon");
198 //damselDropped = true;
199 kissCount = min(1, kissCount);
200 if (hp > 0) playSound('sndDamsel');
202 plr.playSound('sndHit');
206 level.scrShopkeeperAnger(GameLevel::SCAnger.DamselWhipped);
215 } else if (status != THROWN && (isStatusLessThanExit() || status == SLAVE || status == KISS)) {
217 //damselDropped = true;
218 kissCount = min(1, kissCount);
220 plr.playSound('sndHit');
222 level.scrShopkeeperAnger(GameLevel::SCAnger.DamselWhipped);
226 writeln("whiphits=", whiphits);
227 playSound('sndDamsel');
229 if (global.randOther(1, 30) == 1 || whiphits > 10) {
234 //instance_create(x+8, y-8, oBlood);
236 spillBlood(); //k8: was iy-8; why?
248 override void onBulletHit (ObjBullet bullet) {
249 writeln("damsel hit by bullet");
250 if (heldBy) heldBy.holdItem = none;
258 //damselDropped = true;
259 kissCount = 0; // alas
261 xVel = bullet.xVel*0.3;
262 if (hp > 0) playSound('sndDamsel');
266 // return `false` to prevent
267 // owner is usually a player
268 override bool onLostAsHeldItem (MapObject owner, LostCause cause, optional float xvel, optional float yvel) {
271 if (cause == LostCause.Whoa) {
272 xVel = (owner.dir == Dir.Left ? -2 : 2);
273 playSound('sndDamsel');
277 if (cause == LostCause.Drop) {
280 } else if (cause == LostCause.Dead || cause == LostCause.Stunned) {
287 if (specified_xvel) xVel = xvel;
288 if (specified_yvel) yVel = yvel;
294 override void onBeforeThrowBy (PlayerPawn plr) {
295 writeln("throwing a damsel!");
296 if (plr./*kDown*/scrPlayerIsDucking()) {
297 // drop gently - damsel remains calm after stun
299 calm = true; // prevents running after recover from stun
301 counter = round(stunMax/2); //it.stunMax
303 // throw - damsel freaks out after stun
307 if (hp > 0) playSound('sndDamsel');
312 void exitAtDoor (MapTile door) {
314 if (heldBy) heldBy.holdItem = none;
316 // global.plife += 1; // in the original
317 //!if (isRealLevel()) global.damselsSavedTotal += 1;
320 global.xdamsels += 1;
321 global.xdamseldouble = (!hld.damselDropped);
322 door = instance_place(x, y, oExit);
324 level.damselSaved = kissCount;
325 door.snapToExit(self);
326 setSprite(global.isDamsel ? 'sPExit' : 'sDamselExit2');
330 playSound('sndSteps');
336 level.addDamselSaved();
343 // return `true` if item hits the character
344 // this is called after item's event
345 override bool onHitByItem (MapItem item) {
346 // are we invincible?
347 if (invincible) return false;
348 // any items will fly thru stunned enemies (but not projectiles)
349 if ((status >= STUNNED || status == THROWN || status == DEAD || dead) && item !isa ItemProjectile) return false;
350 if (item isa ItemProjectile && !ItemProjectile(item).canHarm(self)) return false;
351 // item does no damage?
352 if (!item.damage) return false;
354 if (fabs(item.xVel) < ItemSpeedToHitX && fabs(item.yVel) < ItemSpeedToHitY) return false;
356 if (heldBy) heldBy.holdItem = none;
359 if (fabs(xVel) < 1) xVel = (item.xVel < 0 ? -1.5 : 1.5);
360 yVel = (self isa ItemProjectile ? -1.5 : -1.0);
362 xVel = item.xVel*0.3;
363 yVel = (self isa ItemProjectile ? -6.0 : -2.0);
367 auto proj = ItemProjectile(item);
368 if (proj) countsAsKill = proj.launchedByPlayer;
371 if (!dead) spillBlood(amount:1);
375 //damselDropped = true;
376 //kissCount = min(1, kissCount);
378 item.playSound('sndHit');
380 return true; // it hit us
384 override void thinkFrame () {
385 lightRadius = (!heldBy && forSale && level.isInShop(ix/16, iy/16) ? 64 : 0);
387 if (status == EXIT) {
388 //writeln("exiting!");
392 // check if it is stolen
393 if (heldBy && forSale && !level.isInShop(ix/16, iy/16)) {
394 //writeln("STOLEN!");
395 level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
400 auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
401 if (active && door && hp > 0 && status != THROWN) {
412 if (invincible > 0) invincible -= 1;
415 bool colTop = !!isCollisionTop(1);
416 bool colLeft = !!isCollisionLeft(1);
417 bool colRight = !!isCollisionRight(1);
418 bool colBot = !!isCollisionBottom(1);
420 if (!isCollisionAtPoint(ix, iy)) {
421 //if (self isa ItemProjectileArrow) writeln("xVel=", xVel, "; yVel=", yVel);
424 if (!colLeft && !colRight) stuck = false;
426 if (!flying /*&& !colBot && !stuck*/) yVel += myGrav;
427 //if (yVel > 8) yVel = 8;
428 yVel = fmin(yVelLimit, yVel);
430 if (colTop && yVel < 0) {
431 if (bounceTop) yVel = -yVel*0.8;
432 else if (fabs(xVel) < 0.0001) yVel = 0;
435 if (colLeft || colRight) {
436 xVel = (bounce ? -xVel*0.5 : 0.0);
443 if (yVel > 1 && bounce) yVel = -yVel*bounceFactor; else yVel = 0;
445 if (fabs(xVel) < 0.1) xVel = 0;
446 else if (fabs(xVel) != 0) xVel *= frictionFactor;
447 if (fabs(yVel) < 1) {
449 if (!isCollisionBottom(1)) shiftY(1);
454 if (colLeft && !stuck) {
455 if (!colRight) shiftX(1);
457 } else if (colRight && !stuck) {
462 if (isCollisionTop(1)) {
463 if (yVel < 0) yVel = -yVel*0.8; else shiftY(1);
467 if (isCollisionInRect(ix-3, iy-3, 7, 7, &level.cbCollisionLava)) {
476 if (colTop && !colBot) shiftY(1);
477 else if (colLeft && !colRight) shiftX(1);
478 else if (colRight && !colLeft) shiftX(-1);
479 else { xVel = 0; yVel = 0; }
482 // end of item physics
487 auto spf = getSpriteFrame();
489 auto wtile = level.isWaterAtPoint(ix+spf.width/2, iy+spf.height/2/*, -1, -1*/);
492 level.MakeMapObject(ix+spf.width/2, iy, 'oSplash');
494 playSound('sndSplash');
497 if (onFellInWater(wtile) || !isInstanceAlive) return;
499 auto wasSwimming = swimming;
503 if (onOutOfWater() || !isInstanceAlive) return;
509 if (!heldBy && level.isSolidAtPoint(x, y)) {
511 if (hp > 0 && !dead) { if (countsAsKill) level.addKill(objName); }
512 playSound('sndDamsel');
518 if (global.randOther(1, 5) == 1) level.MakeMapObject(x+global.randOther(4, 12), y+global.randOther(4, 12), 'oBurn');
520 //damselDropped = true;
521 kissCount = 0; // alas
524 if (level.isLavaAtPoint(x, y+6)) {
526 if (hp > 0 && !dead) { if (countsAsKill) level.addKill(objName); }
528 countsAsKill = false;
535 setSprite(global.isDamsel ? 'sDieL' : 'sDamselDieL');
539 auto spikes = isCollisionAtPoint(x, y+6, &level.cbCollisionSpikes);
541 if (!bloodless) spikes.makeBloody();
542 if (hp > 0 && !dead) { if (countsAsKill) level.addKill(objName); }
544 setSprite(global.isDamsel ? 'sDieL' : 'sDamselDieL');
547 countsAsKill = false;
554 if (myGrav == 0 && fabs(yVel) < 0.01) myGrav = 0.6;
557 if (collision_rectangle(x-3, y-3, x+3, y+3, oSpearsLeft, 0, 0) and (status != THROWN or isCollisionBottom(1))) {
558 //damselDropped = true;
559 kissCount = min(1, kissCount);
560 obj = instance_nearest(x, y, oSpearsLeft);
561 if (obj.image_index >= 19 and obj.image_index < 28) {
564 with (oPlayer1) { holdItem = 0; pickupItemType = ""; }
571 if (obj.x < x) xVel = 4; else xVel = -4;
573 playSound(global.sndHit);
574 instance_create(other.x+8, other.y+8, oBlood);
580 if (!heldBy && yVel > 2 && status != THROWN) {
584 //writeln("SNDDAMSEL!");
585 playSound('sndDamsel');
590 //setSprite('sDamselStunL');
591 /*if (!(dead || status == DEAD))*/ setSprite(global.isDamsel ? 'sDieLBounce' : 'sDamselBounceL');
592 //setSprite('sDamselFallL');
595 //!if (isRealLevel()) global.damselsGrabbed += 1; // why was this in status == THROWN?
598 dir = level.player.dir;
599 } else if (status == SLAVE) {
600 dir = (level.player.ix < x ? Dir.Left : Dir.Right);
601 setSprite(global.isDamsel ? 'sStandLeft' : 'sDamselLeft');
602 } else if (status == KISS) {
604 if (isKissing() && int(imageFrame) == 7) {
605 level.MakeMapObject(x+(dir == Dir.Left ? -8 : 8), y-8, 'oHeart');
606 playSound('sndKiss');
608 } else if (status == IDLE) {
609 setSprite(global.isDamsel ? 'sStandLeft' : 'sDamselLeft');
615 if (global.isDamsel) sprite_index = sYellLeft; else sprite_index = sDamselYellL;
616 if (level.player.x < x) pan = 1;
617 else if (level.player.x > x) pan = -1;
619 playSound(global.sndDamsel, 1, pan);
622 setSprite(global.isDamsel ? 'sYellLeft' : 'sDamselYellL');
623 // pan it slightly if damsel is too far away
624 auto plr = level.player;
625 if (distanceToEntityCenter(plr) <= 16*7) {
626 playSound('sndDamsel');
628 int px = plr.xCenter, py = plr.yCenter;
629 int dx = sign(xCenter-px);
630 int dy = sign(yCenter-py);
631 global.playSound(0, 96*dx, 96*dy, 'sndDamsel');
634 } else if (status == YELL) {
635 if (int(imageFrame) == 4) level.MakeMapObject(x, y-16, 'oYellHelp');
636 } else if (status == RUN) {
637 //damselDropped = true;
638 kissCount = min(1, kissCount);
641 setSprite(global.isDamsel ? 'sRunLeft' : 'sDamselRunL');
642 if (dir == Dir.Left && isCollisionLeft(2)) dir = Dir.Right;
643 if (dir == Dir.Right && isCollisionRight(2)) dir = Dir.Left;
644 xVel = (dir == Dir.Left ? -1.5 : 1.5);
645 } else if (status == THROWN) {
646 //damselDropped = true;
648 // setCollisionBounds(-4, -2, 4, 2); // commented out in the original
649 if (yVel > 2) calm = false; // if player has tried to set damsel down gently but she falls, panic when recover from stun
650 if (!calm) kissCount = min(1, kissCount);
652 if (global.isDamsel) {
655 } else if (bounced) {
656 setSprite(yVel < 0 ? 'sDieLBounce' : 'sDieLFall');
658 setSprite(xVel < 0 ? 'sDieLL' : 'sDieLR');
662 setSprite('sDamselStunL');
663 } else if (bounced) {
664 setSprite(yVel < 0 ? 'sDamselBounceL' : 'sDamselFallL');
666 setSprite(xVel < 0 ? 'sDamselDieLL' : 'sDamselDieLR');
670 if (isCollisionBottom(1) && !bounced) bounced = true;
672 if (isCollisionBottom(2) || level.isObjectInRect(x-4, y-6, 9, 15, &level.cbIsObjectWeb)) {
678 if (cost > 0 && forSale && level.isInShop(x, y)) {
679 // kissing booth girl, resume SLAVE state
683 // resume IDLE state after been dropped gently
695 setSprite(global.isDamsel ? 'sDieL' : 'sDamselDieL');
699 if (countsAsKill) level.addKill(objName);
703 if (isStunned()) { // YASM 1.8.1
704 if (counter > 0 && counter < 30) imageSpeed = 0.8;
708 } else if (status == DEAD) {
712 if (countsAsKill) level.addKill(objName);
714 setSprite(global.isDamsel ? 'sDieL' : 'sDamselDieL');
716 // this fixes held sprite
717 if (hp <= 0 && status != THROWN && status != DEAD && status != STUNNED) status = THROWN;
720 if (status == THROWN || status == DEAD) {
721 if (checkAndPerformSacrifice()) return;
723 sacCount = default.sacCount;
727 if ((status == THROWN || status == DEAD || dead) && (fabs(xVel) > 2 || fabs(yVel) > 2) && !spectral) {
729 level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
730 if (o == self) return false;
731 if (!o.dead && !o.stunned && o.status < STUNNED && o.status != THROWN) {
732 if (!o.collidesWith(self)) return false;
733 onStunnedHitEnemy(MapEnemy(o));
737 }, precise:false, castClass:MapEnemy);
742 if (status == DEAD && removeCorpse) {
743 if (deathTimer > 0) {
756 desc2 = "An unfortunate creature, underfed and driven half-insane by life in caverns and brothels. Perhaps you could free them.";
758 setCollisionBounds(-4, -4, 4, 8);
762 calm = false; // if calm, damsel will return to idle state after recovering from stun, otherwise she will run
766 //damselDropped = false;
767 //!cost = getKissValue()*3;
768 //!buyMessage = "I'LL LET YOU HAVE H";
769 //!if (global.isDamsel) buyMessage = buyMessage+"IM"; else buyMessage = buyMessage+"ER";
770 //!buyMessage = buyMessage+" FOR $"+string(cost)+"!";
801 removeCorpse = false; // only applies to enemies that have corpses (Caveman, Yeti, etc.)
802 deathTimer = 200; // how many steps after death until corpse is removed
804 countsAsKill = true; // sometimes it's not the player's fault!
808 canBeHitByBullet = true;