animate "$" on held item
[k8vacspelynky.git] / mapent / MapItem.vc
blobf39679edb3d87948c549499de167d9614510e8d2
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2018, Ketmar Dark
4  *
5  * This file is part of Spelunky.
6  *
7  * You can redistribute and/or modify Spelunky, including its source code, under
8  * the terms of the Spelunky User License.
9  *
10  * Spelunky is distributed in the hope that it will be entertaining and useful,
11  * but WITHOUT WARRANTY.  Please see the Spelunky User License for more details.
12  *
13  * The Spelunky User License should be available in "Game Information", which
14  * can be found in the Resource Explorer, or as an external file called COPYING.
15  * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
16  *
17  **********************************************************************************/
18 class MapItem : MapObject;
20 bool breakPieces = true;
21 bool dropContents = true;
22 name contents; // to use in `level.MakeMapObject()`
23 int contOfsX, contOfsY;
24 int life;
26 bool breaksOnCollision = false; // jars and skulls will do, rocks will not
27 bool canHitEnemies = false;
28 bool collBroken;
30 float breakYVUp = -3, breakXV = 4, breakYV = 4;
32 int enemyColX, enemyColY;
33 int enemyColW, enemyColH;
36 int holdXOfs, holdYOfs;
38 int forSaleFrame;
41 override bool initialize () {
42   if (!::initialize()) return false;
43   forSaleFrame = global.randOther(0, 9);
44   return true;
49 final void setCollisionBoundsKill (int hx0, int hy0, int hx1, int hy1) {
50   setCollisionBounds(hx0, hy0, hx1, hy1);
51   enemyColX = hitboxX;
52   enemyColY = hitboxY;
53   enemyColW = hitboxW;
54   enemyColH = hitboxH;
59 override void onDestroy () {
60   if (dropContents && contents) {
61     //level.MakeMapObjectByClass(contents, contOfsX, contOfsY);
62     auto obj = level.MakeMapObject(ix+contOfsX, iy+contOfsY, contents);
63     spectral = true; // just in case
64     //obj.active = false;
65     if (obj && obj.isCollision()) {
66       writeln("***STUCK! (", obj.objType, ")");
67       // unstuck it
68       auto ox = obj.fltx, oy = obj.flty;
69       bool didit = false;
70       int ymove = 0;
71       if (!obj.isCollisionBottom(1)) {
72         writeln(" UNSTUCK: go bottom!");
73         ymove = 1;
74       } else if (!obj.isCollisionTop(1)) {
75         writeln(" UNSTUCK: go top!");
76         ymove = -1;
77       }
78       if (ymove != 0) {
79         int xmove = (obj.isCollisionLeft(1) ? 1 : obj.isCollisionRight(1) ? -1 : 0);
80         //if (!xmove) xmove = (obj.isCollisionLeft(1) ? 1 : obj.isCollisionRight(1) ? -1 : 0);
81         //writeln(" xmove=", xmove);
82         foreach (int dy; 0..9*3) {
83           foreach (int dx; 0..9*3) {
84             obj.fltx = ox+dx*xmove;
85             obj.flty = oy+dy*ymove;
86             if (!obj.isCollision()) {
87               //writeln("***UNSTUCK! dy=", dy);
88               didit = true;
89               break;
90             }
91           }
92           if (didit) break;
93         }
94       } else {
95         writeln(" UNSTUCK: horizontal");
96         foreach (int dy; 0..9*3) {
97           foreach (int dx; 0..9*3) {
98             obj.fltx = ox+dx;
99             obj.flty = oy+dy;
100             if (!obj.isCollision()) { didit = true; break; }
101             obj.fltx = ox-dx;
102             obj.flty = oy+dy;
103             if (!obj.isCollision()) { didit = true; break; }
104             obj.fltx = ox+dx;
105             obj.flty = oy-dy;
106             if (!obj.isCollision()) { didit = true; break; }
107             obj.fltx = ox-dx;
108             obj.flty = oy-dy;
109             if (!obj.isCollision()) { didit = true; break; }
110           }
111           if (didit) break;
112         }
113       }
114       //obj.active = false;
115       if (!didit) {
116         obj.fltx = ox;
117         obj.flty = oy;
118       }
119       obj.saveInterpData();
120       obj.updateGrid();
121     }
122   }
123   if (collBroken) {
124     playSound('sndBreak');
125     level.MakeMapObject(ix, iy, 'oSmokePuff');
126     bool colTop = !!isCollisionTop(1);
127     bool colLeft = !!isCollisionLeft(1);
128     bool colRight = !!isCollisionRight(1);
129     //bool colBot = !!isCollisionBottom(1);
130     if (breakPieces) {
131       foreach (; 0..3) {
132         auto piece = level.MakeMapObject(ix-2, iy-2, 'oRubbleSmall');
133         if (piece) {
134                if (colLeft) piece.xVel = global.randOther(1, 3);
135           else if (colRight) piece.xVel = -global.randOther(1, 3);
136           else piece.xVel = global.randOther(1, 3)-global.randOther(1, 3);
137           if (colTop) piece.yVel = global.randOther(0, 3); else piece.yVel = -global.randOther(0, 3);
138         }
139       }
140     }
141   }
142   ::onDestroy();
146 // ////////////////////////////////////////////////////////////////////////// //
147 void drawSignsWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
148   if (!forSale && !sellOfferDone) return;
149   if (!cost) return;
151   int xi, yi;
152   getInterpCoords(currFrameDelta, scale, out xi, out yi);
153   auto spr = level.sprStore[sellOfferDone ? 'sSmallCollectGreen' : 'sSmallCollect']; //sSmallCollectGreen for resale
154   if (spr && spr.frames.length) {
155     forSaleFrame %= spr.frames.length;
156     auto spf = spr.frames[forSaleFrame];
157     spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-(12+spf.yofs)*scale, scale);
158   }
162 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
163   ::drawWithOfs(xpos, ypos, scale, currFrameDelta);
164   drawSignsWithOfs(xpos, ypos, scale, currFrameDelta);
168 // ////////////////////////////////////////////////////////////////////////// //
169 override void processAlarms () {
170   ::processAlarms();
171   if (forSale || sellOfferDone) {
172     if (++forSaleFrame < 0) forSaleFrame = 0;
173     onCheckItemStolen(level.player);
174   }
178 // ////////////////////////////////////////////////////////////////////////// //
179 override bool onCanBePickedUp (PlayerPawn plr) {
180   return true;
184 // ////////////////////////////////////////////////////////////////////////// //
185 protected transient bool onExploAffected = true;
187 override bool onExplosionTouch (MapObject xplo) {
188   if (invincible) return false;
190   if (heldBy) {
191     heldBy.holdItem = none;
192     // drop item from pocket
193     auto plr = PlayerPawn(heldBy);
194     if (plr) {
195       plr.scrSwitchToPocketItem(forceIfEmpty:false);
196       auto hi = plr.holdItem;
197       plr.holdItem = none;
198       if (hi) hi.onExplosionTouch(xplo);
199     }
200   }
202   if (onExploAffected) {
203     if (breaksOnCollision) {
204       collBroken = true;
205       instanceRemove();
206       return true; // stop it, we are dead anyway
207     }
208     if (flty < xplo.flty) yVel -= 6; else yVel += 6;
209     if (xplo.fltx > fltx) xVel -= global.randOther(4, 6); else xVel += global.randOther(4, 6);
210   }
212   /+
213   if (other.type == "Arrow" or other.type == "Fish Bone" or other.type == "Jar" or other.type == "Skull") {
214     with (other) instance_destroy();
215   } else if (other.type == "Bomb") {
216     with (other) {
217       sprite_index = sBombArmed;
218       image_speed = 1;
219       alarm[1] = rand(4, 8);
220       enemyID = 0;
221     }
222     if (other.y < y) other.yVel = -rand(2, 4);
223     if (other.x < x) other.xVel = -rand(2, 4); else other.xVel = rand(2, 4);
224   } else if (other.type == "Rope") {
225     if (not other.falling) {
226       if (other.y < y) other.yVel -= 6; else other.yVel += 6;
227       if (x > other.x) other.xVel -= rand(4, 6); else other.xVel += rand(4, 6);
228     }
229   } else {
230     if (other.y < y) other.yVel -= 6; else other.yVel += 6;
231     if (x > other.x) other.xVel -= rand(4, 6); else other.xVel += rand(4, 6);
232   }
233   +/
235   return true;
239 // ////////////////////////////////////////////////////////////////////////// //
240 //override bool onTouchedByPlayer (PlayerPawn plr)
242 bool doPlayerColAction (PlayerPawn plr) {
243   //if (safe) return false;
245   if (collision_rectangle(x-8, y-8, x+8, y+8, oRock, 0, 0)) {
246     obj = instance_nearest(x, y, oRock);
247   }
249   /*
250   if (enemyColW < 1 || enemyColH < 1 || self isa ItemBomb) return false;
251   if (fabs(xVel) > 2 || fabs(yVel) > 2) {
252     if (!isInstanceAlive) return false; // stop it, we are dead anyway
253     if (heldBy) return false;
254     if (plr.dead || plr.invincible || plr.status == STUNNED || plr.stunned) return false;
255     global.plife -= 1;
256     plr.CreateBlood(plr.ix, plr.iy, 1);
257     plr.stunned = true;
258     plr.stunTimer = 120; // 200?
259     plr.yVel = -6;
260     plr.playSound('sndHit');
261     plr.xVel *= 0.3;
262     if (breaksOnCollision) {
263       collBroken = true;
264       instanceRemove();
265     }
266   }
267   */
269   if (canPickUp && global.hasMitt && !plr.holdItem && (fabs(xVel) > 4 || fabs(yVel) >= 6) &&
270       !safe && !plr.stunned && !plr.dead)
271   {
272     // catch ya!
273     plr.holdItem = self;
274     return true; // no more actions
275   }
278   return false; // go on
282 // ////////////////////////////////////////////////////////////////////////// //
283 // virtual
284 // return `true` to skip normal item processing
285 // if skipped, engine will call `onAfterSomethingHit()`
286 bool onEnemyHit (MapEnemy e) {
287   /+
288   if (enemy.status != STUNNED) enemy.xVel = xVel*0.3; //k8: *0.3 is mine
289   switch (enemy.objName) {
290     case 'Caveman':
291     case 'ManTrap':
292     case 'Yeti':
293     case 'Hawkman':
294     case 'Shopkeeper':
295       if (enemy.status != STUNNED) {
296         switch (enemy.objName) {
297           case 'Caveman':
298           case 'Yeti':
299           case 'Hawkman':
300           case 'Shopkeeper':
301             level.MakeMapObject(enemy.ix, enemy.iy, 'oBlood');
302             break;
303         }
304         enemy.status = STUNNED;
305         enemy.counter = stunTime;
306         enemy.yVel = -6;
307         enemy.hp -= damage;
308       }
309       break;
310     /*
311     case 'oDamsel':
312       level.MakeMapObject(enemy.ix, enemy.iy, 'oBlood');
313       // drop damsel
314       if (enemy.heldBy) enemy.heldBy.holdItem = none;
315       enemy.hp -= damage;
316       enemy.yVel = -6;
317       enemy.status = 2; //???
318       enemy.counter = 120;
319       enemy.damselDropped = true;
320       enemy.xVel = xVel*0.3;
321       break;
322     */
323     default:
324       level.MakeMapObject(enemy.ix+8, enemy.iy+8, 'oBlood');
325       enemy.hp -= damage;
326       //!enemy.origX = enemy.x;
327       //!enemy.origY = enemy.y;
328       enemy.shakeCounter = 10;
329       break;
330   }
331   +/
333   /+
334   playSound('sndHit');
335   if (enemy.status != STUNNED) enemy.xVel = xVel*0.3;
336   //if (objType == 'Arrow' || objType == 'Fish Bone') instance_destroy();
337   +/
339   return false;
343 // return `false` to do standard weapon processing
344 override bool onTouchedByPlayerWeapon (PlayerPawn plr, PlayerWeapon wpn) {
345   if (!wpn.prestrike && breaksOnCollision) {
346     wpn.hitEnemy = true;
347     dropContents = true;
348     collBroken = true;
349     instanceRemove();
350     return true;
351   }
352   return false;
356 override void onBulletHit (ObjBullet bullet) {
357   if (breaksOnCollision) {
358     dropContents = true;
359     collBroken = true;
360     instanceRemove();
361   }
365 // virtual
366 // return `true` to stop further processing
367 bool onAfterSomethingHit () {
368   if (breaksOnCollision) {
369     collBroken = true;
370     instanceRemove();
371     return true; // stop it, we are dead anyway
372   }
373   collBroken = false;
374   //return true;
375   return false;
379 transient bool wasObjectCollision;
381 bool doObjectColAction (MapObject o) {
382   if (!isInstanceAlive) return true; // stop it, we are dead anyway
384   collBroken = true;
386   MapEnemy enemy = MapEnemy(o);
387   if (enemy) {
388     // our handler
389     if (enemy.isInstanceAlive && onEnemyHit(enemy)) {
390       wasObjectCollision = true;
391       if (onAfterSomethingHit()) return true; // anyway
392       return !isInstanceAlive;
393     }
394     // their handler
395     if (enemy.onHitByItem(self)) {
396       wasObjectCollision = true;
397       if (onAfterSomethingHit()) return true; // anyway
398       return !isInstanceAlive;
399     }
400     return !isInstanceAlive;
401   }
403   collBroken = false;
405   return !isInstanceAlive;
409 override void fixHoldCoords () {
410   if (heldBy) {
411     xVel = 0;
412     yVel = 0;
413     xAcc = 0;
414     yAcc = 0;
415     imageAngle = 0;
416     int dx = (heldBy.dir == Dir.Left ? -4 : 4);
417     int dy = ((heldBy.status == DUCKING || heldBy.status == STUNNED || heldBy.stunned) && fabs(heldBy.xVel) < 2 ? 4 : 0);
418     dx += holdXOfs;
419     dy += holdYOfs;
420     setXY(heldBy.fltx+dx, heldBy.flty+dy);
421     prevFltX = heldBy.prevFltX+dx;
422     prevFltY = heldBy.prevFltY+dy;
423     updateGrid();
424     if (spriteRName) dir = heldBy.dir;
425   }
429 // Nudge with melee weapon
430 override void nudgeIt (int nx, int ny, optional bool forced) {
431   if (heldBy) return;
432   if (level.isSolidAtPoint(ix, iy)) return;
433   if (!forced && !global.config.nudge) return;
434   if (nudged) return;
436   if (forSale || /*forVending ||*/ trigger) {
437     if (!trigger) yVel = -1;
438   } else {
439     if (heavy) {
440       yVel -= 1;
441            if (nx < ix) xVel += global.randOther(5, 8)*0.1;
442       else if (nx > ix) xVel -= global.randOther(5, 8)*0.1;
443     } else if (self isa ItemProjectileArrow && fabs(xVel) > 0) {
444              if (fabs(xVel) < 4) xVel = -xVel;
445         else if (xVel < 0) xVel = global.randOther(3, 5);
446         else if (xVel > 0) xVel = -global.randOther(3, 5);
447         yVel = -yVel;
448     } else if (!stuck && !sticky) {
449       yVel -= (global.randOther(0, 1) ? 2.0 : 1.5);
450            if (nx < ix) xVel += global.randOther(10, 15)*0.1;
451       else if (nx > ix) xVel -= global.randOther(10, 15)*0.1;
452       /*
453       if (type == "Basketball") {
454         if (abs(yVel) < 4) yVel -= 5;
455         xVel = xVel*4;
456       }
457       */
458     }
459   }
461   nudged = true;
462   alarmNudge = 10;
466 void onCheckItemStolen (PlayerPawn plr) {
467   // check if it is stolen
468   if (forSale && cost > 0 && !level.isInShop(ix/16, iy/16)) {
469     level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
470   }
474 override bool onFellInWater (MapTile water) {
475   level.MakeMapObject(xCenter, iy, 'oSplash');
476   playSound('sndSplash');
477   //!myGrav = myGravWater;
478   return false;
482 override bool onOutOfWater () {
483   //!myGrav = myGravNorm;
484   return false;
488 int origLightRadius = -666;
490 override void thinkFrame () {
491   if (origLightRadius == -666) {
492     origLightRadius = lightRadius;
493     lightRadius = max(origLightRadius, (forSale && level.isInShop(ix/16, iy/16) ? 64 : 0));
494   }
496   bool doBreak = false;
498   //basicPhysicsStep();
499   //if (!isInstanceAlive) return;
500   //if (!heldBy && self isa ItemBomb) writeln("yVel=", yVel, "; myGrav=", myGrav);
502   // water
503   if (allowWaterProcessing) {
504     //auto wtile = level.isWaterAtPoint(ix+spf.width/2, iy+spf.height/2/*, -1, -1*/);
505     auto wtile = level.isWaterAtPoint(ix+8, iy+8);
506     if (wtile) {
507       if (!swimming) {
508         swimming = true;
509         if (onFellInWater(wtile) || !isInstanceAlive) return;
510       }
511     } else {
512       if (swimming) {
513         swimming = false;
514         if (onOutOfWater() || !isInstanceAlive) return;
515       }
516     }
517   }
519   if (heldBy) return;
522   if (!isCollisionAtPoint(ix, iy)) {
523     //if (self isa ItemProjectileArrow) writeln("xVel=", xVel, "; yVel=", yVel);
524     moveRel(xVel, yVel);
526     bool colTop = !!isCollisionTop(1);
527     bool colLeft = !!isCollisionLeft(1);
528     bool colRight = !!isCollisionRight(1);
529     bool colBot = !!isCollisionBottom(1);
531     if (!colLeft && !colRight) stuck = false;
533     if (!flying && !colBot && !stuck) yVel += myGrav;
534     //if (yVel > 8) yVel = 8;
535     //yVel = fmin(yVelLimit, yVel);
536     yVel = fmin(8, yVel);
538     // not in the original
539     /+
540     if (colTop && yVel < 0) {
541       if (breaksOnCollision && yVel < breakYVUp) doBreak = true;
542            if (bounceTop) yVel = -yVel*0.8;
543       else if (fabs(xVel) < 0.0001) yVel = 0;
544     }
545     +/
547     if (colLeft || colRight) {
548       if (breaksOnCollision && fabs(xVel) > breakXV) doBreak = true;
549       xVel = (bounce ? -xVel*0.5 : 0.0);
550       myGrav = 0.6;
551     }
553     if (colBot) {
554       if (breaksOnCollision && yVel > breakYV) doBreak = true;
555       myGrav = 0.6;
556       // bounce
557       yVel = (yVel > 1 && bounce ? -yVel*bounceFactor : 0.0);
558       // friction
559            if (fabs(xVel) < 0.1) xVel = 0;
560       else if (fabs(xVel) != 0) xVel *= frictionFactor;
561       if (fabs(yVel) < 1) {
562         flty = iy-1;
563         if (!isCollisionBottom(1)) flty = iy+1;
564         yVel = 0;
565       }
566     }
568     if (sticky && self isa ItemBomb && self.armed) {
569       if (colLeft || colRight || colTop || colBot) {
570         xVel = 0;
571         yVel = 0;
572         if (colBot && fabs(yVel) < 1) flty = iy+1;
573       }
574     } else if (self isa ItemProjectileArrow && fabs(xVel) > 6) {
575       if (colLeft) {
576         fltx = ix-2;
577         xVel = 0;
578         yVel = 0;
579       } else if (colRight) {
580         fltx = ix+2;
581         xVel = 0;
582         yVel = 0;
583       }
584       stuck = true;
585     } else if (colLeft && !stuck) {
586       if (!colRight) fltx = ix+1;
587       //yVel = 0; // in the original
588     } else if (colRight && !stuck) {
589       fltx = ix-1;
590       //yVel = 0; // in the original
591     }
593     if (sticky && self isa ItemBomb && self.armed) {
594       // do nothing
595     } else if (isCollisionTop(1)) {
596       if (breaksOnCollision && yVel < breakYVUp) doBreak = true;
597       if (yVel < 0) yVel = -yVel*0.8; else flty = iy+1;
598       myGrav = 0.6;
599     }
601     if (isCollisionInRect(ix-3, iy-3, 7, 7, &level.cbCollisionLava)) {
602       myGrav = 0;
603       xVel = 0;
604       yVel = 0;
605       flty += 0.05;
606     } else {
607       myGrav = 0.6;
608     }
610     if (self !isa ItemWeaponSceptre && isCollisionAtPoint(ix, iy-5, &level.cbCollisionLava)) {
611       auto bomb = ItemBomb(self);
612       if (bomb) bomb.explode();
613       instanceRemove();
614       return;
615     }
616   } else {
617     //if (self isa ItemProjectileArrow) writeln("CLD: xVel=", xVel, "; yVel=", yVel);
618     bool colTop = !!isCollisionTop(1);
619     bool colLeft = !!isCollisionLeft(1);
620     bool colRight = !!isCollisionRight(1);
621     bool colBot = !!isCollisionBottom(1);
622     // collided
623     if (breaksOnCollision) {
624       collBroken = true;
625       instanceRemove();
626     } else {
627            if (colTop && !colBot) flty = iy+1;
628       else if (colLeft && !colRight) fltx = ix+1;
629       else if (colRight && !colLeft) fltx = ix-1;
630       else { xVel = 0; yVel = 0; }
631     }
632   }
634   if (canHitEnemies && width > 0 && height > 0 && isInstanceAlive) {
635     if (fabs(xVel) > 2 || fabs(yVel) > 2) {
636       wasObjectCollision = false;
637       auto plr = level.player;
638       if (isInstanceAlive && !spectral) {
639         spectral = true;
640         level.forEachObjectInRect(x0, y0, width, height, &doObjectColAction);
641         spectral = false;
642       }
643       if (isInstanceAlive && plr.isRectCollision(x0, y0, width, height)) {
644         doPlayerColAction(plr);
645         wasObjectCollision = true;
646       }
647       // this is done in `onHitByItem`
648       //if (wasObjectCollision) obj.xVel = xVel*0.3;
649     }
650   }
652   if (doBreak && breaksOnCollision) {
653     collBroken = true;
654     instanceRemove();
655   }
659 defaultproperties {
660   depth = 101; // behind enemies (60)
662   objType = 'oItem';
663   setCollisionBounds(0, 0, 16, 16);
664   yVelLimit = 8;
665   bounce = true;
666   bounceTop = true;
668   canBeHitByBullet = false;
669   canBeNudged = true;
671   holdYOfs = 2;
675 // ////////////////////////////////////////////////////////////////////////// //
676 class ItemDice['oDice'] : MapItem;
678 //int value;
679 enum RollState {
680   None, // ready to roll
681   Rolling, // rolled
682   Finished, // landed in shop
683   Failed, // landed outside of the shop
685 RollState rollState;
687 bool pickedOutsideOfAShop;
690 override bool initialize () {
691   if (!::initialize()) return false;
692   setSprite('sDice1');
693   value = global.randOther(1, 6);
694   return true;
698 // 0: failed roll
699 final int getRollNumber () {
700   if (rollState != RollState.Finished) return 0;
701   return value;
705 final void resetRollState () {
706   rollState = RollState.None;
710 bool isReadyToThrowForBet () {
711   if (!forSale) return false;
712   return ((rollState == RollState.Failed || rollState == RollState.None) && level.player.bet);
716 // various side effects
717 // called only if object was succesfully put into player hands
718 override void onPickedUp (PlayerPawn plr) {
719   pickedOutsideOfAShop = !level.isInShop(ix/16, iy/16);
720   if (rollState != RollState.Finished) rollState = RollState.None;
724 override void drawSignsWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
725   if (!forSale) return;
726   if ((rollState == RollState.Failed || rollState == RollState.None) && level.player.bet) {
727     int xi, yi;
728     getInterpCoords(currFrameDelta, scale, out xi, out yi);
729     auto spr = level.sprStore['sRedArrowDown'];
730     if (spr && spr.frames.length) {
731       auto spf = spr.frames[0];
732       spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-(12+spf.yofs)*scale, scale);
733     }
734   }
738 override void onCheckItemStolen (PlayerPawn plr) {
739   if (!heldBy || pickedOutsideOfAShop) return;
741   if (forSale && !level.player.bet && rollState != RollState.Failed) {
742     bool inShop = level.isInShop(ix/16, iy/16);
743     if (!inShop) {
744       level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen); // don't steal it!
745     }
746   }
750 override void thinkFrame () {
751   if (forSale && /*!forVending && cost > 0 &&*/ !level.hasAliveShopkeepers(skipAngry:true)) {
752     forSale = false;
753     cost = 0;
754   }
756   lightRadius = max(origLightRadius, (forSale && level.isInShop(ix/16, iy/16) ? 64 : 0));
758   if (heldBy) {
759     /*
760          if (oCharacter.facing == LEFT) x = oCharacter.x - 4;
761     else if (oCharacter.facing == RIGHT) x = oCharacter.x + 4;
763     if (heavy) {
764       if (oCharacter.state == DUCKING and abs(oCharacter.xVel) < 2) y = oCharacter.y; else y = oCharacter.y-2;
765     } else {
766       if (oCharacter.state == DUCKING and abs(oCharacter.xVel) < 2) y = oCharacter.y+4; else y = oCharacter.y+2;
767     }
768     depth = 1;
770     if (oCharacter.holdItem == 0) held = false;
771     */
772     // stealing makes shopkeeper angry
773     //writeln("!!! fs=", forSale, "; bet=", level.player.bet, "; st=", rollState);
774   } else {
775     moveRel(xVel, yVel);
777     bool colLeft = !!isCollisionLeft(1);
778     bool colRight = !!isCollisionRight(1);
779     bool colBot = !!isCollisionBottom(1);
780     bool colTop = !!isCollisionTop(1);
782     if (!colBot && yVel < 6) yVel += myGrav;
784          if (fabs(xVel) < 0.1) xVel = 0;
785     else if (colLeft || colRight) xVel = -xVel*0.5;
787     if (colBot) {
788       // bounce
789       if (yVel > 1) yVel = -yVel*bounceFactor; else yVel = 0;
790       // friction
791            if (fabs(xVel) < 0.1) xVel = 0;
792       else if (fabs(xVel) != 0) xVel *= frictionFactor;
793       if (fabs(yVel) < 1) {
794         flty -= 1;
795         if (!isCollisionBottom(1)) flty += 1;
796         yVel = 0;
797       }
798     }
800     if (colLeft) {
801       if (!colRight) fltx += 1;
802       //yVel = 0;
803     } else if (colRight) {
804       fltx -= 1;
805       //yVel = 0;
806     }
808     if (isCollisionTop(1)) {
809       if (yVel < 0) yVel = -yVel*0.8; else flty += 1;
810     }
812     //!depth = (global.hasSpectacles ? 0 : 101); //???
814     if (isCollisionInRect(ix-3, iy-3, 7, 7, &level.cbCollisionLava)) {
815       myGrav = 0;
816       xVel = 0;
817       yVel = 0;
818       shiftY(0.05);
819     } else {
820       myGrav = 0.6;
821     }
823     if (isCollisionAtPoint(ix, iy-5, &level.cbCollisionLava)) {
824       instanceRemove();
825       return;
826     }
827   }
829   if (!isInstanceAlive || spectral) return;
831   if (fabs(xVel) > 3 || fabs(yVel) > 3) {
832     /*
833     auto plr = level.player;
834     if (plr.isRectCollision(ix+enemyColX, iy+enemyColY, enemyColW, enemyColH)) {
835       doPlayerColAction(plr);
836     }
837     */
838     spectral = true;
839     level.forEachObjectInRect(ix-2, iy-2, 5, 5, &doObjectColAction);
840     spectral = false;
841   }
843   // roll states
844   if (fabs(yVel) > 2 || fabs(xVel) > 2) {
845     setSprite('sDiceRoll');
846     value = global.randOther(1, 6);
847     switch (rollState) {
848       case RollState.Finished:
849         // NO CHEATING!
850         if (level.player.bet > 0) level.scrShopkeeperAnger(GameLevel::SCAnger.CrapsCheated);
851         break;
852       default:
853         rollState = RollState.Rolling;
854         break;
855     }
856   } else if (yVel == 0 && fabs(xVel <= 2) && isCollisionBottom(1)) {
857     // landed
858     switch (rollState) {
859       case RollState.Rolling:
860         rollState = (level.isInShop(ix/16, iy/16) ? RollState.Finished : RollState.Failed);
861         if (rollState == RollState.Finished) level.player.onDieRolled(self);
862         break;
863       case RollState.Finished:
864       case RollState.Failed:
865         break;
866       case RollState.None:
867       default: rollState = RollState.None; break;
868     }
869   }
871   switch (value) {
872     case 1: setSprite('sDice1'); break;
873     case 2: setSprite('sDice2'); break;
874     case 3: setSprite('sDice3'); break;
875     case 4: setSprite('sDice4'); break;
876     case 5: setSprite('sDice5'); break;
877     default: setSprite('sDice6'); break;
878   }
880   canPickUp = (rollState != RollState.Rolling);
884 defaultproperties {
885   objType = 'Dice';
886   desc = "Die";
887   desc2 = "A six-sided die. The storeowner talks to it every night before he goes to sleep.";
888   setCollisionBounds(-6, 0, 6, 8);
889   heavy = true;
890   //rolled = false;
891   //rolling = false;
892   canBeNudged = true;
893   canPickUp = true;
894   holdYOfs = -4;
895   rollState = RollState.None;
896   bloodless = true; // just in case, lol
897   canHitEnemies = true;