F1 now shows game map
[k8vacspelynky.git] / mapent / MapItem.vc
blob97eb6a7c9f441906c527a8217c2b938a4188be93
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) return;
149   if (!cost) return;
151   int xi, yi;
152   getInterpCoords(currFrameDelta, scale, out xi, out yi);
153   auto spr = level.sprStore['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) {
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.scrCreateBlood(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;
378 // virtual
379 // return `true` to skip normal item processing
380 // if skipped, engine will call `onAfterSomethingHit()`
381 bool onCharacterHit (ObjCharacter chr) {
382   return false;
386 transient bool wasObjectCollision;
388 bool doObjectColAction (MapObject o) {
389   if (!isInstanceAlive) return true; // stop it, we are dead anyway
391   collBroken = true;
393   MapEnemy enemy = MapEnemy(o);
394   if (enemy) {
395     // our handler
396     if (enemy.isInstanceAlive && onEnemyHit(enemy)) {
397       wasObjectCollision = true;
398       if (onAfterSomethingHit()) return true; // anyway
399       return !isInstanceAlive;
400     }
401     // their handler
402     if (enemy.onItemHit(self)) {
403       wasObjectCollision = true;
404       if (onAfterSomethingHit()) return true; // anyway
405       return !isInstanceAlive;
406     }
407     return !isInstanceAlive;
408   }
410   // character?
411   ObjCharacter chr = ObjCharacter(o);
412   if (chr) {
413     // our handler
414     if (chr.isInstanceAlive && onCharacterHit(chr)) {
415       wasObjectCollision = true;
416       if (onAfterSomethingHit()) return true; // anyway
417       return !isInstanceAlive;
418     }
419     // their handler
420     if (chr.onItemHit(self)) {
421       wasObjectCollision = true;
422       if (onAfterSomethingHit()) return true; // anyway
423       return !isInstanceAlive;
424     }
425   }
427   collBroken = false;
429   return !isInstanceAlive;
433 override void fixHoldCoords () {
434   if (heldBy) {
435     xVel = 0;
436     yVel = 0;
437     xAcc = 0;
438     yAcc = 0;
439     imageAngle = 0;
440     int dx = (heldBy.dir == Dir.Left ? -4 : 4);
441     int dy = ((heldBy.status == DUCKING || heldBy.status == STUNNED || heldBy.stunned) && fabs(heldBy.xVel) < 2 ? 4 : 0);
442     dx += holdXOfs;
443     dy += holdYOfs;
444     setXY(heldBy.fltx+dx, heldBy.flty+dy);
445     prevFltX = heldBy.prevFltX+dx;
446     prevFltY = heldBy.prevFltY+dy;
447     updateGrid();
448     if (spriteRName) dir = heldBy.dir;
449   }
453 // Nudge with melee weapon
454 override void nudgeIt (int nx, int ny, optional bool forced) {
455   if (heldBy) return;
456   if (level.isSolidAtPoint(ix, iy)) return;
457   if (!forced && !global.config.nudge) return;
458   if (nudged) return;
460   if (forSale || /*forVending ||*/ trigger) {
461     if (!trigger) yVel = -1;
462   } else {
463     if (heavy) {
464       yVel -= 1;
465            if (nx < ix) xVel += global.randOther(5, 8)*0.1;
466       else if (nx > ix) xVel -= global.randOther(5, 8)*0.1;
467     } else if (self isa ItemProjectileArrow && fabs(xVel) > 0) {
468              if (fabs(xVel) < 4) xVel = -xVel;
469         else if (xVel < 0) xVel = global.randOther(3, 5);
470         else if (xVel > 0) xVel = -global.randOther(3, 5);
471         yVel = -yVel;
472     } else if (!stuck && !sticky) {
473       yVel -= (global.randOther(0, 1) ? 2.0 : 1.5);
474            if (nx < ix) xVel += global.randOther(10, 15)*0.1;
475       else if (nx > ix) xVel -= global.randOther(10, 15)*0.1;
476       /*
477       if (type == "Basketball") {
478         if (abs(yVel) < 4) yVel -= 5;
479         xVel = xVel*4;
480       }
481       */
482     }
483   }
485   nudged = true;
486   alarmNudge = 10;
490 void onCheckItemStolen (PlayerPawn plr) {
491   // check if it is stolen
492   if (forSale && cost > 0 && !level.isInShop(ix/16, iy/16)) {
493     level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
494   }
498 override bool onFellInWater (MapTile water) {
499   level.MakeMapObject(xCenter, iy, 'oSplash');
500   playSound('sndSplash');
501   //!myGrav = myGravWater;
502   return false;
506 override bool onOutOfWater () {
507   //!myGrav = myGravNorm;
508   return false;
512 int origLightRadius = -666;
514 override void thinkFrame () {
515   if (origLightRadius == -666) {
516     origLightRadius = lightRadius;
517     lightRadius = max(origLightRadius, (forSale && level.isInShop(ix/16, iy/16) ? 64 : 0));
518   }
520   bool doBreak = false;
522   //basicPhysicsStep();
523   //if (!isInstanceAlive) return;
524   //if (!heldBy && self isa ItemBomb) writeln("yVel=", yVel, "; myGrav=", myGrav);
526   // water
527   if (allowWaterProcessing) {
528     //auto wtile = level.isWaterAtPoint(ix+spf.width/2, iy+spf.height/2/*, -1, -1*/);
529     auto wtile = level.isWaterAtPoint(ix+8, iy+8);
530     if (wtile) {
531       if (!swimming) {
532         swimming = true;
533         if (onFellInWater(wtile) || !isInstanceAlive) return;
534       }
535     } else {
536       if (swimming) {
537         swimming = false;
538         if (onOutOfWater() || !isInstanceAlive) return;
539       }
540     }
541   }
543   if (heldBy) return;
546   if (!isCollisionAtPoint(ix, iy)) {
547     //if (self isa ItemProjectileArrow) writeln("xVel=", xVel, "; yVel=", yVel);
548     moveRel(xVel, yVel);
550     bool colTop = !!isCollisionTop(1);
551     bool colLeft = !!isCollisionLeft(1);
552     bool colRight = !!isCollisionRight(1);
553     bool colBot = !!isCollisionBottom(1);
555     if (!colLeft && !colRight) stuck = false;
557     if (!flying && !colBot && !stuck) yVel += myGrav;
558     //if (yVel > 8) yVel = 8;
559     //yVel = fmin(yVelLimit, yVel);
560     yVel = fmin(8, yVel);
562     // not in the original
563     /+
564     if (colTop && yVel < 0) {
565       if (breaksOnCollision && yVel < breakYVUp) doBreak = true;
566            if (bounceTop) yVel = -yVel*0.8;
567       else if (fabs(xVel) < 0.0001) yVel = 0;
568     }
569     +/
571     if (colLeft || colRight) {
572       if (breaksOnCollision && fabs(xVel) > breakXV) doBreak = true;
573       xVel = (bounce ? -xVel*0.5 : 0.0);
574       myGrav = 0.6;
575     }
577     if (colBot) {
578       if (breaksOnCollision && yVel > breakYV) doBreak = true;
579       myGrav = 0.6;
580       // bounce
581       yVel = (yVel > 1 && bounce ? -yVel*bounceFactor : 0.0);
582       // friction
583            if (fabs(xVel) < 0.1) xVel = 0;
584       else if (fabs(xVel) != 0) xVel *= frictionFactor;
585       if (fabs(yVel) < 1) {
586         flty = iy-1;
587         if (!isCollisionBottom(1)) flty = iy+1;
588         yVel = 0;
589       }
590     }
592     if (sticky && self isa ItemBomb && self.armed) {
593       if (colLeft || colRight || colTop || colBot) {
594         xVel = 0;
595         yVel = 0;
596         if (colBot && fabs(yVel) < 1) flty = iy+1;
597       }
598     } else if (self isa ItemProjectileArrow && fabs(xVel) > 6) {
599       if (colLeft) {
600         fltx = ix-2;
601         xVel = 0;
602         yVel = 0;
603       } else if (colRight) {
604         fltx = ix+2;
605         xVel = 0;
606         yVel = 0;
607       }
608       stuck = true;
609     } else if (colLeft && !stuck) {
610       if (!colRight) fltx = ix+1;
611       //yVel = 0; // in the original
612     } else if (colRight && !stuck) {
613       fltx = ix-1;
614       //yVel = 0; // in the original
615     }
617     if (sticky && self isa ItemBomb && self.armed) {
618       // do nothing
619     } else if (isCollisionTop(1)) {
620       if (breaksOnCollision && yVel < breakYVUp) doBreak = true;
621       if (yVel < 0) yVel = -yVel*0.8; else flty = iy+1;
622       myGrav = 0.6;
623     }
625     if (isCollisionInRect(ix-3, iy-3, 7, 7, &level.cbCollisionLava)) {
626       myGrav = 0;
627       xVel = 0;
628       yVel = 0;
629       flty += 0.05;
630     } else {
631       myGrav = 0.6;
632     }
634     if (self !isa ItemWeaponSceptre && isCollisionAtPoint(ix, iy-5, &level.cbCollisionLava)) {
635       auto bomb = ItemBomb(self);
636       if (bomb) bomb.explode();
637       instanceRemove();
638       return;
639     }
640   } else {
641     //if (self isa ItemProjectileArrow) writeln("CLD: xVel=", xVel, "; yVel=", yVel);
642     bool colTop = !!isCollisionTop(1);
643     bool colLeft = !!isCollisionLeft(1);
644     bool colRight = !!isCollisionRight(1);
645     bool colBot = !!isCollisionBottom(1);
646     // collided
647     if (breaksOnCollision) {
648       collBroken = true;
649       instanceRemove();
650     } else {
651            if (colTop && !colBot) flty = iy+1;
652       else if (colLeft && !colRight) fltx = ix+1;
653       else if (colRight && !colLeft) fltx = ix-1;
654       else { xVel = 0; yVel = 0; }
655     }
656   }
658   if (canHitEnemies && width > 0 && height > 0 && isInstanceAlive) {
659     if (fabs(xVel) > 2 || fabs(yVel) > 2) {
660       wasObjectCollision = false;
661       auto plr = level.player;
662       if (isInstanceAlive && !spectral) {
663         spectral = true;
664         level.forEachObjectInRect(x0, y0, width, height, &doObjectColAction);
665         spectral = false;
666       }
667       if (isInstanceAlive && plr.isRectCollision(x0, y0, width, height)) {
668         doPlayerColAction(plr);
669         wasObjectCollision = true;
670       }
671       // this is done in `onItemHit`
672       //if (wasObjectCollision) obj.xVel = xVel*0.3;
673     }
674   }
676   if (doBreak && breaksOnCollision) {
677     collBroken = true;
678     instanceRemove();
679   }
683 defaultproperties {
684   depth = 101; // behind enemies (60)
686   objType = 'oItem';
687   setCollisionBounds(0, 0, 16, 16);
688   yVelLimit = 8;
689   bounce = true;
690   bounceTop = true;
692   canBeHitByBullet = false;
693   canBeNudged = true;
695   holdYOfs = 2;
699 // ////////////////////////////////////////////////////////////////////////// //
700 class ItemDice['oDice'] : MapItem;
702 //int value;
703 enum RollState {
704   None, // ready to roll
705   Rolling, // rolled
706   Finished, // landed in shop
707   Failed, // landed outside of the shop
709 RollState rollState;
711 bool pickedOutsideOfAShop;
714 override bool initialize () {
715   if (!::initialize()) return false;
716   setSprite('sDice1');
717   value = global.randOther(1, 6);
718   return true;
722 // 0: failed roll
723 final int getRollNumber () {
724   if (rollState != RollState.Finished) return 0;
725   return value;
729 final void resetRollState () {
730   rollState = RollState.None;
734 // various side effects
735 // called only if object was succesfully put into player hands
736 override void onPickedUp (PlayerPawn plr) {
737   pickedOutsideOfAShop = !level.isInShop(ix/16, iy/16);
738   if (rollState != RollState.Finished) rollState = RollState.None;
742 override void drawSignsWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
743   if (!forSale) return;
744   if ((rollState == RollState.Failed || rollState == RollState.None) && level.player.bet) {
745     int xi, yi;
746     getInterpCoords(currFrameDelta, scale, out xi, out yi);
747     auto spr = level.sprStore['sRedArrowDown'];
748     if (spr && spr.frames.length) {
749       auto spf = spr.frames[0];
750       spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-(12+spf.yofs)*scale, scale);
751     }
752   }
756 override void onCheckItemStolen (PlayerPawn plr) {
757   if (!heldBy || pickedOutsideOfAShop) return;
759   if (forSale && !level.player.bet && rollState != RollState.Failed) {
760     bool inShop = level.isInShop(ix/16, iy/16);
761     if (!inShop) {
762       level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen); // don't steal it!
763     }
764   }
768 override void thinkFrame () {
769   if (forSale && /*!forVending && cost > 0 &&*/ !level.hasAliveShopkeepers(skipAngry:true)) {
770     forSale = false;
771     cost = 0;
772   }
774   lightRadius = max(origLightRadius, (forSale && level.isInShop(ix/16, iy/16) ? 64 : 0));
776   if (heldBy) {
777     /*
778          if (oCharacter.facing == LEFT) x = oCharacter.x - 4;
779     else if (oCharacter.facing == RIGHT) x = oCharacter.x + 4;
781     if (heavy) {
782       if (oCharacter.state == DUCKING and abs(oCharacter.xVel) < 2) y = oCharacter.y; else y = oCharacter.y-2;
783     } else {
784       if (oCharacter.state == DUCKING and abs(oCharacter.xVel) < 2) y = oCharacter.y+4; else y = oCharacter.y+2;
785     }
786     depth = 1;
788     if (oCharacter.holdItem == 0) held = false;
789     */
790     // stealing makes shopkeeper angry
791     //writeln("!!! fs=", forSale, "; bet=", level.player.bet, "; st=", rollState);
792   } else {
793     moveRel(xVel, yVel);
795     bool colLeft = !!isCollisionLeft(1);
796     bool colRight = !!isCollisionRight(1);
797     bool colBot = !!isCollisionBottom(1);
798     bool colTop = !!isCollisionTop(1);
800     if (!colBot && yVel < 6) yVel += myGrav;
802          if (fabs(xVel) < 0.1) xVel = 0;
803     else if (colLeft || colRight) xVel = -xVel*0.5;
805     if (colBot) {
806       // bounce
807       if (yVel > 1) yVel = -yVel*bounceFactor; else yVel = 0;
808       // friction
809            if (fabs(xVel) < 0.1) xVel = 0;
810       else if (fabs(xVel) != 0) xVel *= frictionFactor;
811       if (fabs(yVel) < 1) {
812         flty -= 1;
813         if (!isCollisionBottom(1)) flty += 1;
814         yVel = 0;
815       }
816     }
818     if (colLeft) {
819       if (!colRight) fltx += 1;
820       //yVel = 0;
821     } else if (colRight) {
822       fltx -= 1;
823       //yVel = 0;
824     }
826     if (isCollisionTop(1)) {
827       if (yVel < 0) yVel = -yVel*0.8; else flty += 1;
828     }
830     //!depth = (global.hasSpectacles ? 0 : 101); //???
832     if (isCollisionInRect(ix-3, iy-3, 7, 7, &level.cbCollisionLava)) {
833       myGrav = 0;
834       xVel = 0;
835       yVel = 0;
836       shiftY(0.05);
837     } else {
838       myGrav = 0.6;
839     }
841     if (isCollisionAtPoint(ix, iy-5, &level.cbCollisionLava)) {
842       instanceRemove();
843       return;
844     }
845   }
847   if (!isInstanceAlive || spectral) return;
849   if (fabs(xVel) > 3 || fabs(yVel) > 3) {
850     /*
851     auto plr = level.player;
852     if (plr.isRectCollision(ix+enemyColX, iy+enemyColY, enemyColW, enemyColH)) {
853       doPlayerColAction(plr);
854     }
855     */
856     spectral = true;
857     level.forEachObjectInRect(ix-2, iy-2, 5, 5, &doObjectColAction);
858     spectral = false;
859   }
861   // roll states
862   if (fabs(yVel) > 2 || fabs(xVel) > 2) {
863     setSprite('sDiceRoll');
864     value = global.randOther(1, 6);
865     switch (rollState) {
866       case RollState.Finished:
867         // NO CHEATING!
868         if (level.player.bet > 0) level.scrShopkeeperAnger(GameLevel::SCAnger.CrapsCheated);
869         break;
870       default:
871         rollState = RollState.Rolling;
872         break;
873     }
874   } else if (yVel == 0 && fabs(xVel <= 2) && isCollisionBottom(1)) {
875     // landed
876     switch (rollState) {
877       case RollState.Rolling:
878         rollState = (level.isInShop(ix/16, iy/16) ? RollState.Finished : RollState.Failed);
879         if (rollState == RollState.Finished) level.player.onDieRolled(self);
880         break;
881       case RollState.Finished:
882       case RollState.Failed:
883         break;
884       case RollState.None:
885       default: rollState = RollState.None; break;
886     }
887   }
889   switch (value) {
890     case 1: setSprite('sDice1'); break;
891     case 2: setSprite('sDice2'); break;
892     case 3: setSprite('sDice3'); break;
893     case 4: setSprite('sDice4'); break;
894     case 5: setSprite('sDice5'); break;
895     default: setSprite('sDice6'); break;
896   }
898   canPickUp = (rollState != RollState.Rolling);
902 defaultproperties {
903   objType = 'Dice';
904   desc = "Die";
905   desc2 = "A six-sided die. The storeowner talks to it every night before he goes to sleep.";
906   setCollisionBounds(-6, 0, 6, 8);
907   heavy = true;
908   //rolled = false;
909   //rolling = false;
910   canBeNudged = true;
911   canPickUp = true;
912   holdYOfs = -4;
913   rollState = RollState.None;
914   bloodless = true; // just in case, lol
915   canHitEnemies = true;