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 // he is an enemy in the original, but... meh
20 class MonsterShopkeeper['oShopkeeper'] : MapEnemy;
23 string shopkeeperName;
25 array!string namelist;
44 bool tryToJumpOut; // if a player is trying to corner a shopkeeper, try to run and jump
61 final name chooseName (
72 if (specified_n0) ++count;
73 if (specified_n1) ++count;
74 if (specified_n2) ++count;
75 if (specified_n3) ++count;
76 if (specified_n4) ++count;
77 if (specified_n5) ++count;
78 if (specified_n6) ++count;
79 if (specified_n7) ++count;
80 if (!count) return '';
83 auto idx = global.randOther(0, count);
84 if (specified_n0) { if (idx-- == 0) return n0; }
85 if (specified_n1) { if (idx-- == 0) return n1; }
86 if (specified_n2) { if (idx-- == 0) return n2; }
87 if (specified_n3) { if (idx-- == 0) return n3; }
88 if (specified_n4) { if (idx-- == 0) return n4; }
89 if (specified_n5) { if (idx-- == 0) return n5; }
90 if (specified_n6) { if (idx-- == 0) return n6; }
91 if (specified_n7) { if (idx-- == 0) return n7; }
96 // select a single item for sequence of dice house prizes during level generation
97 name scrGeneratePrize () {
98 if (global.randRoom(1, 40) == 1) return 'oJetpack';
99 if (global.randRoom(1, 25) == 1) return 'oCapePickup';
100 if (global.randRoom(1, 20) == 1) return 'oShotgun';
101 if (global.randRoom(1, 10) == 1) return 'oGloves';
102 if (global.randRoom(1, 10) == 1) return 'oTeleporter';
103 if (global.randRoom(1, 8) == 1) return 'oMattock';
104 if (global.randRoom(1, 8) == 1) return 'oPaste';
105 if (global.randRoom(1, 8) == 1) return 'oSpringShoes';
106 if (global.randRoom(1, 8) == 1) return 'oSpikeShoes';
107 if (global.randRoom(1, 8) == 1) return 'oCompass';
108 if (global.randRoom(1, 8) == 1) return 'oPistol';
109 if (global.randRoom(1, 8) == 1) return 'oMachete';
114 void generatePrizes () {
115 foreach (ref name pname; prizes) pname = '';
116 // select sequence of next 9 prizes
117 foreach (auto idx, ref name pname; prizes) {
118 // don't generate dupliate prizes
120 name prz = scrGeneratePrize();
122 foreach (int cidx; 0..idx) if (prizes[cidx] == prz) { found = true; break; }
132 bool hasAnyPrize () {
133 foreach (name pname; prizes) if (pname) return true;
134 return !!level.findCrapsPrize();
139 return namelist[global.randOther(0, namelist.length-1)];
143 override bool initialize () {
144 if (!::initialize()) return false;
145 setSprite('sShopLeft');
146 auto spf = getSpriteFrame();
147 setCollisionBounds(2, 0, spf.width-2, spf.height);
148 cash = 5000*(global.levelType+1)+global.randRoom(0, global.currLevel*2000);
149 betValue = 1000+global.currLevel*500;
150 kissValue = 10000+5000*(global.currLevel-2);
151 //initFixDirection();
152 shopkeeperName = genName();
153 if (level.levelKind == GameLevel::LevelKind.Stars) {
159 writeln("*** generated Shopkeeper at tile (", ix/16, ",", iy/16, ")");
165 override void onAnimationLooped () {
166 if (spriteLName == 'sShopThrowL') {
168 setSprite('sShopLeft');
173 override bool onExplosionTouch (MapObject xplo) {
174 if (invincible) return false;
175 writeln("shopkeeper touched by an explosion");
177 if (xplo.fltx < fltx) xVel = global.randOther(4, 6); else xVel = -global.randOther(4, 6);
180 hp -= global.config.explosionDmg;
182 //if (hp <= 0 && !leavesBody) instanceRemove();
184 //FIXME: move anger code to separate method and call it here
190 override bool onTouchedByPlayer (PlayerPawn plr) {
191 if (dead || status == DEAD) return false;
192 if (!angered) return false;
193 return ::onTouchedByPlayer(plr);
197 // return `false` to do standard weapon processing
198 override bool onTouchedByPlayerWeapon (PlayerPawn plr, PlayerWeapon wpn) {
199 if (heldBy) return true;
200 if (hp > 0 && !dead && !stunned && status < STUNNED) wpn.hitEnemy = true;
210 xVel = (plr.xCenter < xCenter ? 1 : -1);
211 plr.playSound('sndHit');
212 if (status < STUNNED && !wpn.slashing) status = ATTACK;
217 override void onBulletHit (ObjBullet bullet) {
220 writeln("shopkeeper hit by bullet");
221 if (heldBy) heldBy.holdItem = none;
233 xVel = bullet.xVel*0.3;
235 //FIXME: move anger code to separate method and call it here
239 // return `false` to prevent
240 // owner is usually a player
241 override bool onLostAsHeldItem (MapObject owner, LostCause cause, optional float xvel, optional float yvel) {
244 if (cause == LostCause.Whoa) {
245 xVel = (owner.dir == Dir.Left ? -2 : 2);
250 if (cause == LostCause.Drop) {
253 } else if (cause == LostCause.Dead || cause == LostCause.Stunned) {
259 if (specified_xvel) xVel = xvel;
260 if (specified_yvel) yVel = yvel;
266 override void onBeforeThrowBy (PlayerPawn plr) {
267 if (plr./*kDown*/scrPlayerIsDucking()) {
272 // throw - damsel freaks out after stun
276 if (hp > 0) playSound('sndHit');
281 // return `true` if item hits the character
282 // this is called after item's event
283 //TODO: he should take away player's shotgun
284 //TODOK8: he should take and use pistols
285 override bool onHitByItem (MapItem item) {
286 if (item isa ItemWeaponShotgun && hp > 0 && status == ATTACK && !hasGun) {
288 hisGunIsAsh = (item isa ItemWeaponAshShotgun);
289 item.instanceRemove();
293 //writeln("char: acc=(", xAcc, ",", yAcc, "); vel=(", xVel, ",", yVel, ")");
294 //writeln("item: acc=(", item.xAcc, ",", item.yAcc, "); vel=(", item.xVel, ",", item.yVel, ")");
296 // don't hit friendly shopkeeper with items we're putting down
297 if (!angered && (status == IDLE || status == FOLLOW) && item.safe) return false;
299 if (item isa ItemDice && item.forSale) return false; // don't hit with a die
301 // are we invincible?
302 if (invincible) return false;
303 // any items will fly thru stunned enemies (but not projectiles)
304 if (status >= STUNNED && item !isa ItemProjectile) return false;
305 if (item isa ItemProjectile && !ItemProjectile(item).canHarm(self)) return false;
306 // item does no damage?
307 if (!item.damage || item.forSale) return false;
309 if (fabs(item.xVel) < ItemSpeedToHitX && fabs(item.yVel) < ItemSpeedToHitY) return false;
311 if (heldBy) heldBy.holdItem = none;
313 if (status == STUNNED) {
314 if (fabs(xVel) < 1) xVel = (item.xVel < 0 ? -1.5 : 1.5);
315 yVel = (self isa ItemProjectile ? -1.5 : -1.0);
317 xVel = item.xVel*0.3;
318 yVel = (self isa ItemProjectile ? -6.0 : -2.0);
322 auto proj = ItemProjectile(item);
323 if (proj) countsAsKill = proj.launchedByPlayer;
325 if (!dead) spillBlood(amount:2);
330 item.playSound('sndHit');
332 return true; // it hit us
337 // return `true` to stop player from holding it
338 override bool onTryPickup (PlayerPawn plr) {
339 //writeln("try to pickup; res=", (dead || status >= STUNNED));
340 return !(dead || status >= STUNNED);
345 // various side effects
346 // called only if object was succesfully put into player hands
347 override void onPickedUp (PlayerPawn plr) {
348 writeln("shopkeeper picked up");
352 void fireShotgun () {
357 if (dir == Dir.Left) {
361 auto blast = level.MakeMapObject(x, y+9, 'oShotgunBlast');
362 if (blast) blast.setSprite('sShotgunBlastLeft');
367 auto blast = level.MakeMapObject(x+16, y+9, 'oShotgunBlast');
368 if (blast) blast.setSprite('sShotgunBlastRight');
371 if (level.isSolidAtPoint(x+xofs, y+8)) return;
374 auto obj = level.MakeMapObject(x+xofs, y+8, 'oBullet');
376 obj.xVel = (xsgn*global.randOther(6, 8))+xVel;
378 if (obj.xVel >= -6) obj.xVel = -6;
380 if (obj.xVel < 6) obj.xVel = 6;
382 //obj.yVel = random(1)-random(1);
383 obj.yVel = global.randOtherFloat()-global.randOtherFloat();
389 // return true if this shopkeeper can process the item
390 // this won't be called for dead or angered shopkeepers
391 bool canSellItem (PlayerPawn plr, MapObject o) {
393 // holding nothing: this may be dice bet, or kissing parlor
394 return (style == 'Craps' || style == 'Kissing');
397 auto die = ItemDice(o);
398 if (die) return (die.forSale && style == 'Craps');
400 // sell items to shopkeeper?
403 //if (o.sellingToShopAllowed && price > 0) return true;
404 //if (!o.sellingToShopAllowed || price < 1) return false;
412 MonsterDamsel findSlaveDamsel () {
413 return MonsterDamsel(level.findNearestObject(ix, iy, delegate bool (MapObject o) {
414 auto sc = MonsterDamsel(o);
415 if (!sc) return false;
416 if (sc.dead || !sc.forSale && sc.heldBy) return false;
418 }, castClass:MonsterDamsel));
422 // return true to "use" item
423 // this won't be called for dead or angered shopkeepers
424 bool doSellItem (PlayerPawn plr, MapObject o) {
426 // holding nothing: this may be dice bet, or kissing parlor
427 if (style == 'Craps') {
429 // don't allow player to own the item
433 if (style == 'Kissing') {
434 if (!isPlayerInOurShop()) return false;
435 auto dms = findSlaveDamsel();
436 //writeln("11: STYLE: ", style, "; dms: ", (dms ? "TAN" : "ONA"));
437 if (!dms) return false;
438 //writeln("12: STYLE: ", style, "; dist=", level.player.distanceToEntity(dms));
439 if (level.player.distanceToEntity(dms) > 16 || global.thiefLevel > 0 || global.murderer) return false;
440 if (level.stats.money < kissValue) {
441 level.osdMessageTalk(va("YOU NEED |%d| GOLD!\nGET OUTTA HERE, DEADBEAT!", kissValue), replace:true, inShopOnly:false);
444 //writeln("13: STYLE: ", style, "; dms: ", GetClassName(dms.Class));
445 //!!if (isRealLevel()) global.kissesBought += 1;
447 dms.setSprite(global.isDamsel ? 'sPKissL' : 'sDamselKissL');
448 level.stats.takeMoney(kissValue);
450 //!if (isRealLevel()) global.moneySpent += getKissValue();
452 level.osdMessageTalk((global.isDamsel ? "NOW AIN'T HE SWEET!" : "NOW AIN'T SHE SWEET!"), replace:true, inShopOnly:false);
460 writeln("trying to buy '", GetClassName(o.Class), "'");
462 writeln("trying to sell '", GetClassName(o.Class), "'");
466 auto die = ItemDice(o);
469 // don't allow player to own the item
473 // selling various items
476 if (!o.sellingToShopAllowed || price < 1) {
479 //writeln("price not set for ", (o.shopDesc ? o.shopDesc : string(o.objName).toUpperCase));
480 level.osdMessageTalk("I WON'T BUY IT EVEN IF YOU WILL PAY ME FOR THAT!", replace:true, timeout:-1);
481 } else /*if (!o.sellingToShopAllowed && price < 1)*/ {
482 level.osdMessageTalk("YOU SHOULD NOT SELL IT!", replace:true, timeout:-1);
486 //!!!if (!(status == IDLE && abs(plr.ix-ix) < 80)) return false; // shopkeeper is busy or too far
487 if (status != IDLE && status != FOLLOW) return false; // shopkeeper is busy
488 price = max(1, round(price*0.894));
490 if (!o.sellOfferDone) {
492 level.osdMessageTalk(va("SORRY, I CAN'T BUY |%s| FOR |%d| GOLD.\nI HAVEN'T GOT ENOUGH MONEY!", (o.shopDesc ? o.shopDesc : string(o.objName).toUpperCase), price), replace:true, timeout:-1);
495 level.osdMessageTalk(va("SELL |%s| FOR |%d| GOLD?\nPRESS $PAY AGAIN TO CONFIRM.", (o.shopDesc ? o.shopDesc : string(o.objName).toUpperCase), price), replace:true, timeout:-1);
496 o.sellOfferDone = true;
500 o.sellOfferDone = false;
502 level.osdMessageTalk(va("SORRY, I CAN'T BUY |%s| FOR |%d| GOLD.\nI HAVEN'T GOT ENOUGH MONEY!", (o.shopDesc ? o.shopDesc : string(o.objName).toUpperCase), price), replace:true, timeout:-1);
505 if (o.heldBy) o.heldBy.holdItem = none;
508 plr.playSound('sndCoin');
509 level.MakeMapObject(plr.ix, plr.iy-8, 'oBigCollect');
510 level.osdMessageTalk("~PLEASURE DOING BUSINESS!~", replace:true, inShopOnly:false, timeout:4);
511 auto idol = ItemGoldIdol(o);
512 if (idol) idol.registerConverted(); // stats
517 if (!o.forSale) FatalError("oShopkeeper::doSellItem: the thing that should not be! (0)");
519 if (o.shopType != style) {
521 writeln("SELL-SKIP: me is ", style, "; item is from ", o.shopType);
525 if (level.stats.money < plr.holdItem.cost) {
526 level.osdMessageTalk("YOU HAVEN'T GOT ENOUGH MONEY!", replace:true);
529 plr.playSound('sndCoin');
530 // take away some money
531 level.stats.takeMoney(o.cost);
532 // give some money to shopkeeper
534 // allow player to use the item
536 level.osdMessageTalk("~PLEASURE DOING BUSINESS!~", replace:true, inShopOnly:false, timeout:4);
541 // if we hit a bank, we cannot play anymore
542 bool isCrapsBetAllowed (PlayerPawn plr) {
543 if (style != 'Craps') return false;
544 if (plr.bet) return true; // already did
545 if (!isPlayerInOurShop()) return false;
546 // "no prize" probably means that we hit a bank
547 return hasAnyPrize();
551 void doCrapsBet (PlayerPawn plr) {
552 if (plr.bet) return; // already did
553 if (!isPlayerInOurShop()) return; // oops
555 if (!isCrapsBetAllowed(plr)) return;
556 if (level.stats.money >= betValue) {
557 level.stats.takeMoney(betValue);
561 level.osdMessageTalk("YOU HAVEN'T GOT ENOUGH MONEY!", replace:true);
566 // returns current prize, creates new one
567 MapObject getCrapsPrize (PlayerPawn plr, optional bool generateNew) {
568 auto prize = level.findCrapsPrize();
569 if (!prize) return none;
570 prize.forSale = false;
571 prize.inDiceHouse = false;
572 createCrapsPoofAt(prize.ix, prize.iy);
573 if (generateNew || !specified_generateNew) {
574 bool foundIt = false;
575 foreach (auto idx, ref name pname; prizes) {
577 auto np = level.MakeMapObject(prize.ix, prize.iy, pname);
580 np.inDiceHouse = true;
581 np.shopType = 'Craps';
588 //if (!foundIt) level.osdMessageTalk("SORRY, WE DON'T HAVE PRIZES ANYMORE!");
590 // put prize near the player
591 if (level.isInShop(prize.ix/16-3, prize.iy/16)) {
593 } else if (level.isInShop(prize.ix/16+3, prize.iy/16)) {
599 createCrapsPoofAt(prize.ix, prize.iy);
604 void createCrapsPoofAt (int x, int y) {
605 auto obj = level.MakeMapObject(x-4, y+6, 'oPoof');
606 if (obj) obj.xVel = -0.4;
607 obj = level.MakeMapObject(x+4, y+6, 'oPoof');
608 if (obj) obj.xVel = 0.4;
612 final void resetAllDices () {
614 level.forEachObject(delegate bool (MapObject o) {
615 if (!o.forSale || o.spectral || !o.isInstanceAlive) return false;
616 auto dc = ItemDice(o);
617 if (!dc) return false;
618 if (dc.rollState != ItemDice::RollState.Rolling) {
619 dc.resetRollState(); // allow to throw it again
626 transient int rval, rcount, rtotal;
629 final int checkAllDices () {
634 level.forEachObject(delegate bool (MapObject o) {
635 if (!o.forSale || o.spectral || !o.isInstanceAlive) return false;
636 auto dc = ItemDice(o);
637 if (!dc) return false;
639 if (dc.heldBy) return false;
640 auto rv = dc.getRollNumber();
648 if (rcount == 0 || rcount != rtotal) return 0; // not yet
653 // return `true` to stop checking other shopkeepers
654 // this will be called for *EACH* shopkeeper, sorry
655 bool onDiePlayed (PlayerPawn plr, ItemDice die) {
656 if (style != 'Craps') return false;
657 if (angered || dead) return false;
658 if (!die.forSale) return false;
662 die.resetRollState(); // allow to throw it again
666 auto rval = checkAllDices();
667 if (!rval) return false;
669 writeln("ROLL: ", rval);
676 ++level.stats.totalDiceGamesWonPrize;
677 plr.playSound('sndChestOpen');
678 //cash += bet; // already taken in `doCrapsBet()`
679 level.osdMessageTalk("YOU ROLLED A |SEVEN|!\n~YOU WIN A PRIZE!~", replace:true, inShopOnly:false);
681 } else if (rval > 7) {
683 ++level.stats.totalDiceGamesWon;
684 level.osdMessageTalk(va("YOU ROLLED A |%d|!\nCONGRATULATIONS! YOU ~WIN~!", rval), replace:true, inShopOnly:false);
685 plr.playSound('sndCoin');
689 ++level.stats.totalDiceGamesWonPrize;
691 level.osdMessageTalk("YOU BROKE THE BANK!\n~TAKE THE PRIZE~!", replace:true, inShopOnly:false);
692 plr.playSound('sndChestOpen');
693 getCrapsPrize(plr, generateNew:false);
695 } else if (rval < 7) {
696 ++level.stats.totalDiceGamesLost;
697 level.osdMessageTalk(va("YOU ROLLED A |%d|!\nI'M SORRY, BUT YOU ~LOSE~!", rval), replace:true, inShopOnly:false, hiColor1:0xff_00_00);
698 plr.playSound('sndThud');
699 //cash += bet; // already taken in `doCrapsBet()`
706 void doCrapsShop () {
707 auto plr = level.player;
709 if (isPlayerInOurShop()) {
711 auto die = ItemDice(plr.holdItem);
713 if (!die.forSale) return;
715 level.osdMessageTalk("THROW A DICE, PLEASE.", replace:true);
717 if (!isCrapsBetAllowed(plr)) {
718 level.osdMessageTalk("SORRY, I AM OUT OF BUSINESS.", replace:true);
721 if (level.stats.money < betValue) {
722 level.osdMessageTalk(va("YOU DON'T HAVE ENOUGH MONEY\nTO BET |%d| GOLD.", betValue), replace:true);
724 level.osdMessageTalk(va("PRESS $PAY TO BET |%d| GOLD.", betValue), replace:true);
731 level.osdMessageTalk("THROW A DICE, PLEASE.", replace:true);
733 if (!level.osdGetTalkMessage()) {
734 if (!isCrapsBetAllowed(plr)) {
735 level.osdMessageTalk("SORRY, I AM OUT OF BUSINESS.", replace:true);
738 if (level.stats.money < betValue) {
739 level.osdMessageTalk(va("YOU DON'T HAVE ENOUGH MONEY\nTO BET |%d| GOLD.", betValue), replace:true);
741 level.osdMessageTalk(va("PRESS $PAY TO BET |%d| GOLD.", betValue), replace:true);
748 // don't steal a prize
749 auto obj = MapItem(plr.holdItem);
750 if (obj && obj.forSale) {
751 //writeln("0:item '", GetClassName(obj.Class), "'; inshop=", level.isInShop(level.player.ix/16, level.player.iy/16));
752 if (obj !isa ItemDice && !level.isInShop(level.player.ix/16, level.player.iy/16)) {
753 level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
759 void dropShotgun () {
761 auto obj = level.MakeMapObject(ix+8, iy+8, (hisGunIsAsh ? 'oAshShotgun' : 'oShotgun'));
763 obj.yVel = global.randOther(4, 6);
764 obj.xVel = global.randOther(4, 6)*(xVel < 0 ? -1 : 1);
775 level.forEachObject(delegate bool (MapObject o) {
776 if (o.forSale && o.shopType == style) o.forSale = false;
784 final bool cbCheckMoveableNearby (MapTile t) {
785 if (!t.solid || !t.moveable) return false;
786 // check if it has a player nearby
787 //writeln("SK-MOVEABLE: dx0=", t.x0-level.player.x0, "; dx1=", level.player.x1-t.x1, "; dy=", level.player.y1-t.y1);
788 if (abs(level.player.y1-t.y1) < 4) {
789 int dx0 = t.x0-level.player.x0;
790 if (dx0 >= 10 && dx0 <= 14) return true;
791 int dx1 = level.player.x1-t.x1;
792 if (dx1 >= 10 && dx1 <= 14) return true;
793 //if ((dx < 0 && dx < -16-4) || (dx >= 0 && dx < 4)) return true;
799 bool isPlayerInOurShop () {
800 if (outlaw) return false;
801 int ptx = level.player.ix/16, pty = level.player.iy/16;
802 if (!level.isInShop(ptx, pty)) return false;
803 return (style == level.lg.roomShopType(ptx, pty));
807 void setFollowStates () {
808 auto plr = level.player;
809 auto item = MapItem(plr.holdItem);
811 if (!item.forSale) return;
812 if (item isa ItemDice) return; // never ever
813 //writeln("0:item '", GetClassName(obj.Class), "'; inshop=", level.isInShop(level.player.ix/16, level.player.iy/16));
814 int px = level.player.ix, py = level.player.iy;
815 int ptx = px/16, pty = py/16;
816 if (!level.isInShop(ptx, pty)) {
817 //k8: anger all shopkeepers, why not?
818 level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
821 //auto sk = level.findNearestCalmShopkeeper(level.player.ix, level.player.iy);
822 //if (sk) sk.status = FOLLOW; else status = FOLLOW;
823 //auto pstyle = level.lg.roomShopType(level.player.ix/16, level.player.iy/16);
824 foreach (MonsterShopkeeper sk; level.objGrid.inRectPix(px-176, py-176, 176*2, 176*2, precise:false, castClass:MonsterShopkeeper)) {
825 if (sk.dead || sk.angered || sk.outlaw || sk.style == 'Craps') continue;
826 if (sk.status != IDLE && sk.status != FOLLOW) continue;
827 //if (sk.style != level.lg.roomShopType(level.player.ix/16, level.player.iy/16)) continue;
828 if (item.shopType && item.shopType != sk.style) continue;
834 override bool onSpearTrapHit (MapObject spear) {
835 if (heldBy) heldBy.holdItem = none;
837 countsAsKill = false;
841 xVel = (spear.ix+8 < ix+8 ? 4 : -4);
842 spear.playSound('sndHit');
843 hp -= global.config.spearDmg;
850 override void thinkFrame () {
852 //if (hitByArrow) writeln("after ::think hp=", hp);
853 if (!isInstanceAlive) return;
855 if (status == THROWN) {
864 if (level.isInShop(level.player.ix/16, level.player.iy/16)) {
865 writeln("me: ", style, "; player is in ", level.lg.roomShopType(level.player.ix/16, level.player.iy/16));
870 yVel = fmin(yVel+myGrav, 8);
874 bool colLeft = !!isCollisionLeft(1);
875 bool colRight = !!isCollisionRight(1);
876 bool colBot = !!isCollisionBottom(1);
877 bool colTop = !!isCollisionTop(1);
879 if (colBot && status != STUNNED) yVel = 0;
881 if (throwCount > 0) --throwCount;
887 if ((status >= STUNNED && level.isSolidAtPoint(x+8, y+12)) ||
888 (status < STUNNED && level.isSolidAtPoint(x+8, y+8)))
891 playSound('sndCavemanDie');
892 if (hp > 0 && countsAsKill) level.addKill(objName);
893 if (!outlaw) global.murderer = true;
894 if (status != DEAD && !dead) dropShotgun();
899 // check if player is cornered shopeekeper with a moveable block
900 if (!global.config.optShopkeeperIdiots &&
901 !angered && !outlaw && (status == IDLE || status == FOLLOW) &&
902 !level.player.dead && !level.player.stunned && distanceToEntity(level.player) < 96+16) {
903 auto leftsolid = level.isSolidAtPoint(x-8, y+8); //, delegate (bool) (MapTile t) { return (t.solid && !t.moveable); });
904 auto rightsolid = level.isSolidAtPoint(x+16+8, y+8); //, delegate (bool) (MapTile t) { return (t.solid && !t.moveable); });
905 if (leftsolid || rightsolid) {
906 //writeln("checking for cornered shopkeeper... (l=", !!leftsolid, "; r=", !!rightsolid, "); y=", y, "; y0=", y0, "; h=", height);
907 // some direction is blocked; check if there is a solid nearby, and a player behind it
908 //foundSolidAndPlayer = false;
909 int warnLength = 2*16;
910 auto stl = level.checkTilesInRect(x0-warnLength, y0, warnLength, height, &cbCheckMoveableNearby);
911 auto str = level.checkTilesInRect(x1, y0, warnLength, height, &cbCheckMoveableNearby);
912 if ((str && leftsolid) || (stl && rightsolid)) {
913 writeln("SHOPKEEPER IS CORNERED!");
921 if (status != DEAD && status != STUNNED && hp < 1) status = DEAD;
923 auto plr = level.player;
924 auto dist = distanceToEntityCenter(plr);
926 if (status == IDLE || status == FOLLOW) {
927 auto item = MapItem(plr.holdItem);
928 if (item && item !isa ItemDice && item.forSale && item.shopType == style) {
929 level.osdMessageTalk(va("BUY |%s| FOR |%d| GOLD.\nPRESS $PAY TO PURCHASE.", item.shopDesc, item.cost), replace:true);
933 if (status == PATROL || status == WALK) {
934 if (!plr.dead && plr.visible && dist <= 96 && plr.iy-(y+8) < 16) {
936 } else if (abs(plr.ix-(x+8)) < 4) {
941 // pickup shotgun (or get it from the player, lol)
942 if (hp > 0 && !hasGun && (status == PATROL || status == WALK || status == ATTACK)) {
943 level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
944 if (o isa ItemWeaponShotgun) {
945 if (o.heldBy) o.heldBy.holdItem = none;
947 hisGunIsAsh = (o isa ItemWeaponAshShotgun);
954 if (!hasGun && level.player.holdItem isa ItemWeaponShotgun && plr.collidesWith(self)) {
955 auto o = level.player.holdItem;
956 if (o.heldBy) o.heldBy.holdItem = none;
958 hisGunIsAsh = (o isa ItemWeaponAshShotgun);
961 // ...and from a pocket too
962 if (!hasGun && level.player.pickedItem isa ItemWeaponShotgun && plr.collidesWith(self)) {
963 auto o = level.player.pickedItem;
964 if (o.heldBy) o.heldBy.holdItem = none;
965 level.player.pickedItem = none;
967 hisGunIsAsh = (o isa ItemWeaponAshShotgun);
972 if (status == IDLE) {
975 if (colLeft) fltx += 1;
976 if (colRight) fltx -= 1;
977 x = ix; // update cached value
979 if (colLeft && colRight) status = ATTACK;
981 dir = (plr.ix < x+8 ? Dir.Left : Dir.Right);
983 if (yVel < 0 && colTop) yVel = 0;
985 if (global.murderer || global.thiefLevel > 0) {
987 } else if (isPlayerInOurShop() && (!welcomed || level.player.shopType != style || !level.osdGetTalkMessage())) {
988 //writeln("::: SHOP TYPE: ", level.lg.roomShopType(level.player.ix/16, level.player.iy/16));
989 writeln("*** WELCOME TO SHOP: ", style);
993 if (style == 'Bomb') msg = "WELCOME TO |"~shopkeeperName~"'S| ~BOMB~ SHOP!";
994 else if (style == 'Weapon') msg = "WELCOME TO |"~shopkeeperName~"'S| ~ARMORY~!";
995 else if (style == 'Clothing') msg = "WELCOME TO |"~shopkeeperName~"'S| ~CLOTHING~ SHOP!";
996 else if (style == 'Rare') msg = "WELCOME TO |"~shopkeeperName~"'S| ~SPECIALTY~ SHOP!";
997 else if (style == 'Craps') msg = "WELCOME TO |"~shopkeeperName~"'S| ~DICE HOUSE~!";
998 else if (style == 'Kissing') { msg = "WELCOME TO |"~shopkeeperName~"'S| ~KISSING PARLOR~!"; hi2 = 0xff_00_00; }
999 else if (style == 'Ankh') msg = "I HAVE ~SOMETHING SPECIAL~...";
1000 else msg = "WELCOME TO |"~shopkeeperName~"'S| SUPPLY SHOP!";
1002 if (style == 'Craps') {
1003 msg = va("%s\nPRESS $PAY TO BET |%d| GOLD.", msg, betValue);
1004 } else if (style == 'Kissing') {
1005 msg = va("%s\n|%d| GOLD A KISS. PRESS $PAY.", msg, kissValue);
1008 if (msg) level.osdMessageTalk(msg, replace:true, hiColor2:hi2);
1015 if (!angered && !outlaw && (status == IDLE || status == FOLLOW || !welcomed)) {
1016 if (!welcomed && level.isInShop(level.player.ix/16, level.player.iy/16) && level.lg.roomShopType(level.player.ix/16, level.player.iy/16) == style) welcomed = true;
1017 if (style == 'Craps') {
1019 } else if (plr.holdItem) {
1020 auto obj = MapItem(plr.holdItem);
1021 if (obj && obj.forSale) setFollowStates();
1025 if (status == FOLLOW) {
1026 if (style != 'Craps' && plr.holdItem) {
1027 auto obj = MapItem(plr.holdItem);
1028 if (obj && obj.forSale) {
1030 } else if (!obj || !obj.forSale) {
1031 writeln("STOP FOLLOWING");
1038 if (colLeft || colRight) dir ^= 1;
1040 if (turnTimer > 0) {
1042 } else if (abs(plr.iy-(y+8)) < 8 && colBot && dist > 16) {
1043 dir = (plr.ix < x ? Dir.Left : Dir.Right);
1047 float i = dist/16.0*1.5;
1048 xVel = fclamp((dir == Dir.Left ? -i : i), -3, 3);
1050 if (dist < 12 || plr.iy < y) xVel = 0;
1053 auto obj = plr.holdItem;
1054 if (!obj.forSale || obj isa ItemDice) status = IDLE;
1058 } else if (status == PATROL) {
1061 if (yVel < 0 && colBot) yVel = 0;
1063 if (colBot && counter > 0) --counter;
1065 dir = global.randOther(0, 1);
1068 } else if (status == WALK) {
1071 if (colLeft || colRight) dir ^= 1;
1073 xVel = (dir == Dir.Left ? -1.5 : 1.5);
1074 if (level.isSolidAtPoint(x+(dir == Dir.Left ? -1 : 16), y/*, -1, -1*/)) {
1076 counter = global.randOther(20, 50);
1080 if (global.randOther(1, 100) == 1) {
1082 counter = global.randOther(20, 50);
1085 } else if (status == ATTACK) {
1088 if (!angered) angerIt();
1090 if (turnTimer > 0) {
1092 } else if (abs(plr.iy-(y+8)) < 8 && colBot && dist > 16) {
1093 dir = (plr.ix < x ? Dir.Left : Dir.Right);
1097 if (colLeft || colRight) dir ^= 1;
1099 xVel = (dir == Dir.Left ? -3 : 3);
1104 } else if (abs(plr.iy-(y+8)) < 32) {
1105 if ((dir == Dir.Left && plr.ix < x+8 && dist < 96) ||
1106 (dir == Dir.Right && plr.ix > x+8 && dist < 96))
1110 xVel += (dir == Dir.Left ? 3 : -3);
1111 playSound('sndShotgun');
1118 if (!tryToJumpOut && plr.iy > y && abs(plr.ix-(x+8)) < 64) {
1120 } else if ((dir == Dir.Left && level.isSolidAtPoint(x-16, y)) ||
1121 (dir == Dir.Right && level.isSolidAtPoint(x+32, y)))
1123 if (colBot && !isCollisionTop(4)) {
1124 yVel = -global.randOther(7, 8);
1125 tryToJumpOut = false;
1127 //else { if (facing == LEFT) xVel = -1.5; else xVel = 1.5; }
1128 } else if (plr.iy <= y+16 &&
1129 ((dir == Dir.Left && !level.isSolidAtPoint(x-16, y+16)) ||
1130 (dir == Dir.Right && !level.isSolidAtPoint(x+32, y+16))))
1132 if (colBot && !isCollisionTop(4)) {
1133 yVel = -global.randOther(7, 8);
1134 tryToJumpOut = false;
1138 if (!colBot && plr.iy > y+8) {
1139 xVel = (dir == Dir.Left ? -1.5 : 1.5);
1142 if (plr.dead || !plr.visible) status = WALK;
1143 } else if (status == STUNNED) {
1145 setSprite('sShopStunL');
1146 } else if (bounced) {
1147 setSprite(yVel < 0 ? 'sShopBounceL' : 'sShopFallL');
1149 setSprite(xVel < 0 ? 'sShopDieLL' : 'sShopDieLR');
1152 if (colBot && !bounced) {
1154 spillBlood(amount:1);
1157 if (heldBy || colBot) {
1160 } else if (hp > 0) {
1162 if (heldBy) heldBy.holdItem = none;
1165 if (spriteLName == 'sShopStunL') {
1167 if (counter > 0 && counter < 30) imageSpeed = 0.8;
1171 } else if (status == DEAD) {
1173 /*if (isRoom("rStars")) {
1174 if (oStarsRoom.kills < 99) oStarsRoom.kills += 1;
1176 if (countsAsKill) level.addKill(objName);
1178 if (isRealLevel()) global.enemyKills[19] += 1;
1179 global.shopkeepers += 1;
1182 if (level.levelKind != GameLevel::LevelKind.Stars) {
1184 global.murderer = true;
1185 if (false /*!isRealLevel()*/) {
1187 repeat(rand(1, 4)) {
1188 obj = instance_create(x+8, y+8, oGoldNugget);
1190 obj.xVel = rand(1, 3)-rand(1, 3);
1193 } else if (style != 'Bounty Hunter') {
1194 if (style == 'Craps') {
1201 while (cash > loot) {
1203 if (cash >= loot+5000+trunc(ceil(5000.0/4.0)*global.levelType)) {
1204 obj = level.MakeMapObject(x+8, y+8, chooseName('oGoldNugget', 'oGoldBar', 'oGoldBars', 'oEmeraldBig', 'oSapphireBig', 'oRubyBig', 'oDiamond'));
1205 } else if (cash >= loot+1600+trunc(ceil(1600.0/4.0)*global.levelType)) {
1206 obj = level.MakeMapObject(x+8, y+8, chooseName('oGoldNugget', 'oGoldBar', 'oGoldBars', 'oEmeraldBig', 'oSapphireBig', 'oRubyBig'));
1207 } else if (cash >= loot+1000+trunc(ceil(1000.0/4.0)*global.levelType)) {
1208 obj = level.MakeMapObject(x+8, y+8, chooseName('oGoldNugget', 'oGoldBar', 'oGoldBars'));
1210 obj = level.MakeMapObject(x+8, y+8, 'oGoldChunk');
1214 obj.xVel = global.randOther(1, 4)-global.randOther(1, 4);
1216 writeln("SHOPKEEPER LOOT: WTF?!");
1218 loot += obj.value+trunc(ceil(obj.value/4.0)*global.levelType);
1223 foreach (; 0..global.randOther(1, 4)) {
1224 auto obj = level.MakeMapObject(x+8, y+8, 'oGoldNugget');
1227 obj.xVel = global.randOther(1, 3)-global.randOther(1, 3);
1234 foreach (; 0..global.randOther(3, 6)) {
1235 auto obj = level.MakeMapObject(x+8, y+8, chooseName('oGoldNugget', 'oGoldNugget', 'oGoldBar', 'oGoldBar', 'oGoldBars', 'oEmeraldBig', 'oSapphireBig', 'oRubyBig'));
1238 obj.xVel = global.randOther(1, 3)-global.randOther(1, 3);
1244 playSound('sndCavemanDie');
1247 setSprite('sShopDieL');
1248 if (fabs(xVel) > 0 || fabs(yVel) > 0) status = STUNNED;
1251 if (status >= STUNNED) {
1253 scrCheckCollisions();
1254 if (xVel == 0 && yVel == 0 && hp < 1) status = DEAD;
1255 //k8: it should be something like the following, but...
1257 if (fabs(xVel) <= 0.001 && fabs(yVel) <= 0.001 && hp < 1) {
1265 //if (isCollisionSolid()) y -= 2;
1267 if (xVel > 0) xVel -= 0.1;
1268 if (xVel < 0) xVel += 0.1;
1269 if (fabs(xVel) < 0.5) xVel = 0;
1271 if (status < STUNNED && status != THROWN) {
1272 setSprite(fabs(xVel) > 0 ? 'sShopRunLeft' : 'sShopLeft');
1276 setSprite(hp > 0 ? 'sShopHeldL' : 'sShopDHeldL');
1279 if (dead && deathTimer > 0 && level.levelKind == GameLevel::LevelKind.Stars) {
1280 if (--deathTimer == 0) {
1289 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
1290 ::drawWithOfs(xpos, ypos, scale, currFrameDelta);
1292 if (hasGun && status != IDLE && status != FOLLOW) {
1294 getInterpCoords(currFrameDelta, scale, out xi, out yi);
1296 auto spr = level.sprStore[dir == Dir.Left ? 'sShotgunLeft' : 'sShotgunRight'];
1297 auto spf = spr.frames[0];
1298 xi -= spf.xofs*scale;
1299 yi -= spf.yofs*scale;
1300 spf.tex.blitAt(xi-xpos+(dir == Dir.Left ? 6 : 10)*scale, yi-ypos+10*scale, scale);
1306 objName = 'Shopkeeper';
1307 //!style = "General";
1308 desc = "Shopkeeper";
1309 desc2 = "The only thing deadlier than this saleman's twelve gauge is his temper.";
1311 //setCollisionBounds(2, 0, sprite_width-2, sprite_height);
1344 stunMax = 100; //?!!!
1348 removeCorpse = false; // only applies to enemies that have corpses (Caveman, Yeti, etc.)
1349 deathTimer = 200; // how many steps after death until corpse is removed
1351 countsAsKill = true; // sometimes it's not the player's fault!
1355 canBeHitByBullet = true;
1356 canBeNudged = false;
1361 doBasicPhysics = false;
1363 canBeStunned = true;
1364 checkInsideBlock = false; // we'll do our own check
1367 namelist[$] = "AHKMED";
1368 namelist[$] = "TERRY";
1369 namelist[$] = "SMITHY";
1370 namelist[$] = "KASIM";
1371 namelist[$] = "DOUG";
1372 namelist[$] = "BEN";
1373 namelist[$] = "KERT";
1374 namelist[$] = "DUKE";
1375 namelist[$] = "TOBY";
1376 namelist[$] = "GUERT";
1377 namelist[$] = "PANCHO";
1378 namelist[$] = "EARL";
1379 namelist[$] = "IVAN";
1380 namelist[$] = "OLLIE";
1381 namelist[$] = "IDO";
1382 namelist[$] = "BOB";
1383 namelist[$] = "RUDY";
1384 namelist[$] = "JIMBO";
1385 namelist[$] = "ERIC";
1386 namelist[$] = "WILLY";
1387 namelist[$] = "MATT";
1388 namelist[$] = "LAZLO";
1389 namelist[$] = "WANG";
1390 namelist[$] = "PETER";
1391 namelist[$] = "ANDY";
1392 namelist[$] = "DONG";
1393 namelist[$] = "LEMMY";
1394 namelist[$] = "OMAR";
1395 namelist[$] = "VADIM";
1396 namelist[$] = "TARN";
1397 namelist[$] = "SLASH";
1398 namelist[$] = "LANCE";
1399 namelist[$] = "ALEC";
1400 namelist[$] = "NOEL";
1401 namelist[$] = "KYLE";
1402 namelist[$] = "DEREK";
1403 namelist[$] = "RICH";
1404 namelist[$] = "JON";
1405 namelist[$] = "TOM";
1406 namelist[$] = "PHIL";
1407 namelist[$] = "CALVIN";
1408 namelist[$] = "HASSAN";
1409 namelist[$] = "PAUL";
1410 namelist[$] = "COLIN";
1411 namelist[$] = "IAN";
1412 namelist[$] = "EDDIE";
1413 namelist[$] = "JAKE";
1414 namelist[$] = "JOEY";
1415 namelist[$] = "JOSH";
1416 namelist[$] = "STEVE";