shopkeeper hit by spear trap will react accordingly now
[k8vacspelynky.git] / mapent / characters / shopkeeper.vc
blobefe72472546e4873a72135cffb4bff41c3e93586
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2010, Moloch
4  * Copyright (c) 2018, Ketmar Dark
5  *
6  * This file is part of Spelunky.
7  *
8  * You can redistribute and/or modify Spelunky, including its source code, under
9  * the terms of the Spelunky User License.
10  *
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.
13  *
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/>
17  *
18  **********************************************************************************/
19 // he is an enemy in the original, but... meh
20 class MonsterShopkeeper['oShopkeeper'] : MapEnemy;
22 name style;
23 string shopkeeperName;
25 array!string namelist;
27 int throwCount;
28 int turnTimer;
29 int stunMax;
31 int cash;
32 int betValue;
33 int kissValue;
35 int firing;
36 int firingMax;
38 bool welcomed;
39 bool angered;
40 bool outlaw;
41 bool broke; // bank
42 bool hasGun;
43 bool hisGunIsAsh;
44 bool tryToJumpOut; // if a player is trying to corner a shopkeeper, try to run and jump
47 IDLE = 0;
48 WALK = 1;
49 ATTACK = 2;
50 THROWN = 3;
51 PATROL = 4;
52 FOLLOW = 5;
53 STUNNED = 98;
54 DEAD = 99;
58 name prizes[9];
61 final name chooseName (
62   optional name n0,
63   optional name n1,
64   optional name n2,
65   optional name n3,
66   optional name n4,
67   optional name n5,
68   optional name n6,
69   optional name n7)
71   int count = 0;
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 '';
82   for (;;) {
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; }
92   }
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';
110   return 'oBombBox';
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
119     for (;;) {
120       name prz = scrGeneratePrize();
121       bool found = false;
122       foreach (int cidx; 0..idx) if (prizes[cidx] == prz) { found = true; break; }
123       if (!found) {
124         pname = prz;
125         break;
126       }
127     }
128   }
132 bool hasAnyPrize () {
133   foreach (name pname; prizes) if (pname) return true;
134   return !!level.findCrapsPrize();
138 string genName () {
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) {
154     status = ATTACK;
155     hasGun = false;
156     deathTimer = 200;
157     stunMax = 60;
158   } else {
159     writeln("*** generated Shopkeeper at tile (", ix/16, ",", iy/16, ")");
160   }
161   return true;
165 override void onAnimationLooped () {
166   if (spriteLName == 'sShopThrowL') {
167     status = ATTACK;
168     setSprite('sShopLeft');
169   }
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);
178   yVel = -6;
179   burning = 50;
180   hp -= global.config.explosionDmg;
181   spillBlood();
182   //if (hp <= 0 && !leavesBody) instanceRemove();
184   //FIXME: move anger code to separate method and call it here
186   return true;
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;
201   if (wpn.slashing) {
202     wpn.hitEnemy = true;
203     hp -= wpn.damage;
204     spillBlood();
205     status = THROWN;
206     counter = stunMax;
207   }
208   angerIt();
209   yVel = -2;
210   xVel = (plr.xCenter < xCenter ? 1 : -1);
211   plr.playSound('sndHit');
212   if (status < STUNNED && !wpn.slashing) status = ATTACK;
213   return true;
217 override void onBulletHit (ObjBullet bullet) {
218   //if (dead) return;
220   writeln("shopkeeper hit by bullet");
221   if (heldBy) heldBy.holdItem = none;
223   if (hp > 0) {
224     countsAsKill = true;
225     hp -= bullet.damage;
226   }
227   spillBlood();
229   yVel = -6;
230   status = THROWN;
231   counter = stunMax;
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) {
242   resaleValue = 0;
244   if (cause == LostCause.Whoa) {
245     xVel = (owner.dir == Dir.Left ? -2 : 2);
246     playSound('sndHit');
247     return true;
248   }
250   if (cause == LostCause.Drop) {
251     visible = true;
252     resaleValue = 0;
253   } else if (cause == LostCause.Dead || cause == LostCause.Stunned) {
254     visible = true;
255     status = THROWN;
256     counter = stunMax;
257   }
259   if (specified_xvel) xVel = xvel;
260   if (specified_yvel) yVel = yvel;
262   return true;
266 override void onBeforeThrowBy (PlayerPawn plr) {
267   if (plr./*kDown*/scrPlayerIsDucking()) {
268     status = THROWN;
269     flty -= 2;
270     counter = stunMax;
271   } else {
272     // throw - damsel freaks out after stun
273     status = THROWN;
274     counter = stunMax;
275     flty -= 4;
276     if (hp > 0) playSound('sndHit');
277   }
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) {
287     hasGun = true;
288     hisGunIsAsh = (item isa ItemWeaponAshShotgun);
289     item.instanceRemove();
290     return true;
291   }
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;
308   // speed check
309   if (fabs(item.xVel) < ItemSpeedToHitX && fabs(item.yVel) < ItemSpeedToHitY) return false;
310   // drop us
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);
316   } else {
317     xVel = item.xVel*0.3;
318     yVel = (self isa ItemProjectile ? -6.0 : -2.0);
319   }
320   hp -= damage;
321   if (hp <= 0) {
322     auto proj = ItemProjectile(item);
323     if (proj) countsAsKill = proj.launchedByPlayer;
324   }
325   if (!dead) spillBlood(amount:2);
326   yVel = -6;
327   status = THROWN;
328   counter = stunMax;
330   item.playSound('sndHit');
332   return true; // it hit us
336 // virtual
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);
344 // virtual
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 () {
353   int x = ix, y = iy;
354   int xofs, xsgn;
355   bool asleft;
357   if (dir == Dir.Left) {
358     asleft = true;
359     xofs = 4;
360     xsgn = -1;
361     auto blast = level.MakeMapObject(x, y+9, 'oShotgunBlast');
362     if (blast) blast.setSprite('sShotgunBlastLeft');
363   } else {
364     asleft = false;
365     xofs = 12;
366     xsgn = 1;
367     auto blast = level.MakeMapObject(x+16, y+9, 'oShotgunBlast');
368     if (blast) blast.setSprite('sShotgunBlastRight');
369   }
371   if (level.isSolidAtPoint(x+xofs, y+8)) return;
373   foreach (; 0..6) {
374     auto obj = level.MakeMapObject(x+xofs, y+8, 'oBullet');
375     if (!obj) continue;
376     obj.xVel = (xsgn*global.randOther(6, 8))+xVel;
377     if (asleft) {
378       if (obj.xVel >= -6) obj.xVel = -6;
379     } else {
380       if (obj.xVel < 6) obj.xVel = 6;
381     }
382     //obj.yVel = random(1)-random(1);
383     obj.yVel = global.randOtherFloat()-global.randOtherFloat();
384     //obj.safe = true;
385   }
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) {
392   if (!o) {
393     // holding nothing: this may be dice bet, or kissing parlor
394     return (style == 'Craps' || style == 'Kissing');
395   }
397   auto die = ItemDice(o);
398   if (die) return (die.forSale && style == 'Craps');
400   // sell items to shopkeeper?
401   if (!o.forSale) {
402     auto price = o.cost;
403     //if (o.sellingToShopAllowed && price > 0) return true;
404     //if (!o.sellingToShopAllowed || price < 1) return false;
405     return true;
406   }
408   return o.forSale;
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;
417     return true;
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) {
425   if (!o) {
426     // holding nothing: this may be dice bet, or kissing parlor
427     if (style == 'Craps') {
428       doCrapsBet(plr);
429       // don't allow player to own the item
430       return false;
431     }
432     // kissing parlor?
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);
442         return false;
443       }
444       //writeln("13: STYLE: ", style, "; dms: ", GetClassName(dms.Class));
445       //!!if (isRealLevel()) global.kissesBought += 1;
446       dms.status = KISS;
447       dms.setSprite(global.isDamsel ? 'sPKissL' : 'sDamselKissL');
448       level.stats.takeMoney(kissValue);
449       cash += kissValue;
450       //!if (isRealLevel()) global.moneySpent += getKissValue();
451       global.plife += 1;
452       level.osdMessageTalk((global.isDamsel ? "NOW AIN'T HE SWEET!" : "NOW AIN'T SHE SWEET!"), replace:true, inShopOnly:false);
453       return false;
454     }
455     return false;
456   }
458   /*
459   if (forSale) {
460     writeln("trying to buy '", GetClassName(o.Class), "'");
461   } else {
462     writeln("trying to sell '", GetClassName(o.Class), "'");
463   }
464   */
466   auto die = ItemDice(o);
467   if (die) {
468     doCrapsBet(plr);
469     // don't allow player to own the item
470     return false;
471   }
473   // selling various items
474   if (!o.forSale) {
475     auto price = o.cost;
476     if (!o.sellingToShopAllowed || price < 1) {
477       // oops
478       if (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);
483       }
484       return false;
485     }
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));
489     // offer?
490     if (!o.sellOfferDone) {
491       if (price > cash) {
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);
493         return false;
494       }
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;
497       return false;
498     }
499     // deal
500     o.sellOfferDone = false;
501     if (price > cash) {
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);
503       return false;
504     }
505     if (o.heldBy) o.heldBy.holdItem = none;
506     plr.addScore(price);
507     cash -= price;
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
513     o.instanceRemove();
514     return false;
515   }
517   if (!o.forSale) FatalError("oShopkeeper::doSellItem: the thing that should not be! (0)");
519   if (o.shopType != style) {
520     // not ours
521     writeln("SELL-SKIP: me is ", style, "; item is from ", o.shopType);
522     return false;
523   }
525   if (level.stats.money < plr.holdItem.cost) {
526     level.osdMessageTalk("YOU HAVEN'T GOT ENOUGH MONEY!", replace:true);
527     return false;
528   }
529   plr.playSound('sndCoin');
530   // take away some money
531   level.stats.takeMoney(o.cost);
532   // give some money to shopkeeper
533   cash += o.cost;
534   // allow player to use the item
535   o.forSale = false;
536   level.osdMessageTalk("~PLEASURE DOING BUSINESS!~", replace:true, inShopOnly:false, timeout:4);
537   return true;
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
554   resetAllDices();
555   if (!isCrapsBetAllowed(plr)) return;
556   if (level.stats.money >= betValue) {
557     level.stats.takeMoney(betValue);
558     plr.bet = betValue;
559     cash += betValue;
560   } else {
561     level.osdMessageTalk("YOU HAVEN'T GOT ENOUGH MONEY!", replace:true);
562   }
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) {
576       if (pname) {
577         auto np = level.MakeMapObject(prize.ix, prize.iy, pname);
578         if (np) {
579           np.forSale = true;
580           np.inDiceHouse = true;
581           np.shopType = 'Craps';
582           foundIt = true;
583         }
584         pname = '';
585         if (np) break;
586       }
587     }
588     //if (!foundIt) level.osdMessageTalk("SORRY, WE DON'T HAVE PRIZES ANYMORE!");
589   }
590   // put prize near the player
591   if (level.isInShop(prize.ix/16-3, prize.iy/16)) {
592     prize.fltx -= 32;
593   } else if (level.isInShop(prize.ix/16+3, prize.iy/16)) {
594     prize.fltx += 32;
595   } else {
596     prize.fltx = plr.ix;
597     prize.flty = plr.iy;
598   }
599   createCrapsPoofAt(prize.ix, prize.iy);
600   return prize;
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 () {
613   // reset all dices
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
620     }
621     return false;
622   });
626 transient int rval, rcount, rtotal;
628 // 0: not ready yet
629 final int checkAllDices () {
630   rval = 0;
631   rcount = 0;
632   rtotal = 0;
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;
638     ++rtotal;
639     if (dc.heldBy) return false;
640     auto rv = dc.getRollNumber();
641     if (rv) {
642       rval += rv;
643       ++rcount;
644     }
645     return false;
646   });
648   if (rcount == 0 || rcount != rtotal) return 0; // not yet
649   return rval;
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;
660   auto bet = plr.bet;
661   if (!bet) {
662     die.resetRollState(); // allow to throw it again
663     return false;
664   }
666   auto rval = checkAllDices();
667   if (!rval) return false;
669   writeln("ROLL: ", rval);
670   resetAllDices();
672   //rval = 7; //dbg
673   plr.bet = 0;
675   if (rval == 7) {
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);
680     getCrapsPrize(plr);
681   } else if (rval > 7) {
682     if (cash >= bet) {
683       ++level.stats.totalDiceGamesWon;
684       level.osdMessageTalk(va("YOU ROLLED A |%d|!\nCONGRATULATIONS! YOU ~WIN~!", rval), replace:true, inShopOnly:false);
685       plr.playSound('sndCoin');
686       cash -= bet*2;
687       plr.addScore(bet*2);
688     } else {
689       ++level.stats.totalDiceGamesWonPrize;
690       broke = true;
691       level.osdMessageTalk("YOU BROKE THE BANK!\n~TAKE THE PRIZE~!", replace:true, inShopOnly:false);
692       plr.playSound('sndChestOpen');
693       getCrapsPrize(plr, generateNew:false);
694     }
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()`
700   }
702   return true;
706 void doCrapsShop () {
707   auto plr = level.player;
709   if (isPlayerInOurShop()) {
710     if (plr.holdItem) {
711       auto die = ItemDice(plr.holdItem);
712       if (die) {
713         if (!die.forSale) return;
714         if (plr.bet) {
715           level.osdMessageTalk("THROW A DICE, PLEASE.", replace:true);
716         } else {
717           if (!isCrapsBetAllowed(plr)) {
718             level.osdMessageTalk("SORRY, I AM OUT OF BUSINESS.", replace:true);
719             return;
720           }
721           if (level.stats.money < betValue) {
722             level.osdMessageTalk(va("YOU DON'T HAVE ENOUGH MONEY\nTO BET |%d| GOLD.", betValue), replace:true);
723           } else {
724             level.osdMessageTalk(va("PRESS $PAY TO BET |%d| GOLD.", betValue), replace:true);
725           }
726         }
727         return;
728       }
729     } else {
730       if (plr.bet) {
731         level.osdMessageTalk("THROW A DICE, PLEASE.", replace:true);
732       } else {
733         if (!level.osdGetTalkMessage()) {
734           if (!isCrapsBetAllowed(plr)) {
735             level.osdMessageTalk("SORRY, I AM OUT OF BUSINESS.", replace:true);
736             return;
737           }
738           if (level.stats.money < betValue) {
739             level.osdMessageTalk(va("YOU DON'T HAVE ENOUGH MONEY\nTO BET |%d| GOLD.", betValue), replace:true);
740           } else {
741             level.osdMessageTalk(va("PRESS $PAY TO BET |%d| GOLD.", betValue), replace:true);
742           }
743         }
744       }
745     }
746   }
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);
754     }
755   }
759 void dropShotgun () {
760   if (hasGun) {
761     auto obj = level.MakeMapObject(ix+8, iy+8, (hisGunIsAsh ? 'oAshShotgun' : 'oShotgun'));
762     if (obj) {
763       obj.yVel = global.randOther(4, 6);
764       obj.xVel = global.randOther(4, 6)*(xVel < 0 ? -1 : 1);
765     }
766     hasGun = false;
767     hisGunIsAsh = false;
768   }
772 void angerIt () {
773   if (angered) return;
774   if (!outlaw) {
775     level.forEachObject(delegate bool (MapObject o) {
776       if (o.forSale && o.shopType == style) o.forSale = false;
777       return false;
778     });
779   }
780   angered = true;
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;
794   }
795   return false;
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);
810   if (!item) return;
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);
819     return;
820   }
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;
829     sk.status = FOLLOW;
830   }
834 override bool onSpearTrapHit (MapObject spear) {
835   if (heldBy) heldBy.holdItem = none;
837   countsAsKill = false;
838   status = THROWN;
839   counter = stunMax;
840   yVel = -6;
841   xVel = (spear.ix+8 < ix+8 ? 4 : -4);
842   spear.playSound('sndHit');
843   hp -= global.config.spearDmg;
844   spillBlood();
846   return false;
850 override void thinkFrame () {
851   ::thinkFrame();
852   //if (hitByArrow) writeln("after ::think hp=", hp);
853   if (!isInstanceAlive) return;
855   if (status == THROWN) {
856     if (dead) {
857       status = DEAD;
858     } else {
859       status = STUNNED;
860     }
861   }
863   /*
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));
866   }
867   */
869   if (!heldBy) {
870     yVel = fmin(yVel+myGrav, 8);
871     moveRel(xVel, yVel);
872   }
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;
883   int x = ix, y = iy;
885   // crushed
886   if (!heldBy) {
887     if ((status >= STUNNED && level.isSolidAtPoint(x+8, y+12)) ||
888         (status < STUNNED && level.isSolidAtPoint(x+8, y+8)))
889     {
890       spillBlood();
891       playSound('sndCavemanDie');
892       if (hp > 0 && countsAsKill) level.addKill(objName);
893       if (!outlaw) global.murderer = true;
894       if (status != DEAD && !dead) dropShotgun();
895       instanceRemove();
896       return;
897     }
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!");
914           status = ATTACK;
915           tryToJumpOut = true;
916         }
917       }
918     }
919   }
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);
930     }
931   }
933   if (status == PATROL || status == WALK) {
934     if (!plr.dead && plr.visible && dist <= 96 && plr.iy-(y+8) < 16) {
935       status = ATTACK;
936     } else if (abs(plr.ix-(x+8)) < 4) {
937       status = ATTACK;
938     }
939   }
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;
946         hasGun = true;
947         hisGunIsAsh = (o isa ItemWeaponAshShotgun);
948         o.instanceRemove();
949         return true;
950       }
951       return false;
952     });
953     // get from player
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;
957       hasGun = true;
958       hisGunIsAsh = (o isa ItemWeaponAshShotgun);
959       o.instanceRemove();
960     }
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;
966       hasGun = true;
967       hisGunIsAsh = (o isa ItemWeaponAshShotgun);
968       o.instanceRemove();
969     }
970   }
972   if (status == IDLE) {
973     bounced = false;
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) {
986       status = PATROL;
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);
991       string msg;
992       int hi2 = -1;
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);
1006       }
1008       if (msg) level.osdMessageTalk(msg, replace:true, hiColor2:hi2);
1009     }
1010   }
1012   ///////////////
1013   // CRAPS
1014   ///////////////
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') {
1018       doCrapsShop();
1019     } else if (plr.holdItem) {
1020       auto obj = MapItem(plr.holdItem);
1021       if (obj && obj.forSale) setFollowStates();
1022     }
1023   }
1025   if (status == FOLLOW) {
1026     if (style != 'Craps' && plr.holdItem) {
1027       auto obj = MapItem(plr.holdItem);
1028       if (obj && obj.forSale) {
1029         setFollowStates();
1030       } else if (!obj || !obj.forSale) {
1031         writeln("STOP FOLLOWING");
1032         status = IDLE;
1033       }
1034     }
1036     imageSpeed = 0.5;
1038     if (colLeft || colRight) dir ^= 1;
1040     if (turnTimer > 0) {
1041       turnTimer -= 1;
1042     } else if (abs(plr.iy-(y+8)) < 8 && colBot && dist > 16) {
1043       dir = (plr.ix < x ? Dir.Left : Dir.Right);
1044       turnTimer = 10;
1045     }
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;
1052     if (plr.holdItem) {
1053       auto obj = plr.holdItem;
1054       if (!obj.forSale || obj isa ItemDice) status = IDLE;
1055     } else {
1056       status = IDLE;
1057     }
1058   } else if (status == PATROL) {
1059     bounced = false;
1061     if (yVel < 0 && colBot) yVel = 0;
1063     if (colBot && counter > 0) --counter;
1064     if (counter < 1) {
1065       dir = global.randOther(0, 1);
1066       status = WALK;
1067     }
1068   } else if (status == WALK) {
1069     imageSpeed = 0.5;
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*/)) {
1075       status = PATROL;
1076       counter = global.randOther(20, 50);
1077       xVel = 0;
1078     }
1080     if (global.randOther(1, 100) == 1) {
1081       status = PATROL;
1082       counter = global.randOther(20, 50);
1083       xVel = 0;
1084     }
1085   } else if (status == ATTACK) {
1086     imageSpeed = 1;
1088     if (!angered) angerIt();
1090     if (turnTimer > 0) {
1091       turnTimer -= 1;
1092     } else if (abs(plr.iy-(y+8)) < 8 && colBot && dist > 16) {
1093       dir = (plr.ix < x ? Dir.Left : Dir.Right);
1094       turnTimer = 20;
1095     }
1097     if (colLeft || colRight) dir ^= 1;
1099     xVel = (dir == Dir.Left ? -3 : 3);
1101     if (hasGun) {
1102       if (firing > 0) {
1103         firing -= 1;
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))
1107         {
1108           fireShotgun();
1109           yVel -= 1;
1110           xVel += (dir == Dir.Left ? 3 : -3);
1111           playSound('sndShotgun');
1112           firing = firingMax;
1113         }
1114       }
1115     }
1117     // jump
1118     if (!tryToJumpOut && plr.iy > y && abs(plr.ix-(x+8)) < 64) {
1119       // do nothing
1120     } else if ((dir == Dir.Left && level.isSolidAtPoint(x-16, y)) ||
1121                (dir == Dir.Right && level.isSolidAtPoint(x+32, y)))
1122     {
1123       if (colBot && !isCollisionTop(4)) {
1124         yVel = -global.randOther(7, 8);
1125         tryToJumpOut = false;
1126       }
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))))
1131     {
1132       if (colBot && !isCollisionTop(4)) {
1133         yVel = -global.randOther(7, 8);
1134         tryToJumpOut = false;
1135       }
1136     }
1138     if (!colBot && plr.iy > y+8) {
1139       xVel = (dir == Dir.Left ? -1.5 : 1.5);
1140     }
1142     if (plr.dead || !plr.visible) status = WALK;
1143   } else if (status == STUNNED) {
1144     if (colBot) {
1145       setSprite('sShopStunL');
1146     } else if (bounced) {
1147       setSprite(yVel < 0 ? 'sShopBounceL' : 'sShopFallL');
1148     } else {
1149       setSprite(xVel < 0 ? 'sShopDieLL' : 'sShopDieLR');
1150     }
1152     if (colBot && !bounced) {
1153       bounced = true;
1154       spillBlood(amount:1);
1155     }
1157     if (heldBy || colBot) {
1158       if (counter > 0) {
1159         --counter;
1160       } else if (hp > 0) {
1161         status = ATTACK;
1162         if (heldBy) heldBy.holdItem = none;
1163       }
1164     }
1165     if (spriteLName == 'sShopStunL') {
1166       // YASM 1.8.1
1167       if (counter > 0 && counter < 30) imageSpeed = 0.8;
1168     } else {
1169       imageSpeed = 0.5;
1170     }
1171   } else if (status == DEAD) {
1172     if (!dead) {
1173       /*if (isRoom("rStars")) {
1174         if (oStarsRoom.kills < 99) oStarsRoom.kills += 1;
1175       } else*/ {
1176         if (countsAsKill) level.addKill(objName);
1177         /*
1178         if (isRealLevel()) global.enemyKills[19] += 1;
1179         global.shopkeepers += 1;
1180         global.kills += 1;
1181         */
1182         if (level.levelKind != GameLevel::LevelKind.Stars) {
1183           if (!outlaw) {
1184             global.murderer = true;
1185             if (false /*!isRealLevel()*/) {
1186               /*
1187               repeat(rand(1, 4)) {
1188                 obj = instance_create(x+8, y+8, oGoldNugget);
1189                 obj.yVel = -1;
1190                 obj.xVel = rand(1, 3)-rand(1, 3);
1191               }
1192               */
1193             } else if (style != 'Bounty Hunter') {
1194               if (style == 'Craps') {
1195                 cash += plr.bet;
1196                 plr.bet = 0;
1197               }
1198               x = ix;
1199               y = iy;
1200               int loot = 0;
1201               while (cash > loot) {
1202                 MapObject obj;
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'));
1209                 } else {
1210                   obj = level.MakeMapObject(x+8, y+8, 'oGoldChunk');
1211                 }
1212                 if (obj) {
1213                   obj.yVel = -2;
1214                   obj.xVel = global.randOther(1, 4)-global.randOther(1, 4);
1215                 } else {
1216                   writeln("SHOPKEEPER LOOT: WTF?!");
1217                 }
1218                 loot += obj.value+trunc(ceil(obj.value/4.0)*global.levelType);
1219               }
1220             } else {
1221               x = ix;
1222               y = iy;
1223               foreach (; 0..global.randOther(1, 4)) {
1224                 auto obj = level.MakeMapObject(x+8, y+8, 'oGoldNugget');
1225                 if (obj) {
1226                   obj.yVel = -1;
1227                   obj.xVel = global.randOther(1, 3)-global.randOther(1, 3);
1228                 }
1229               }
1230             }
1231           } else {
1232             x = ix;
1233             y = iy;
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'));
1236               if (obj) {
1237                 obj.yVel = -1;
1238                 obj.xVel = global.randOther(1, 3)-global.randOther(1, 3);
1239               }
1240             }
1241           }
1242         }
1243       }
1244       playSound('sndCavemanDie');
1245       dead = true;
1246     }
1247     setSprite('sShopDieL');
1248     if (fabs(xVel) > 0 || fabs(yVel) > 0) status = STUNNED;
1249   }
1251   if (status >= STUNNED) {
1252     dropShotgun();
1253     scrCheckCollisions();
1254     if (xVel == 0 && yVel == 0 && hp < 1) status = DEAD;
1255     //k8: it should be something like the following, but...
1256     /*
1257     if (fabs(xVel) <= 0.001 && fabs(yVel) <= 0.001 && hp < 1) {
1258       xVel = 0;
1259       yVel = 0;
1260       status = DEAD;
1261     }
1262     */
1263   }
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');
1273   }
1275   if (heldBy) {
1276     setSprite(hp > 0 ? 'sShopHeldL' : 'sShopDHeldL');
1277   }
1279   if (dead && deathTimer > 0 && level.levelKind == GameLevel::LevelKind.Stars) {
1280     if (--deathTimer == 0) {
1281       spillBlood();
1282       instanceRemove();
1283       return;
1284     }
1285   }
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) {
1293     int xi, yi;
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);
1301   }
1305 defaultproperties {
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);
1312   xVel = 0;
1313   imageSpeed = 0.5;
1314   myGrav = 0.6;
1316   // stats
1317   hp = 20;
1318   invincible = 0;
1319   favor = 12;
1320   outlaw = false;
1321   broke = false;
1323   status = IDLE;
1325   bounced = false;
1326   dead = false;
1327   counter = 0;
1328   sightCounter = 0;
1329   turnTimer = 0;
1330   throwCount = 0;
1331   stunTime = 5;
1333   welcomed = false;
1334   angered = false;
1336   firing = 0;
1337   firingMax = 30;
1339   dir = Dir.Left;
1341   bounced = false;
1342   burning = false;
1343   counter = 200;
1344   stunMax = 100; //?!!!
1346   invincible = 0;
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!
1353   canPickUp = true;
1355   canBeHitByBullet = true;
1356   canBeNudged = false;
1358   hasGun = true;
1359   hisGunIsAsh = true;
1361   doBasicPhysics = false;
1362   leavesBody = true;
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";