# Difference between revisions of "8-bit panda"

TIC-80 game by Bruno Oliveira

Find the cheat code !

At line 683 you find (finally) the TIC function (main entry point of the program). Notice that the first line is a call to CheckDbgMenu (line 693). The careful observer will be able to decode the "cheat code" for a "debug mode" of the game. TIP: Refer to the key-map.

```   1 -- title:  8 Bit Panda
2 -- author: Bruno Oliveira
3 -- desc:   A panda platformer
4 -- script: lua
5 -- saveid: eightbitpanda
6 --
7 -- WARNING: this file must be kept under
8 -- 64kB (TIC-80 limit)!
9
10 NAME="8-BIT PANDA"
11 C=8
12 ROWS=17
13 COLS=30
14 SCRW=240
15 SCRH=136
16
17 -- jump sequence (delta y at each frame)
18 JUMP_DY={-3,-3,-3,-3,-2,-2,-2,-2,
19   -1,-1,0,0,0,0,0}
20
21 -- swimming seq (implemented as a "jump")
22 SWIM_JUMP_DY={-2,-2,-1,-1,-1,-1,0,0,0,0,0}
23 RESURF_DY={-3,-3,-2,-2,-1,-1,0,0,0,0,0}
24
25 -- attack sequence (1=preparing,
26 -- 2=attack,3=recovery)
27 ATK_SEQ={1,1,1,1,2,3,3,3}
28
29 -- die sequence (dx,dy)
30 DIE_SEQ={{-1,-1},{-2,-2},{-3,-3},{-4,-4},
31  {-5,-5},{-6,-5},{-7,-4},{-8,-3},{-8,-2},
32  {-8,1},{-8,3},{-8,5},{-8,9},{-8,13},
33  {-8,17},{-8,21},{-8,26},{-8,32},{-8,39}
34 }
35
36 -- display x coords in which
37 -- to keep the player (for scrolling)
38 SX_MIN=50
39 SX_MAX=70
40
41 -- entity/tile solidity
42 SOL={
43  NOT=0,  -- not solid
44  HALF=1, -- only when going down,
45          -- allows movement upward.
46  FULL=2, -- fully solid
47 }
48
49 FIRE={
50  -- duration of fire powerup
51  DUR=1000,
52  -- time between successive fires.
53  INTERVAL=20,
54  -- offset from player pos
55  OFFY=2,OFFX=7,OFFX_FLIP=-2,
56  OFFX_PLANE=14,OFFY_PLANE=8,
57  -- projectile collision rect
58  COLL={x=0,y=0,w=3,h=3},
59 }
60
61 -- Tiles
62 -- 0: empty
63 -- 1-79: static solid blocks
64 -- 80-127: decorative
65 -- 128-239: entities
66 -- 240-255: special markers
67 T={
68  EMPTY=0,
69  -- platform that's only solid when
70  -- going down, but allows upward move
71  HPLAF=4,
72
73  SURF=16,
74  WATER=32,
75  WFALL=48,
76
77  TARMAC=52, -- (where plane can land).
78
79  -- sprite id above which tiles are
80  -- non-solid decorative elements
81  FIRST_DECO=80,
82
83  -- level-end gate components
84  GATE_L=110,GATE_R=111,
85  GATE_L2=142,GATE_R=143,
86
87  -- tile id above which tiles are
88  -- representative of entities, not bg
89  FIRST_ENT=128,
90
91  -- tile id above which tiles have special
92  -- meanings
93  FIRST_META=240,
94
95  -- number markers (used for level
96  -- packing and annotations).
97  META_NUM_0=240,
98    -- followed by nums 1-9.
99
100  -- A/B markers (entity-specific meaning)
101  META_A=254,
102  META_B=255
103 }
104
105 -- Autocomplete of tiles patterns.
106 -- Auto filled when top left map tile
107 -- is present.
108 TPAT={
109  [85]={w=2,h=2},
110  [87]={w=2,h=2},
111  [94]={w=2,h=2},
112  [89]={w=2,h=2},
113 }
114
115 -- solidity of tiles (overrides)
116 TSOL={
117  [T.EMPTY]=SOL.NOT,
118  [T.HPLAF]=SOL.HALF,
119  [T.SURF]=SOL.NOT,
120  [T.WATER]=SOL.NOT,
121  [T.WFALL]=SOL.NOT,
122 }
123
124 -- animated tiles
125 TANIM={
126  [T.SURF]={T.SURF,332},
127  [T.WFALL]={T.WFALL,333,334,335},
128 }
129
130 -- sprites
131 S={
132  PLR={  -- player sprites
133   STAND=257,
134   WALK1=258,
135   WALK2=259,
136   JUMP=273,
137   SWING=276,
138   SWING_C=260,
139   HIT=277,
140   HIT_C=278,
141   DIE=274,
142   SWIM1=267,SWIM2=268,
143   -- overlays for fire powerup
144   FIRE_BAMBOO=262, -- bamboo powerup
145   FIRE_F=265,  -- suit, front
146   FIRE_P=266,  -- suit, profile
147   FIRE_S=284,  -- suit, swimming
148   -- overlays for super panda powerup
149   SUPER_F=281, -- suit, front
150   SUPER_P=282, -- suit, profile
151   SUPER_S=283, -- suit, swimming
152  },
153  EN={  -- enemy sprites
154   A=176,
155   B=177,
156   DEMON=178,
157   DEMON_THROW=293,
158   SLIME=180,
159   BAT=181,
160   HSLIME=182, -- hidden slime
161   DASHER=183,
162   VBAT=184,
163   SDEMON=185, -- snow demon
164   SDEMON_THROW=300,
165   PDEMON=188,  -- plasma demon
166   PDEMON_THROW=317,
167   FISH=189,
168   FISH2=190,
169  },
170  -- crumbling block
171  CRUMBLE=193,CRUMBLE_2=304,CRUMBLE_3=305,
172  FIREBALL=179,
173  FIRE_1=263,FIRE_2=264,
174  LIFT=192,
175  PFIRE=263, -- player fire (bamboo)
176  FIRE_PWUP=129,
177  -- background mountains
178  BGMNT={DIAG=496,FULL=497},
179  SCRIM=498, -- also 499,500
180  SPIKE=194,
181  CHEST=195,CHEST_OPEN=311,
182  -- timed platform (opens and closes)
183  TPLAF=196,TPLAF_HALF=312,TPLAF_OFF=313,
184  SUPER_PWUP=130,
185  SIGN=197,
186  SNOWBALL=186,
187  FLAG=198,
188  FLAG_T=326,  -- flag after taken
189  ICICLE=187,   -- icicle while hanging
190  ICICLE_F=303, -- icicle falling
191  PLANE=132,  -- plane (item)
192  AVIATOR=336, -- aviator sprite (3x2)
193  AVIATOR_PROP_1=339, -- propeller anim
194  AVIATOR_PROP_2=340, -- propeller anim
195  PLASMA=279,  -- plasma ball
196  SICICLE=199,   -- stone-themed icicle,
197                 -- while hanging
198  SICICLE_F=319, -- stone-themed icicle,
199                 -- while falling
200  FUEL=200,      -- fuel item
201  IC_FUEL=332,   -- icon for HUD
202  TINY_NUM_00=480, -- "00" sprite
203  TINY_NUM_50=481, -- "50" sprite
204  TINY_NUM_R1=482, -- 1-10, right aligned
205
206  -- food items
207  FOOD={LEAF=128,A=133,B=134,C=135,D=136},
208
209  SURF1=332,SURF2=333, -- water surface fx
210
211  -- world map tiles
212  WLD={
213   -- tiles that player can walk on
215   -- level tiles
216   LVL1=61,LVL2=62,LVL3=63,
217   LVLF=79, -- finale level
218   -- "cleared level" tile
219   LVLC=463,
220  },
221
222  -- Special EIDs that don't correspond to
223  -- sprites. ID must be > 512
224  POP=600, -- entity that dies immediately
225           -- with a particle effect
226 }
227
228 -- Sprite numbers also function as entity
229 -- IDs. For readability we write S.FOO
230 -- when it's a sprite but EID.FOO when
231 -- it identifies an entity type.
232 EID=S
233
234 -- anims for each entity ID
235 ANIM={
236  [EID.EN.A]={S.EN.A,290},
237  [EID.EN.B]={S.EN.B,291},
238  [EID.EN.DEMON]={S.EN.DEMON,292},
239  [EID.EN.SLIME]={S.EN.SLIME,295},
240  [EID.EN.BAT]={S.EN.BAT,296},
241  [EID.FIREBALL]={S.FIREBALL,294},
242  [EID.FOOD.LEAF]={S.FOOD.LEAF,288,289},
243  [EID.PFIRE]={S.PFIRE,264},
244  [EID.FIRE_PWUP]={S.FIRE_PWUP,306,307},
245  [EID.EN.HSLIME]={S.EN.HSLIME,297},
246  [EID.SPIKE]={S.SPIKE,308},
247  [EID.CHEST]={S.CHEST,309,310},
248  [EID.EN.DASHER]={S.EN.DASHER,314},
249  [EID.EN.VBAT]={S.EN.VBAT,298},
250  [EID.SUPER_PWUP]={S.SUPER_PWUP,320,321},
251  [EID.EN.SDEMON]={S.EN.SDEMON,299},
252  [EID.SNOWBALL]={S.SNOWBALL,301},
253  [EID.FOOD.D]={S.FOOD.D,322,323,324},
254  [EID.FLAG]={S.FLAG,325},
255  [EID.ICICLE]={S.ICICLE,302},
256  [EID.SICICLE]={S.SICICLE,318},
257  [EID.PLANE]={S.PLANE,327,328,329},
258  [EID.EN.PDEMON]={S.EN.PDEMON,316},
259  [EID.PLASMA]={S.PLASMA,280},
260  [EID.FUEL]={S.FUEL,330,331},
261  [EID.EN.FISH]={S.EN.FISH,368},
262  [EID.EN.FISH2]={S.EN.FISH2,369},
263 }
264
265 PLANE={
266  START_FUEL=2000,
267  MAX_FUEL=4000,
268  FUEL_INC=1000,
269  FUEL_BAR_W=50
270 }
271
272 -- modes
273 M={
274  BOOT=0,
275  TITLE=1,    -- title screen
276  TUT=2,      -- instructions
277  RESTORE=3,  -- prompting to restore game
278  WLD=4,      -- world map
279  PREROLL=5,  -- "LEVEL X-Y" banner
280  PLAY=6,
281  DYING=7,    -- die anim
282  EOL=8,      -- end of level
283  GAMEOVER=9,
284  WIN=10,     -- beat entire game
285 }
286
287 -- collider rects
288 CR={
289  PLR={x=2,y=0,w=4,h=8},
290  AVIATOR={x=-6,y=2,w=18,h=10},
291  -- default
292  DFLT={x=2,y=0,w=4,h=8},
293  FULL={x=0,y=0,w=8,h=8},
294  -- small projectiles
295  BALL={x=2,y=2,w=3,h=3},
296  -- just top rows
297  TOP={x=0,y=0,w=8,h=2},
298  -- player attack
299  ATK={x=6,y=0,w=7,h=8},
300  -- what value to use for x instead if
301  -- player is flipped (facing left)
302  ATK_FLIP_X=-5,
303  FOOD={x=1,y=1,w=6,h=6},
304 }
305
306 -- max dist entity to update it
307 ENT_MAX_DIST=220
308
309 -- EIDs to always update regardless of
310 -- distance.
311 ALWAYS_UPDATED_EIDS={
312  -- lifts need to always be updated for
313  -- position determinism.
314  [EID.LIFT]=true
315 }
316
317 -- player damage types
318 DMG={
319  MELEE=0,      -- melee attack
320  FIRE=1,       -- fire from fire powerup
321  PLANE_FIRE=2, -- fire from plane
322 }
323
324 -- default palette
325 PAL={
326  [0]=0x000000,  [1]=0x402434,
327  [2]=0x30346d,  [3]=0x4a4a4a,
328  [4]=0x854c30,  [5]=0x346524,
329  [6]=0xd04648,  [7]=0x757161,
330  [8]=0x34446d,  [9]=0xd27d2c,
331  [10]=0x8595a1, [11]=0x6daa2c,
332  [12]=0x1ce68d, [13]=0x6dc2ca,
334 }
335
336 -- music tracks
337 BGM={A=0,B=1,EOL=2,C=3,WLD=4,TITLE=5,
338  FINAL=6,WIN=7}
339
340 -- bgm for each mode (except M.PLAY, which
341 -- is special)
342 BGMM={
343  [M.TITLE]=BGM.TITLE,
344  [M.WLD]=BGM.WLD,
345  [M.EOL]=BGM.EOL,
346  [M.WIN]=BGM.WIN,
347 }
348
349 -- map data is organized in pages.
350 -- Each page is 30x17. TIC80 has 64 map
351 -- pages laid out as an 8x8 grid. We
352 -- number them in reading order, so 0
353 -- is top left, 63 is bottom right.
354
355 -- Level info.
356 -- Levels in the cart are packed
357 -- (RLE compressed). When a level is loaded,
358 -- it gets unpacked to the top 8 map pages
359 -- (0,0-239,16).
360 --  palor: palette overrides
361 --  pkstart: map page where packed
362 --    level starts.
363 --  pklen: length of level. Entire level
364 --    must be on same page row, can't
365 --    span multiple page rows.
366 LVL={
367  {
368   name="1-1",bg=2,
369   palor={},
370   pkstart=8,pklen=3,
371   mus=BGM.A,
372  },
373  {
374   name="1-2",bg=0,
375   palor={[8]=0x102428},
376   pkstart=11,pklen=2,
377   mus=BGM.B,
378  },
379  {
380   name="1-3",bg=2,
381   pkstart=13,pklen=3,
382   mus=BGM.C,
383   save=true,
384  },
385  {
386   name="2-1",bg=1,
387   palor={[8]=0x553838},
388   pkstart=16,pklen=3,
389   mus=BGM.A,
390  },
391  {
392   name="2-2",bg=0,
393   palor={[8]=0x553838},
394   pkstart=19,pklen=2,
395   snow={clr=2},
396   mus=BGM.B,
397  },
398  {
399   name="2-3",bg=1,
400   palor={[8]=0x553838},
401   pkstart=21,pklen=3,
402   mus=BGM.C,
403   save=true,
404  },
405  {
406   name="3-1",bg=2,
407   palor={[8]=0x7171ae},
408   pkstart=24,pklen=3,
409   snow={clr=10},
410   mus=BGM.A,
411  },
412  {
413   name="3-2",bg=0,
414   palor={[8]=0x3c3c50},
415   pkstart=27,pklen=2,
416   snow={clr=10},
417   mus=BGM.B,
418  },
419  {
420   name="3-3",bg=2,
421   palor={[8]=0x7171ae},
422   pkstart=29,pklen=3,
423   mus=BGM.C,
424   save=true,
425  },
426  {
427   name="4-1",bg=2,
428   palor={[2]=0x443c14,[8]=0x504410},
429   pkstart=32,pklen=3,
430   mus=BGM.A,
431  },
432  {
433   name="4-2",bg=2,
434   palor={[2]=0x443c14,[8]=0x504410},
435   pkstart=35,pklen=2,
436   mus=BGM.B,
437  },
438  {
439   name="4-3",bg=2,
440   palor={[2]=0x443c14,[8]=0x504410},
441   pkstart=37,pklen=3,
442   mus=BGM.C,
443   save=true,
444  },
445  {
446   name="5-1",bg=1,
447   palor={[8]=0x553838},
448   pkstart=40,pklen=3,
449   mus=BGM.A,
450  },
451  {
452   name="5-2",bg=1,
453   palor={[8]=0x553838},
454   pkstart=43,pklen=2,
455   mus=BGM.B,
456  },
457  {
458   name="5-3",bg=1,
459   palor={[8]=0x553838},
460   pkstart=45,pklen=3,
461   mus=BGM.C,
462   save=true,
463  },
464  {
465   name="6-1",bg=0,
466   palor={[8]=0x303030},
467   pkstart=48,pklen=3,
468   mus=BGM.FINAL,
469   snow={clr=8},
470  },
471  {
472   name="6-2",bg=0,
473   palor={[8]=0x303030},
474   pkstart=51,pklen=5,
475   mus=BGM.FINAL,
476   snow={clr=8},
477  },
478 }
479
480 -- length of unpacked level, in cols
481 -- 240 means the top 8 map pages
482 LVL_LEN=240
483
484 -- sound specs
485 SND={
486  KILL={sfxid=62,note=30,dur=5},
487  JUMP={sfxid=61,note=30,dur=4},
488  SWIM={sfxid=61,note=50,dur=3},
489  ATTACK={sfxid=62,note=40,dur=4},
490  POINT={sfxid=60,note=60,dur=5,speed=3},
491  DIE={sfxid=63,note=18,dur=20,speed=-1},
492  HURT={sfxid=63,note="C-4",dur=4},
493  PWUP={sfxid=60,note=45,dur=15,speed=-2},
494  ONEUP={sfxid=60,note=40,dur=60,speed=-3},
495  PLANE={sfxid=59,note="C-4",dur=70,speed=-3},
496  OPEN={sfxid=62,note="C-3",dur=4,speed=-2},
497 }
498
499 -- world map consts
500 WLD={
501  -- foreground tile page
502  FPAGE=61,
503  -- background tile page
504  BPAGE=62,
505 }
506
507 -- WLD point of interest types
508 POI={
509  LVL=0,
510 }
511
512 -- settings
513 Sett={
514  snd=true,
515  mus=true
516 }
517
518 -- game state
519 Game={
520  -- mode
521  m=M.BOOT,
522  -- ticks since mode start
523  t=0,
524  -- current level# we're playing
525  lvlNo=0,
526  lvl=nil,  -- shortcut to LVL[lvlNo]
527  -- scroll offset in current level
528  scr=0,
529  -- auto-generated background mountains
530  bgmnt=nil,
531  -- snow flakes (x,y pairs). These don't
532  -- change, we just shift when rendering.
533  snow=nil,
534  -- highest level cleared by player,
535  -- -1 if no level cleared
536  topLvl=-1,
537 }
538
539 -- world map state
540 Wld={
541  -- points of interest (levels, etc)
542  pois={},
543  -- savegame start pos (maps start level
544  -- to col,row)
545  spos={},
546  plr={
547   -- start pos
548   x0=-1,y0=-1,
549   -- player pos, in pixels not row/col
550   x=0,y=0,
551   -- player move dir, if moving. Will move
552   -- until plr arrives at next cell
553   dx=0,dy=0,
554   -- last move dir
555   ldx=0,ldy=0,
556   -- true iff player facing left
557   flipped=false,
558  }
559 }
560
561 -- player
562 Plr={} -- deep-copied from PLR_INIT_STATE
563 PLR_INIT_STATE={
564  lives=3,
565  x=0,y=0, -- current pos
566  dx=0,dy=0, -- last movement
567  flipped=false, -- if true, is facing left
568  jmp=0, -- 0=not jumping, otherwise
569         -- it's the cur jump frame
570  jmpSeq=JUMP_DY, -- set during jump
571  grounded=false,
572  swim=false,
573  -- true if plr is near surface of water
574  surf=false,
575  -- attack state. 0=not attacking,
576  -- >0 indexes into ATK_SEQ
577  atk=0,
578  -- die animation frame, 0=not dying
579  -- indexes into DIE_SEQ
580  dying=0,
581  -- nudge (movement resulting from
582  -- collisions)
583  nudgeX=0,nudgeY=0,
584  -- if >0, has fire bamboo powerup
585  -- and this is the countdown to end
586  firePwup=0,
587  -- if >0 player has fired bamboo.
588  -- This is ticks until player can fire
589  -- again.
590  fireCd=0,
591  -- if >0, is invulnerable for this
592  -- many ticks, 0 if not invulnerable
593  invuln=0,
594  -- if true, has the super panda powerup
595  super=false,
596  -- if != 0, player is being dragged
597  -- horizontally (forced to move in that
598  -- direction -- >0 is right, <0 is left)
599  -- The abs value is how many frames
600  -- this lasts for.
601  drag=0,
602  -- the sign message (index) the player
603  -- is currently reading.
604  signMsg=0,
605  -- sign cycle counter: 1 when just
606  -- starting to read sign, increases.
607  -- when player stops reading sign,
608  -- decreases back to 0.
609  signC=0,
610  -- respawn pos, 0,0 if unset
611  respX=-1,respY=-1,
612  -- if >0, the player is on the plane
613  -- and this is the fuel left (ticks).
614  plane=0,
615  -- current score
616  score=0,
617  -- for performance, we keep the
618  -- stringified score ready for display
619  scoreDisp={text=nil,value=-1},
620  -- time (Game.t) when score last changed
621  scoreMt=-999,
622  -- if >0, player is blocked from moving
623  -- for that many frames.
624  locked=0,
625 }
626
627 -- max cycle counter for signs
628 SIGN_C_MAX=10
629
630 -- sign texts.
631 SIGN_MSGS={
632  [0]={
633   l1="Green bamboo protects",
634   l2="against one enemy attack.",
635  },
636  [1]={
637   l1="Yellow bamboo allows you to throw",
638   l2="bamboo shoots (limited time).",
639  },
640  [2]={
641   l1="Pick up leaves and food to get",
642   l2="points. 10,000 = extra life.",
643  },
644  [4]={
645   l1="Bon voyage!",
646   l2="Don't run out of fuel.",
647  },
648 }
649
650 -- entities
651 Ents={}
652
653 -- particles
654 Parts={}
655
656 -- score toasts
657 Toasts={}
658
659 -- animated tiles, for quick lookup
660 -- indexed by COLUMN.
661 -- Tanims[c] is a list of integers
662 -- indicating rows of animated tiles.
663 Tanims={}
664
665 function SetMode(m)
666  Game.m=m
667  Game.t=0
668  if m~=M.PLAY and m~=M.DYING and
669    m~=M.EOL then
670   ResetPal()
671  end
672  UpdateMus()
673 end
674
675 function UpdateMus()
676  if Game.m==M.PLAY then
677   PlayMus(Game.lvl.mus)
678  else
679   PlayMus(BGMM[Game.m] or -1)
680  end
681 end
682
683 function TIC()
685  if Plr.dbg then
686   DbgTic()
687   return
688  end
689  Game.t=Game.t+1
690  TICF[Game.m]()
691 end
692
694  if not btn(6) then
695   Game.dbgkc=0
696   return
697  end
698  if btnp(0) then
699   Game.dbgkc=10+(Game.dbgkc or 0)
700  end
701  if btnp(1) then
702   Game.dbgkc=1+(Game.dbgkc or 0)
703  end
704  if Game.dbgkc==42 then Plr.dbg=true end
705 end
706
707 function Boot()
708  ResetPal()
709  WldInit()
710  SetMode(M.TITLE)
711 end
712
713 -- restores default palette with
714 -- the given overrides.
715 function ResetPal(palor)
716  for c=0,15 do
717   local clr=PAL[c]
718   if palor and palor[c] then
719    clr=palor[c]
720   end
721   poke(0x3fc0+c*3+0,(clr>>16)&255)
722   poke(0x3fc0+c*3+1,(clr>>8)&255)
723   poke(0x3fc0+c*3+2,clr&255)
724  end
725 end
726
727 function TitleTic()
728  ResetPal()
729  cls(2)
730  local m=MapPageStart(63)
731  map(m.c,m.r,30,17,0,0,0)
732
733  spr(S.PLR.WALK1+(time()//128)%2,16,104,0)
734  rect(0,0,240,24,5)
735  print(NAME,88,10)
736  rect(0,24,240,1,15)
737  rect(0,26,240,1,5)
738  rect(0,SCRH-8,SCRW,8,0)
739  print("github.com/btco/panda",60,SCRH-7,7)
740
741  if (time()//512)%2>0 then
742   print("- PRESS 'Z' TO START -",65,84,15)
743  end
744
745  RendSpotFx(COLS//2,ROWS//2,Game.t)
746  if btnp(4) then
747   SetMode(M.RESTORE)
748  end
749 end
750
751 function RestoreTic()
752  local saveLvl=pmem(0) or 0
753  if saveLvl<1 then
754   StartGame(1)
755   return
756  end
757
758  Game.restoreSel=Game.restoreSel or 0
759
760  cls(0)
761  local X=40
762  local Y1=30
763  local Y2=60
764  print("CONTINUE (LEVEL "..
765    LVL[saveLvl].name ..")",X,Y1)
766  print("START NEW GAME",X,Y2)
767  spr(S.PLR.STAND,X-20,
768   Iif(Game.restoreSel>0,Y2,Y1))
769  if btnp(0) or btnp(1) then
770   Game.restoreSel=
771    Iif(Game.restoreSel>0,0,1)
772  elseif btnp(4) then
773   StartGame(Game.restoreSel>0 and
774     1 or saveLvl)
775  end
776 end
777
778 function TutTic()
779  cls(0)
780  if Game.tutdone then
781   StartLvl(1)
782   return
783  end
784  local p=MapPageStart(56)
785  map(p.c,p.r,COLS,ROWS)
786
787  print("CONTROLS",100,10)
788  print("JUMP",56,55);
789  print("ATTACK",72,90);
790  print("MOVE",160,50);
791  print("10,000 PTS = EXTRA LIFE",60,110,3);
792
793  if Game.t>150 and 0==((Game.t//16)%2) then
794    print("- Press Z to continue -",60,130)
795  end
796
797  if Game.t>150 and btnp(4) then
798   Game.tutdone=true
799   StartLvl(1)
800  end
801 end
802
803 function WldTic()
804  WldUpdate()
805  WldRend()
806 end
807
808 function PrerollTic()
809  cls(0)
810  print("LEVEL "..Game.lvl.name,100,40)
811  spr(S.PLR.STAND,105,60)
812  print("X " .. Plr.lives,125,60)
813  if Game.t>60 then
814   SetMode(M.PLAY)
815  end
816 end
817
818 function PlayTic()
819  if Plr.dbgFly then
820   UpdateDbgFly()
821  else
822   UpdatePlr()
823   UpdateEnts()
824   UpdateParts()
825   DetectColl()
826   ApplyNudge()
827   CheckEndLvl()
828  end
830  Rend()
831  if Game.m==M.PLAY then
832   RendSpotFx((Plr.x-Game.scr)//C,
833     Plr.y//C,Game.t)
834  end
835 end
836
837 function EolTic()
838  if Game.t>160 then
840   return
841  end
842  Rend()
843  print("LEVEL CLEAR",85,20)
844 end
845
846 function DyingTic()
847  Plr.dying=Plr.dying+1
848
849  if Game.t>100 then
850   if Plr.lives>1 then
851    Plr.lives=Plr.lives-1
852    SetMode(M.WLD)
853   else
854    SetMode(M.GAMEOVER)
855   end
856  else
857   Rend()
858  end
859 end
860
861 function GameOverTic()
862  cls(0)
863  print("GAME OVER!",92,50)
864  if Game.t>150 then
865   SetMode(M.TITLE)
866  end
867 end
868
869 function WinTic()
870  cls(0)
871  Game.scr=0
872  local m=MapPageStart(57)
873  map(m.c,m.r,
874    math.min(30,(Game.t-300)//8),17,0,0,0)
875  print("THE END!",100,
876    math.max(20,SCRH-Game.t//2))
877  print("Thanks for playing!",70,
878    math.max(30,120+SCRH-Game.t//2))
879
880  if Game.t%100==0 then
881   SpawnParts(PFX.FW,Rnd(40,SCRW-40),
882    Rnd(40,SCRH-40),Rnd(2,15))
883  end
884
885  UpdateParts()
886  RendParts()
887
888  if Game.t>1200 and btnp(4) then
889   SetMode(M.TITLE)
890  end
891 end
892
893 TICF={
894  [M.BOOT]=Boot,
895  [M.TITLE]=TitleTic,
896  [M.TUT]=TutTic,
897  [M.RESTORE]=RestoreTic,
898  [M.WLD]=WldTic,
899  [M.PREROLL]=PrerollTic,
900  [M.PLAY]=PlayTic,
901  [M.DYING]=DyingTic,
902  [M.GAMEOVER]=GameOverTic,
903  [M.EOL]=EolTic,
904  [M.WIN]=WinTic,
905 }
906
907 function StartGame(startLvlNo)
908  Game.topLvl=startLvlNo-1
909  Plr=DeepCopy(PLR_INIT_STATE)
910  -- put player at the right start pos
911  local sp=Wld.spos[startLvlNo] or
912    {x=Wld.plr.x0,y=Wld.plr.y0}
913  Wld.plr.x=sp.x
914  Wld.plr.y=sp.y
915  SetMode(M.WLD)
916 end
917
918 function StartLvl(lvlNo)
919  local oldLvlNo=Game.lvlNo
920  Game.lvlNo=lvlNo
921  Game.lvl=LVL[lvlNo]
922  Game.scr=0
923  local old=Plr
924  Plr=DeepCopy(PLR_INIT_STATE)
925  -- preserve lives, score
926  Plr.lives=old.lives
927  Plr.score=old.score
928  Plr.super=old.super
929  if oldLvlNo==lvlNo then
930   Plr.respX=old.respX
931   Plr.respY=old.respY
932  end
933  SetMode(M.PREROLL)
934  Ents={}
935  Parts={}
936  Toasts={}
937  Tanims={}
938  UnpackLvl(lvlNo,UMODE.GAME)
939  GenBgMnt()
940  GenSnow()
941  ResetPal(Game.lvl.palor)
943 end
944
946  if Plr.respX<0 then return end
947  for i=1,#Ents do
948   local e=Ents[i]
949   if e.eid==EID.FLAG and e.x<Plr.respX then
950    EntRepl(e,EID.FLAG_T)
951   end
952  end
953  Plr.x=Plr.respX
954  Plr.y=Plr.respY
955 end
956
957 -- generates background mountains.
958 function GenBgMnt()
959  local MAX_Y=12
960  local MIN_Y=2
961  -- min/max countdown to change direction:
962  local MIN_CD=2
963  local MAX_CD=6
964  Game.bgmnt={}
965  RndSeed(Game.lvlNo)
966  local y=Rnd(MIN_Y,MAX_Y)
967  local dy=1
968  local cd=Rnd(MIN_CD,MAX_CD)
969  for i=1,LVL_LEN do
970   Ins(Game.bgmnt,{y=y,dy=dy})
971   cd=cd-1
972   if cd<=0 or y+dy<MIN_Y or y+dy>MAX_Y then
973    -- keep same y but change direction
974    cd=Rnd(MIN_CD,MAX_CD)
975    dy=-dy
976   else
977    y=y+dy
978   end
979  end
980  RndSeed(time())
981 end
982
983 function GenSnow()
984  if not Game.lvl.snow then
985   Game.snow=nil
986   return
987  end
988  Game.snow={}
989  for r=0,ROWS-1,2 do
990   for c=0,COLS-1,2 do
991    Ins(Game.snow,{
992     x=c*C+Rnd(-8,8),
993     y=r*C+Rnd(-8,8)
994    })
995   end
996  end
997 end
998
999 -- Whether player is on solid ground.
1000 function IsOnGround()
1001  return not CanMove(Plr.x,Plr.y+1)
1002 end
1003
1004 -- Get level tile at given point
1005 function LvlTileAtPt(x,y)
1006  return LvlTile(x//C,y//C)
1007 end
1008
1009 -- Get level tile.
1010 function LvlTile(c,r)
1011  if c<0 or c>=LVL_LEN then return 0 end
1012  if r<0 then return 0 end
1013  -- bottom-most tile repeats infinitely
1014  -- below (to allow player to swim
1015  -- when bottom tile is water).
1016  if r>=ROWS then r=ROWS-1 end
1017  return mget(c,r)
1018 end
1019
1020 function SetLvlTile(c,r,t)
1021  if c<0 or c>=LVL_LEN then return false end
1022  if r<0 or r>=ROWS then return false end
1023  mset(c,r,t)
1024 end
1025
1026 function UpdatePlr()
1027  local oldx=Plr.x
1028  local oldy=Plr.y
1029
1030  Plr.plane=Max(Plr.plane-1,0)
1031  Plr.fireCd=Max(Plr.fireCd-1,0)
1032  Plr.firePwup=Max(Plr.firePwup-1,0)
1033  Plr.invuln=Max(Plr.invuln-1,0)
1034  Plr.drag=Iif2(Plr.drag>0,Plr.drag-1,
1035    Plr.drag<0,Plr.drag+1,0)
1036  Plr.signC=Max(Plr.signC-1,0)
1037  Plr.locked=Max(Plr.locked-1,0)
1039
1040  local swimmod=Plr.swim and Game.t%2 or 0
1041
1042  if (Plr.plane==0 and Plr.jmp==0 and
1043    not IsOnGround()) then
1044   -- fall
1045   Plr.y=Plr.y+1-swimmod
1046  end
1047
1048  -- check if player fell into pit
1049  if Plr.y>SCRH+8 then
1050   StartDying()
1051   return
1052  end
1053
1054  -- horizontal movement
1055  local dx=0
1056  local dy=0
1057  local wantLeft=Plr.locked==0 and
1058    Iif(Plr.drag==0,btn(2),Plr.drag<0)
1059  local wantRight=Plr.locked==0 and
1060    Iif(Plr.drag==0,btn(3),Plr.drag>0)
1061  local wantJmp=Plr.locked==0 and
1062    Plr.plane==0 and btnp(4) and Plr.drag==0
1063  local wantAtk=Plr.locked==0 and
1064    btnp(5) and Plr.drag==0
1065
1066  if wantLeft then
1067   dx=-1+swimmod
1068   -- plane doesn't flip
1069   Plr.flipped=true
1070  elseif wantRight then
1071   dx=1-swimmod
1072   Plr.flipped=false
1073  end
1074
1075  -- vertical movement (plane only)
1076  dy=dy+Iif2(Plr.plane>0 and btn(0) and
1077    Plr.y>8,-1,
1078    Plr.plane>0 and btn(1) and
1079    Plr.y<SCRH-16,1,0)
1080
1081  -- is player flipped (facing left?)
1082  Plr.flipped=Iif3(
1083    Plr.plane>0,false,btn(2),true,
1084    btn(3),false,Plr.flipped)
1085
1086  TryMoveBy(dx,dy)
1087
1088  Plr.grounded=Plr.plane==0 and IsOnGround()
1089
1090  local canJmp=Plr.grounded or Plr.swim
1091  -- jump
1092  if wantJmp and canJmp then
1093   Plr.jmp=1
1094   Plr.jmpSeq=Plr.surf and
1095     RESURF_DY or
1096     (Plr.swim and SWIM_JUMP_DY or
1097     JUMP_DY)
1098   Snd(Plr.surf and SND.JUMP or
1099     Plr.swim and SND.SWIM or SND.JUMP)
1100   -- TODO play swim snd if swim
1101  end
1102
1103  if Plr.jmp>#Plr.jmpSeq then
1104   -- end jump
1105   Plr.jmp=0
1106  elseif Plr.jmp>0 then
1107   local ok=TryMoveBy(
1108     0,Plr.jmpSeq[Plr.jmp])
1109   -- if blocked, cancel jump
1110   Plr.jmp=ok and Plr.jmp+1 or 0
1111  end
1112
1113  -- attack
1114  if Plr.atk==0 then
1115   if wantAtk then
1116    -- start attack sequence
1117    if Plr.plane==0 then Plr.atk=1 end
1118    Snd(SND.ATTACK)
1119    TryFire()
1120   end
1121  elseif Plr.atk>#ATK_SEQ then
1122   -- end of attack sequence
1123   Plr.atk=0
1124  else
1125   -- advance attack sequence
1126   Plr.atk=Plr.atk+1
1127  end
1128
1129  -- check plane landing
1130  if Plr.plane>0 then CheckTarmac() end
1131
1132  Plr.dx=Plr.x-oldx
1133  Plr.dy=Plr.y-oldy
1134 end
1135
1136 function IsWater(t)
1137  return t==T.WATER or t==T.SURF or
1138    t==T.WFALL
1139 end
1140
1142  local wtop=IsWater(
1143    LvlTileAtPt(Plr.x+4,Plr.y+1))
1144  local wbottom=IsWater(
1145    LvlTileAtPt(Plr.x+4,Plr.y+7))
1146  local wtop2=IsWater(
1147    LvlTileAtPt(Plr.x+4,Plr.y-8))
1148  Plr.swim=wtop and wbottom
1149  -- is plr near surface?
1150  Plr.surf=wbottom and not wtop2
1151 end
1152
1153 function UpdateDbgFly()
1154  local d=Iif(btn(4),5,1)
1155  if btn(0) then Plr.y=Plr.y-d end
1156  if btn(1) then Plr.y=Plr.y+d end
1157  if btn(2) then Plr.x=Plr.x-d end
1158  if btn(3) then Plr.x=Plr.x+d end
1159  if btn(5) then Plr.dbgFly=false end
1160 end
1161
1162 function TryFire()
1163  if Plr.firePwup<1 and Plr.plane==0 then
1164   return
1165  end
1166  if Plr.fireCd>0 then return end
1167  Plr.fireCd=FIRE.INTERVAL
1168  local x=Plr.x
1169  if Plr.plane==0 then
1170   x=x+(Plr.flipped and
1171     FIRE.OFFX_FLIP or FIRE.OFFX)
1172  else
1173   -- end of plane
1174   x=x+FIRE.OFFX_PLANE
1175  end
1176  local y=Plr.y+Iif(Plr.plane>0,
1177    FIRE.OFFY_PLANE,FIRE.OFFY)
1179  e.moveDx=Plr.plane>0 and 2 or
1180    (Plr.flipped and -1 or 1)
1181  e.ttl=Plr.plane>0 and e.ttl//2 or e.ttl
1182 end
1183
1184 function ApplyNudge()
1185  Plr.y=Plr.y+Plr.nudgeY
1186  Plr.x=Plr.x+Plr.nudgeX
1187  Plr.nudgeX=0
1188  Plr.nudgeY=0
1189 end
1190
1191 function TryMoveBy(dx,dy)
1192  if CanMove(Plr.x+dx,Plr.y+dy) then
1193   Plr.x=Plr.x+dx
1194   Plr.y=Plr.y+dy
1195   return true
1196  end
1197  return false
1198 end
1199
1200 function GetPlrCr()
1201  return Iif(Plr.plane>0,CR.AVIATOR,CR.PLR)
1202 end
1203
1204 -- Check if plr can move to given pos.
1205 function CanMove(x,y)
1206  local dy=y-Plr.y
1207  local pcr=GetPlrCr()
1208  local r=CanMoveEx(x,y,pcr,dy)
1209  if not r then return false end
1210
1211  -- check if would bump into solid ent
1212  local pr=RectXLate(pcr,x,y)
1213  for i=1,#Ents do
1214   local e=Ents[i]
1215   local effSolid=(e.sol==SOL.FULL) or
1216    (e.sol==SOL.HALF and dy>0 and
1217    Plr.y+5<e.y) -- (HACK)
1218   if effSolid then
1219    local er=RectXLate(e.coll,e.x,e.y)
1220    if RectIsct(pr,er) then
1221     return false
1222    end
1223   end
1224  end
1225  return true
1226 end
1227
1228 function EntCanMove(e,x,y)
1229  return CanMoveEx(x,y,e.coll,y-e.y)
1230 end
1231
1232 function GetTileSol(t)
1233  local s=TSOL[t]
1234  -- see if an override is present.
1235  if s~=nil then return s end
1236  -- default:
1237  return Iif(t>=T.FIRST_DECO,SOL.NOT,SOL.FULL)
1238 end
1239
1240 -- x,y: candidate pos; cr: collision rect
1241 -- dy: y direction of movement
1242 function CanMoveEx(x,y,cr,dy)
1243  local x1=x+cr.x
1244  local y1=y+cr.y
1245  local x2=x1+cr.w-1
1246  local y2=y1+cr.h-1
1247  -- check all tiles touched by the rect
1248  local startC=x1//C
1249  local endC=x2//C
1250  local startR=y1//C
1251  local endR=y2//C
1252  for c=startC,endC do
1253   for r=startR,endR do
1254    local sol=GetTileSol(LvlTile(c,r))
1255    if sol==SOL.FULL then return false end
1256   end
1257  end
1258
1259  -- special case: check for half-solidity
1260  -- tiles. Only solid when standing on
1261  -- top of them (y2%C==0) and going
1262  -- down (dy>0).
1263  local sA=GetTileSol(LvlTileAtPt(x1,y2))
1264  local sB=GetTileSol(LvlTileAtPt(x2,y2))
1265  if dy>0 and (sA==SOL.HALF or
1266    sB==SOL.HALF) and
1267    y2%C==0 then return false end
1268
1269  return true
1270 end
1271
1272 function EntWouldFall(e,x)
1273  return EntCanMove(e,x,e.y+1)
1274 end
1275
1276 -- check if player landed plane on tarmac
1277 function CheckTarmac()
1278  local pr=RectXLate(
1279    CR.AVIATOR,Plr.x,Plr.y)
1280  local bottom=pr.y+pr.h+1
1281  local t1=LvlTileAtPt(pr.x,bottom)
1282  local t2=LvlTileAtPt(pr.x+pr.w,bottom)
1283  if t1==T.TARMAC and t2==T.TARMAC then
1284   -- landed
1285   Plr.plane=0
1286   SpawnParts(PFX.POP,Plr.x+4,Plr.y,14)
1287   -- TODO: more vfx, sfx
1288  end
1289 end
1290
1292  local dispx=Plr.x-Game.scr
1293  if dispx>SX_MAX then
1294   Game.scr=Plr.x-SX_MAX
1295  elseif dispx<SX_MIN then
1296   Game.scr=Plr.x-SX_MIN
1297  end
1298 end
1299
1301  local rem=points%100
1302  if points>1000 or (rem~=50 and rem~=0) then
1303   return
1304  end
1305  local sp2=rem==50 and S.TINY_NUM_50 or
1306    S.TINY_NUM_00
1307  local sp1=points>=100 and
1308    (S.TINY_NUM_R1-1+points//100) or 0
1309  Ins(Toasts,{
1310     x=Iif(points>=100,x-8,x-12),
1311     y=y,ttl=40,sp1=sp1,sp2=sp2})
1312 end
1313
1314 -- tx,ty: position where to show toast
1315 -- (optional)
1317  local old=Plr.score
1318  Plr.score=Plr.score+points
1319  Plr.scoreMt=Game.t
1320  if (old//10000)<(Plr.score//10000) then
1321   Snd(SND.ONEUP)
1322   Plr.lives=Plr.lives+1
1323   -- TODO: vfx
1324  else
1325   Snd(SND.POINT)
1326  end
1327  if tx and ty then
1329  end
1330 end
1331
1332 function StartDying()
1333  SetMode(M.DYING)
1334  Snd(SND.DIE)
1335  Plr.dying=1 -- start die anim
1336  Plr.super=false
1337  Plr.firePwup=0
1338  Plr.plane=0
1339 end
1340
1342  local e={
1343   eid=newEid,
1344   x=newX,
1345   y=newY
1346  }
1347  Ins(Ents,e)
1348  EntInit(e)
1349  return e
1350 end
1351
1352 function EntInit(e)
1353  -- check if we have an animation for it
1354  if ANIM[e.eid] then
1355   e.anim=ANIM[e.eid]
1356   e.sprite=e.anim[1]
1357  else
1358   -- default to static sprite image
1359   e.sprite=e.eid
1360  end
1361  -- whether ent sprite is flipped
1362  e.flipped=false
1363  -- collider rect
1364  e.coll=CR.DFLT
1365  -- solidity (defaults to not solid)
1366  e.sol=SOL.NOT
1367  -- EBT entry
1368  local ebte=EBT[e.eid]
1369  -- behaviors
1370  e.beh=ebte and ebte.beh or {}
1371  -- copy initial behavior data to entity
1372  for _,b in pairs(e.beh) do
1373   ShallowMerge(e,b.data)
1374  end
1375  -- overlay the entity-defined data.
1376  if ebte and ebte.data then
1377   ShallowMerge(e,ebte.data)
1378  end
1379  -- call the entity init funcs
1380  for _,b in pairs(e.beh) do
1381   if b.init then b.init(e) end
1382  end
1383 end
1384
1385 function EntRepl(e,eid,data)
1388  if data then
1389   ShallowMerge(newE,data)
1390  end
1391 end
1392
1393 function EntHasBeh(e,soughtBeh)
1394  for _,b in pairs(e.beh) do
1395   if b==soughtBeh then return true end
1396  end
1397  return false
1398 end
1399
1401  if EntHasBeh(e,beh) then return end
1402  -- note: can't mutate the original
1403  -- e.beh because it's a shared ref.
1404  e.beh=DeepCopy(e.beh)
1405  ShallowMerge(e,beh.data,true)
1406  Ins(e.beh,beh)
1407 end
1408
1409 function UpdateEnts()
1410  -- iterate backwards so we can delete
1411  for i=#Ents,1,-1 do
1412   local e=Ents[i]
1413   UpdateEnt(e)
1414   if e.dead then
1415    -- delete
1416    Rem(Ents,i)
1417   end
1418  end
1419 end
1420
1421 function UpdateEnt(e)
1422  if not ALWAYS_UPDATED_EIDS[e.eid] and
1423    Abs(e.x-Plr.x)>ENT_MAX_DIST then
1424   -- too far, don't update
1425   return
1426  end
1427  -- update anim frame
1428  if e.anim then
1429   e.sprite=e.anim[1+(time()//128)%#e.anim]
1430  end
1431  -- run update behaviors
1432  for _,b in pairs(e.beh) do
1433   if b.update then b.update(e) end
1434  end
1435 end
1436
1437 function GetEntAt(x,y)
1438  for i=1,#Ents do
1439   local e=Ents[i]
1440   if e.x==x and e.y==y then return e end
1441  end
1442  return nil
1443 end
1444
1445 -- detect collisions
1446 function DetectColl()
1447  -- player rect
1448  local pr=RectXLate(GetPlrCr(),
1449   Plr.x,Plr.y)
1450
1451  -- attack rect
1452  local ar=nil
1453  if ATK_SEQ[Plr.atk]==2 then
1454   -- player is attacking, so check if
1455   -- entity was hit by attack
1456   ar=RectXLate(CR.ATK,Plr.x,Plr.y)
1457   if Plr.flipped then
1458    ar.x=Plr.x+CR.ATK_FLIP_X
1459   end
1460  end
1461
1462  for i=1,#Ents do
1463   local e=Ents[i]
1464   local er=RectXLate(e.coll,e.x,e.y)
1465   if RectIsct(pr,er) then
1466    -- collision between player and ent
1467    HandlePlrColl(e)
1468   elseif ar and RectIsct(ar,er) then
1469    -- ent hit by player attack
1470    HandleDamage(e,DMG.MELEE)
1471   end
1472  end
1473 end
1474
1475 function CheckEndLvl()
1476  local t=LvlTileAtPt(
1477    Plr.x+C//2,Plr.y+C//2)
1478  if t==T.GATE_L or t==T.GATE_R or
1479     t==T.GATE_L2 or t==T.GATE_R2 then
1480   EndLvl()
1481  end
1482 end
1483
1484 function EndLvl()
1485  Game.topLvl=Max(
1486    Game.topLvl,Game.lvlNo)
1487  SetMode(M.EOL)
1488 end
1489
1491  -- save game if we should
1492  if Game.lvl.save then
1493   pmem(0,Max(pmem(0) or 0,
1494     Game.lvlNo+1))
1495  end
1496  if Game.lvlNo>=#LVL then
1497   -- end of game.
1498   SetMode(M.WIN)
1499  else
1500   -- go back to map.
1501   SetMode(M.WLD)
1502  end
1503 end
1504
1505 -- handle collision w/ given ent
1506 function HandlePlrColl(e)
1507  for _,b in pairs(e.beh) do
1508   if b.coll then b.coll(e) end
1509   if e.dead then break end
1510  end
1511 end
1512
1513 function HandleDamage(e,dtype)
1514  for _,b in pairs(e.beh) do
1515   if b.dmg then b.dmg(e,dtype) end
1516   if e.dead then
1517    SpawnParts(PFX.POP,e.x+4,e.y+4,e.clr)
1518    Snd(SND.KILL)
1519    break
1520   end
1521  end
1522 end
1523
1524 function HandlePlrHurt()
1525  if Plr.invuln>0 then return end
1526  if Plr.plane==0 and Plr.super then
1527   Snd(SND.HURT)
1528   Plr.super=false
1529   Plr.invuln=100
1530   Plr.drag=Iif(Plr.dx>=0,-10,10)
1531   Plr.jmp=0
1532  else
1533   StartDying()
1534  end
1535 end
1536
1537 function Snd(spec)
1538  if not Sett.snd then return end
1539  sfx(spec.sfxid,spec.note,spec.dur,
1540    0,spec.vol or 15,spec.speed or 0)
1541 end
1542
1543 function PlayMus(musid)
1544  if Sett.mus or musid==-1 then
1545   music(musid)
1546  end
1547 end
1548
1549
1550 ---------------------------------------
1551 -- PARTICLES
1552 ---------------------------------------
1553
1554 -- possible effects
1555 PFX={
1556  POP={
1558   count=15,
1559   speed=4,
1560   fall=true,
1561   ttl=15
1562  },
1563  FW={ -- fireworks
1565   count=40,
1566   speed=1,
1567   fall=false,
1568   ttl=100
1569  }
1570 }
1571
1572 -- fx=one of the effects in PFX
1573 -- cx,cy=center, clr=the color
1574 function SpawnParts(fx,cx,cy,clr)
1575  for i=1,fx.count do
1577   local phi=Rnd01()*math.pi*2
1578   local part={
1579    x=cx+r*Cos(phi),
1580    y=cy+r*Sin(phi),
1581    vx=fx.speed*Cos(phi),
1582    vy=fx.speed*Sin(phi),
1583    fall=fx.fall,
1584    ttl=fx.ttl,
1585    age=0,
1586    clr=clr
1587   }
1588   Ins(Parts,part)
1589  end
1590 end
1591
1592 function UpdateParts()
1593  -- iterate backwards so we can delete
1594  for i=#Parts,1,-1 do
1595   local p=Parts[i]
1596   p.age=p.age+1
1597   if p.age>=p.ttl then
1598    -- delete
1599    Rem(Parts,i)
1600   else
1601    p.x=p.x+p.vx
1602    p.y=p.y+p.vy+(p.fall and p.age//2 or 0)
1603   end
1604  end
1605 end
1606
1607 function RendParts()
1608  for i,p in pairs(Parts) do
1609   pix(p.x-Game.scr,p.y,p.clr)
1610  end
1611 end
1612
1613 ---------------------------------------
1614 -- WLD MAP
1615 ---------------------------------------
1616 -- convert "World W-L" into index
1617 function Wl(w,l) return (w-1)*3+l end
1618
1619 -- Init world (runs once at start of app).
1620 function WldInit()
1621  for r=0,ROWS-1 do
1622   for c=0,COLS-1 do
1623    local t=WldFgTile(c,r)
1624    local lval=WldLvlVal(t)
1625    if t==T.META_A then
1626     -- player start pos
1627     Wld.plr.x0=c*C
1628     Wld.plr.y0=(r-1)*C
1629    elseif t==T.META_B then
1630     local mv=WldGetTag(c,r)
1631     -- savegame start pos
1632     Wld.spos[Wl(mv,1)]={
1633       x=c*C,y=(r-1)*C}
1634    elseif lval>0 then
1635     local mv=WldGetTag(c,r)
1636     -- It's a level tile.
1637     local poi={c=c,r=r,
1638      t=POI.LVL,lvl=Wl(mv,lval)}
1639     Ins(Wld.pois,poi)
1640    end
1641   end
1642  end
1643 end
1644
1645 -- Looks around tc,tr for a numeric tag.
1646 function WldGetTag(tc,tr)
1647  for r=tr-1,tr+1 do
1648   for c=tc-1,tc+1 do
1649    local mv=MetaVal(WldFgTile(c,r),0)
1650    if mv>0 then
1651     return mv
1652    end
1653   end
1654  end
1655  trace("No WLD tag @"..tc..","..tr)
1656  return 0
1657 end
1658
1659 -- Returns the value (1, 2, 3) of a WLD
1660 -- level tile.
1661 function WldLvlVal(t)
1662  return Iif4(t==S.WLD.LVLF,1,
1663    t==S.WLD.LVL1,1,
1664    t==S.WLD.LVL2,2,
1665    t==S.WLD.LVL3,3,0)
1666 end
1667
1668 function WldFgTile(c,r)
1669  return MapPageTile(WLD.FPAGE,c,r)
1670 end
1671
1672 function WldBgTile(c,r)
1673  return MapPageTile(WLD.BPAGE,c,r)
1674 end
1675
1676 function WldPoiAt(c,r)
1677  for i=1,#Wld.pois do
1678   local poi=Wld.pois[i]
1679   if poi.c==c and poi.r==r then
1680    return poi
1681   end
1682  end
1683  return nil
1684 end
1685
1687  local t=WldFgTile(c,r)
1688  for i=1,#S.WLD.ROADS do
1689   if S.WLD.ROADS[i]==t then
1690    return true
1691   end
1692  end
1693  return false
1694 end
1695
1696 function WldUpdate()
1697  local p=Wld.plr  -- shorthand
1698
1699  if p.dx~=0 or p.dy~=0 then
1700   -- Just move.
1701   p.x=p.x+p.dx
1702   p.y=p.y+p.dy
1703   if p.x%C==0 and p.y%C==0 then
1704    -- reached destination.
1705    p.ldx=p.dx
1706    p.ldy=p.dy
1707    p.dx=0
1708    p.dy=0
1709   end
1710   return
1711  end
1712
1713  if btn(0) then WldTryMove(0,-1) end
1714  if btn(1) then WldTryMove(0,1) end
1715  if btn(2) then WldTryMove(-1,0) end
1716  if btn(3) then WldTryMove(1,0) end
1717
1718  Wld.plr.flipped=Iif(
1719    Iif(Wld.plr.flipped,btn(3),btn(2)),
1720    not Wld.plr.flipped,
1721    Wld.plr.flipped) -- wtf
1722
1723  if btnp(4) then
1724   local poi=WldPoiAt(p.x//C,p.y//C)
1725   if poi and poi.lvl>Game.topLvl then
1726    if poi.lvl==1 then
1727     SetMode(M.TUT)
1728    else
1729     StartLvl(poi.lvl)
1730    end
1731   end
1732  end
1733 end
1734
1735 function WldTryMove(dx,dy)
1736  local p=Wld.plr  -- shorthand
1737
1738  -- if we are in locked POI, we can only
1739  -- come back the way we came.
1740  local poi=WldPoiAt(p.x//C,p.y//C)
1741  if not Plr.dbgFly and poi and
1742    poi.lvl>Game.topLvl and
1743    (dx ~= -p.ldx or dy ~= -p.ldy) then
1744   return
1745  end
1746
1747  -- target row,col
1748  local tc=p.x//C+dx
1749  local tr=p.y//C+dy
1750  if WldHasRoadAt(tc,tr) or
1751     WldPoiAt(tc,tr) then
1752   -- Destination is a road or level.
1753   -- Move is valid.
1754   p.dx=dx
1755   p.dy=dy
1756   return
1757  end
1758 end
1759
1760 function WldFgRemapFunc(t)
1761  return t<T.FIRST_META and t or 0
1762 end
1763
1764 function WldRend()
1765  if Game.m~=M.WLD then return end
1766  cls(2)
1767  rect(0,SCRH-8,SCRW,8,0)
1768  local fp=MapPageStart(WLD.FPAGE)
1769  local bp=MapPageStart(WLD.BPAGE)
1770  -- render map bg
1771  map(bp.c,bp.r,COLS,ROWS,0,0,0,1)
1772  -- render map fg, excluding markers
1773  map(fp.c,fp.r,COLS,ROWS,0,0,0,1,
1774    WldFgRemapFunc)
1775
1776  -- render the "off" version of level
1777  -- tiles on top of cleared levels.
1778  for _,poi in pairs(Wld.pois) do
1779   if poi.lvl<=Game.topLvl then
1780    spr(S.WLD.LVLC,poi.c*C,poi.r*C,0)
1781   end
1782  end
1783
1784  print("SELECT LEVEL TO PLAY",
1785    70,10)
1786  print("= MOVE",34,SCRH-6)
1787  print("= ENTER LEVEL",98,SCRH-6)
1788
1789  RendSpotFx(Wld.plr.x//C,
1790    Wld.plr.y//C,Game.t)
1791  if 0==(Game.t//16)%2 then
1792   rectb(Wld.plr.x-3,Wld.plr.y-3,13,13,15)
1793  end
1794  spr(S.PLR.STAND,Wld.plr.x,Wld.plr.y,0,
1795    1,Wld.plr.flipped and 1 or 0)
1796  RendHud()
1797 end
1798
1799 ---------------------------------------
1800 -- LEVEL UNPACKING
1801 ---------------------------------------
1802 -- unpack modes
1803 UMODE={
1804  GAME=0, -- unpack for gameplay
1805  EDIT=1, -- unpack for editing
1806 }
1807
1808 function MapPageStart(pageNo)
1809  return {c=(pageNo%8)*30,r=(pageNo//8)*17}
1810 end
1811
1812 function MapPageTile(pageNo,c,r,newVal)
1813  local pstart=MapPageStart(pageNo)
1814  if newVal then
1815   mset(c+pstart.c,r+pstart.r,newVal)
1816  end
1817  return mget(c+pstart.c,r+pstart.r)
1818 end
1819
1820 -- Unpacked level is written to top 8
1821 -- map pages (cells 0,0-239,16).
1822 function UnpackLvl(lvlNo,mode)
1823  local lvl=LVL[lvlNo]
1824  local start=MapPageStart(lvl.pkstart)
1825  local offc=start.c
1826  local offr=start.r
1827  local len=lvl.pklen*30
1828  local endc=FindLvlEndCol(offc,offr,len)
1829
1830  MapClear(0,0,LVL_LEN,ROWS)
1831
1832  -- next output col
1833  local outc=0
1834
1835  -- for each col in packed map
1836  for c=offc,endc do
1837   local cmd=mget(c,offr)
1838   local copies=MetaVal(cmd,1)
1839   -- create that many copies of this col
1840   for i=1,copies do
1841    CreateCol(c,outc,offr,mode==UMODE.GAME)
1842    -- advance output col
1843    outc=outc+1
1844    if outc>=LVL_LEN then
1845     trace("ERROR: level too long: "..lvlNo)
1846     return
1847    end
1848   end
1849  end
1850
1851  -- if in gameplay, expand patterns and
1852  -- remove special markers
1853  -- (first META_A is player start pos)
1854  if mode==UMODE.GAME then
1855   for c=0,LVL_LEN-1 do
1856    for r=0,ROWS-1 do
1857     local t=mget(c,r)
1858     local tpat=TPAT[t]
1859     if tpat then ExpandTpat(tpat,c,r) end
1860     if Plr.x==0 and Plr.y==0 and
1861       t==T.META_A then
1862      -- player start position.
1863      Plr.x=c*C
1864      Plr.y=r*C
1865     end
1866     if t>=T.FIRST_META then
1867      mset(c,r,0)
1868     end
1869    end
1870   end
1871   if Plr.x==0 and Plr.y==0 then
1872    trace("*** start pos UNSET L"..lvlNo)
1873   end
1874   FillWater()
1875   SetUpTanims()
1876  end
1877 end
1878
1879 -- expand tile pattern at c,r
1880 function ExpandTpat(tpat,c,r)
1881  local s=mget(c,r)
1882  for i=0,tpat.w-1 do
1883   for j=0,tpat.h-1 do
1884    mset(c+i,r+j,s+j*16+i)
1885   end
1886  end
1887 end
1888
1889 -- Sets up tile animations.
1890 function SetUpTanims()
1891  for c=0,LVL_LEN-1 do
1892   for r=0,ROWS-1 do
1893    local t=mget(c,r)
1894    if TANIM[t] then
1896    end
1897   end
1898  end
1899 end
1900
1901 function FindLvlEndCol(c0,r0,len)
1902  -- iterate backwards until we find a
1903  -- non-empty col.
1904  for c=c0+len-1,c0,-1 do
1905   for r=r0,r0+ROWS-1 do
1906    if mget(c,r)>0 then
1907     -- rightmost non empty col
1908     return c
1909    end
1910   end
1911  end
1912  return c0
1913 end
1914
1915 function FillWater()
1916  -- We fill downward from surface tiles,
1917  -- Downward AND upward from water tiles.
1918  local surfs={}  -- surface tiles
1919  local waters={} -- water tiles
1920  for c=LVL_LEN-1,0,-1 do
1921   for r=ROWS-1,0,-1 do
1922    if mget(c,r)==T.SURF then
1923     Ins(surfs,{c=c,r=r})
1924    elseif mget(c,r)==T.WATER then
1925     Ins(waters,{c=c,r=r})
1926    end
1927   end
1928  end
1929
1930  for i=1,#surfs do
1931   local s=surfs[i]
1932   -- fill water below this tile
1933   FillWaterAt(s.c,s.r,1)
1934  end
1935  for i=1,#waters do
1936   local s=waters[i]
1937   -- fill water above AND below this tile
1938   FillWaterAt(s.c,s.r,-1)
1939   FillWaterAt(s.c,s.r,1)
1940  end
1941 end
1942
1943 -- Fill water starting (but not including)
1944 -- given tile, in the given direction
1945 -- (1:down, -1:up)
1946 function FillWaterAt(c,r0,dir)
1947  local from=r0+dir
1948  local to=Iif(dir>0,ROWS-1,0)
1949  for r=from,to,dir do
1950   if mget(c,r)==T.EMPTY then
1951    mset(c,r,T.WATER)
1952   else
1953    return
1954   end
1955  end
1956 end
1957
1959  if Tanims[c] then
1960   Ins(Tanims[c],r)
1961  else
1962   Tanims[c]={r}
1963  end
1964 end
1965
1966 -- pack lvl from 0,0-239,16 to the packed
1967 -- level area of the indicated level
1968 function PackLvl(lvlNo)
1969  local lvl=LVL[lvlNo]
1970  local start=MapPageStart(lvl.pkstart)
1971  local outc=start.c
1972  local outr=start.r
1973  local len=lvl.pklen*30
1974
1975  local endc=FindLvlEndCol(0,0,LVL_LEN)
1976
1977  -- pack
1978  local reps=0
1979  MapClear(outc,outr,len,ROWS)
1980  for c=0,endc do
1981   if c>0 and MapColsEqual(c,c-1,0) and
1982     reps<12 then
1983    -- increment repeat marker on prev col
1984    local m=mget(outc-1,outr)
1985    m=Iif(m==0,T.META_NUM_0+2,m+1)
1986    mset(outc-1,outr,m)
1987    reps=reps+1
1988   else
1989    reps=1
1990    -- copy col to packed level
1991    MapCopy(c,0,outc,outr,1,ROWS)
1992    outc=outc+1
1993    if outc>=start.c+len then
1994     trace("Capacity exceeded.")
1995     return false
1996    end
1997   end
1998  end
1999  trace("packed "..(endc+1).." -> "..
2000   (outc+1-start.c))
2001  return true
2002 end
2003
2004 -- Create map col (dstc,0)-(dstc,ROWS-1)
2005 -- from source col located at
2006 -- (srcc,offr)-(srcc,offr+ROWS-1).
2007 -- if ie, instantiates entities.
2008 function CreateCol(srcc,dstc,offr,ie)
2009  -- copy entire column first
2010  MapCopy(srcc,offr,dstc,0,1,ROWS)
2011  mset(dstc,0,T.EMPTY) -- top cell is empty
2012  if not ie then return end
2013  -- instantiate entities
2014  for r=1,ROWS-1 do
2015   local t=mget(dstc,r)
2016   if t>=T.FIRST_ENT and EBT[t] then
2017    -- entity tile: create entity
2018    mset(dstc,r,T.EMPTY)
2020   end
2021  end
2022 end
2023
2024 ---------------------------------------
2025 -- RENDERING
2026 ---------------------------------------
2027
2028 function Rend()
2029  RendBg()
2030  if Game.snow then RendSnow() end
2031  RendMap()
2032  RendTanims()
2033  RendEnts()
2034  RendToasts()
2035  if Game.m==M.EOL then RendScrim() end
2036  RendPlr()
2037  RendParts()
2038  RendHud()
2039  RendSign()
2040 end
2041
2042 function RendBg()
2043  local END_R=ROWS
2044  cls(Game.lvl.bg)
2045  local offset=Game.scr//2+50
2046  -- If i is a col# of mountains (starting
2047  -- at index 1), then its screen pos
2048  -- sx=(i-1)*C-off
2049  -- Solving for i, i=1+(sx+off)/C
2050  -- so at the left of screen, sx=0, we
2051  -- have i=1+off/C
2052  local startI=Max(1,1+offset//C)
2053  local endI=Min(
2054    startI+COLS,#Game.bgmnt)
2055  for i=startI,endI do
2056   local sx=(i-1)*C-offset
2057   local part=Game.bgmnt[i]
2058   for r=part.y,END_R do
2059    local spid=Iif(r==part.y,
2060     S.BGMNT.DIAG,S.BGMNT.FULL)
2061    spr(spid,(i-1)*C-offset,r*C,0,1,
2062     Iif(part.dy>0,1,0))
2063   end
2064  end
2065 end
2066
2067 function RendSnow()
2068  local dx=-Game.scr
2069  local dy=Game.t//2
2070  for _,p in pairs(Game.snow) do
2071   local sx=((p.x+dx)%SCRW+SCRW)%SCRW
2072   local sy=((p.y+dy)%SCRH+SCRH)%SCRH
2073   pix(sx,sy,Game.lvl.snow.clr)
2074  end
2075 end
2076
2077 function RendToasts()
2078  for i=#Toasts,1,-1 do
2079   local t=Toasts[i]
2080   t.ttl=t.ttl-1
2081   if t.ttl<=0 then
2082    Toasts[i]=Toasts[#Toasts]
2083    Rem(Toasts)
2084   else
2085    t.y=t.y-1
2086    spr(t.sp1,t.x-Game.scr,t.y,0)
2087    spr(t.sp2,t.x-Game.scr+C,t.y,0)
2088   end
2089  end
2090 end
2091
2092 function RendMap()
2093  -- col c is rendered at
2094  --   sx=-Game.scr+c*C
2095  -- Setting sx=0 and solving for c
2096  --   c=Game.scr//C
2097  local c=Game.scr//C
2098  local sx=-Game.scr+c*C
2099  local w=Min(COLS+1,LVL_LEN-c)
2100  if c<0 then
2101   sx=sx+C*(-c)
2102   c=0
2103  end
2104  map(
2105   -- col,row,w,h
2106   c,0,w,ROWS,
2107   -- sx,sy,colorkey,scale
2108   sx,0,0,1)
2109 end
2110
2111 function RendPlr()
2112  local spid
2113  local walking=false
2114
2115  if Plr.plane>0 then
2116   RendPlane()
2117   return
2118  end
2119
2120  if Plr.dying>0 then
2121   spid=S.PLR.DIE
2122  elseif Plr.atk>0 then
2123   spid=
2124     ATK_SEQ[Plr.atk]==1 and S.PLR.SWING
2125     or S.PLR.HIT
2126  elseif Plr.grounded then
2127   if btn(2) or btn(3) then
2128    spid=S.PLR.WALK1+time()%2
2129    walking=true
2130   else
2131    spid=S.PLR.STAND
2132   end
2133  elseif Plr.swim then
2134   spid=S.PLR.SWIM1+(Game.t//4)%2
2135  else
2136   spid=S.PLR.JUMP
2137  end
2138
2139  local sx=Plr.x-Game.scr
2140  local sy=Plr.y
2141  local flip=Plr.flipped and 1 or 0
2142
2143  -- apply dying animation
2144  if spid==S.PLR.DIE then
2145   if Plr.dying<=#DIE_SEQ then
2146    sx=sx+DIE_SEQ[Plr.dying][1]
2147    sy=sy+DIE_SEQ[Plr.dying][2]
2148   else
2149    sx=-1000
2150    sy=-1000
2151   end
2152  end
2153
2154  -- if invulnerable, blink
2155  if Plr.invuln>0 and
2156    0==(Game.t//4)%2 then return end
2157
2158  spr(spid,sx,sy,0,1,flip)
2159
2160  -- extra sprite for attack states
2161  if spid==S.PLR.SWING then
2162   spr(S.PLR.SWING_C,sx,sy-C,0,1,flip)
2163  elseif spid==S.PLR.HIT then
2164   spr(S.PLR.HIT_C,
2165     sx+(Plr.flipped and -C or C),
2166     sy,0,1,flip)
2167  end
2168
2169  -- draw super panda overlay if player
2170  -- has the super panda powerup
2171  if Plr.super then
2172   local osp=Iif3(Plr.atk>0,S.PLR.SUPER_F,
2173    Plr.swim and not Plr.grounded,
2174    S.PLR.SUPER_S,
2175    walking,S.PLR.SUPER_P,S.PLR.SUPER_F)
2176   spr(osp,sx,Plr.y,0,1,flip)
2177  end
2178
2179  -- draw overlays (blinking bamboo and
2180  -- yellow body) if powerup
2181  if spid~=S.PLR.SWING and Plr.firePwup>0
2182    and (time()//128)%2==0 then
2183   spr(S.PLR.FIRE_BAMBOO,sx,Plr.y,0,1,flip)
2184  end
2185  if Plr.firePwup>100 or
2186     1==(Plr.firePwup//16)%2 then
2187   local osp=Iif3(Plr.atk>0,S.PLR.FIRE_F,
2188    Plr.swim and not Plr.grounded,
2189    S.PLR.FIRE_S,
2190    walking,S.PLR.FIRE_P,S.PLR.FIRE_F)
2191   spr(osp,sx,Plr.y,0,1,flip)
2192  end
2193
2194  -- if just respawned, highlight player
2195  if Game.m==M.PLAY and Plr.dying==0 and
2196    Plr.respX>=0 and Game.t<100 and
2197    (Game.t//8)%2==0 then
2198   rectb(Plr.x-Game.scr-2,Plr.y-2,
2199     C+4,C+4,15)
2200  end
2201 end
2202
2203 function RendPlane()
2204  local ybias=(Game.t//8)%2==0 and 1 or 0
2205
2206  local sx=Plr.x-Game.scr
2207  spr(S.AVIATOR,
2208    sx-C,Plr.y+ybias,0,1,0,0,3,2)
2209  local spid=(Game.t//4)%2==0 and
2210    S.AVIATOR_PROP_1 or S.AVIATOR_PROP_2
2211  spr(spid,sx+C,
2212    Plr.y+ybias+4,0)
2213 end
2214
2215 function RendHud()
2216  rect(0,0,SCRW,C,3)
2217
2218  if Plr.scoreDisp.value~=Plr.score then
2219   Plr.scoreDisp.value=Plr.score
2221  end
2222
2223  local clr=15
2224
2225  print(Plr.scoreDisp.text,192,1,clr,true)
2226  print((Game.m==M.WLD and
2227    "WORLD MAP" or
2228    ("LEVEL "..Game.lvl.name)),95,1,7)
2229  spr(S.PLR.STAND,5,0,0)
2230  print("x "..Plr.lives,16,1)
2231
2232  if Plr.plane>0 then
2233   local barw=PLANE.FUEL_BAR_W
2234   local lx=120-barw//2
2235   local y=8
2236   local clr=(Plr.plane<800 and
2237     (Game.t//16)%2==0) and 6 or 14
2238   local clrLo=(clr==14 and 4 or clr)
2239   print("E",lx-7,y,clr)
2240   print("F",lx+barw+1,y,14)
2241   rectb(lx,y,barw,6,clrLo)
2242   local bw=Plr.plane*
2243     (PLANE.FUEL_BAR_W-2)//PLANE.MAX_FUEL
2244   rect(lx+1,y+1,Max(bw,1),4,clr)
2245   pix(lx+barw//4,y+4,clrLo)
2246   pix(lx+barw//2,y+4,clrLo)
2247   pix(lx+barw//2,y+3,clrLo)
2248   pix(lx+3*barw//4,y+4,clrLo)
2249  end
2250 end
2251
2252 function RendEnts()
2253  for i=1,#Ents do
2254   local e=Ents[i]
2255   local sx=e.x-Game.scr
2256   if sx>-C and sx<SCRW then
2257    spr(e.sprite,sx,e.y,0,1,
2258      e.flipped and 1 or 0)
2259   end
2260  end
2261 end
2262
2263 function RendScrim(sp)
2264  sp=sp or Iif3(Game.t>45,0,
2265   Game.t>30,S.SCRIM+2,
2266   Game.t>15,S.SCRIM+1,
2267   S.SCRIM)
2268  for r=0,ROWS-1 do
2269   for c=0,COLS-1 do
2270    spr(sp,c*C,r*C,15)
2271   end
2272  end
2273 end
2274
2275 -- Render spotlight effect.
2276 -- fc,fr: cell at center of effect
2277 -- t: clock (ticks)
2278 function RendSpotFx(fc,fr,t)
2280  if rad>COLS then return end
2281  for r=0,ROWS-1 do
2282   for c=0,COLS-1 do
2283    local d=Max(Abs(fc-c),
2284      Abs(fr-r))
2285    local sa=d-rad  -- scrim amount
2286    local spid=Iif2(sa<=0,-1,sa<=3,
2287      S.SCRIM+sa-1,0)
2288    if spid>=0 then
2289     spr(spid,c*C,r*C,15)
2290    end
2291   end
2292  end
2293 end
2294
2295 function RendSign()
2296  if 0==Plr.signC then return end
2297  local w=Plr.signC*20
2298  local h=Plr.signC*3
2299  local x=SCRW//2-w//2
2300  local y=SCRH//2-h//2-20
2301  local s=SIGN_MSGS[Plr.signMsg]
2302  rect(x,y,w,h,15)
2303  if Plr.signC==SIGN_C_MAX then
2304   print(s.l1,x+6,y+8,0)
2305   print(s.l2,x+6,y+8+C,0)
2306  end
2307 end
2308
2309 -- Rend tile animations
2310 function RendTanims()
2311  local c0=Max(0,Game.scr//C)
2312  local cf=c0+COLS
2313  for c=c0,cf do
2314   local anims=Tanims[c]
2315   if anims then
2316    for i=1,#anims do
2317     local r=anims[i]
2318     local tanim=TANIM[mget(c,r)]
2319     if tanim then
2320      local spid=tanim[
2321        1+(Game.t//16)%#tanim]
2322      spr(spid,c*C-Game.scr,r*C)
2323     end
2324    end
2325   end
2326  end
2327 end
2328
2329 --------------------------------------
2330 -- ENTITY BEHAVIORS
2331 ---------------------------------------
2332
2333 -- move hit modes: what happens when
2334 -- entity hits something solid.
2335 MOVE_HIT={
2336  NONE=0,
2337  STOP=1,
2338  BOUNCE=2,
2339  DIE=3,
2340 }
2341
2342 -- aim mode
2343 AIM={
2344  NONE=0,   -- just shoot in natural
2345            -- direction of projectile
2346  HORIZ=1,  -- adjust horizontal vel to
2347            -- go towards player
2348  VERT=2,   -- adjust vertical vel to go
2349            -- towards player
2350  FULL=3,   -- adjust horiz/vert to aim
2351            -- at player
2352 }
2353
2354 -- moves horizontally
2355 -- moveDen: every how many ticks to move
2356 -- moveDx: how much to move
2357 -- moveHitMode: what to do on wall hit
2358 -- noFall: if true, flip instead of falling
2359 function BehMove(e)
2360  if e.moveT>0 then e.moveT=e.moveT-1 end
2361  if e.moveT==0 then return end
2362  if e.moveWaitPlr>0 then
2363   if Abs(Plr.x-e.x)>e.moveWaitPlr then
2364    return
2365   else e.moveWaitPlr=0 end
2366  end
2367  e.moveNum=e.moveNum+1
2368  if e.moveNum<e.moveDen then return end
2369  e.moveNum=0
2370
2371  if e.noFall and
2372    EntWouldFall(e,e.x+e.moveDx) then
2373   -- flip rather than fall
2374   e.moveDx=-e.moveDx
2375   e.flipped=e.moveDx>0
2376  elseif e.moveHitMode==MOVE_HIT.NONE or
2377     EntCanMove(e,e.x+e.moveDx,e.y) then
2378   e.x=e.x+(e.moveDx or 0)
2379   e.y=e.y+(e.moveDy or 0)
2380  elseif e.moveHitMode==MOVE_HIT.BOUNCE
2381    then
2382   e.moveDx=-(e.moveDx or 0)
2383   e.flipped=e.moveDx>0
2384  elseif e.moveHitMode==MOVE_HIT.DIE then
2386  end
2387 end
2388
2389 -- Moves up/down.
2390 -- e.yamp: amplitude
2391 function BehUpDownInit(e)
2392  e.maxy=e.y
2393  e.miny=e.maxy-e.yamp
2394 end
2395
2396 function BehUpDown(e)
2397  e.ynum=e.ynum+1
2398  if e.ynum<e.yden then return end
2399  e.ynum=0
2400  e.y=e.y+e.dy
2401  if e.y<=e.miny then e.dy=1 end
2402  if e.y>=e.maxy then e.dy=-1 end
2403 end
2404
2405 function BehFacePlr(e)
2406  e.flipped=Plr.x>e.x
2407  if e.moveDx then
2408   e.moveDx=Abs(e.moveDx)*
2409    (e.flipped and 1 or -1)
2410  end
2411 end
2412
2413 -- automatically flips movement
2414 -- flipDen: every how many ticks to flip
2415 function BehFlip(e)
2416  e.flipNum=e.flipNum+1
2417  if e.flipNum<e.flipDen then return end
2418  e.flipNum=0
2419  e.flipped=not e.flipped
2420  e.moveDx=(e.moveDx and -e.moveDx or 0)
2421 end
2422
2423 function BehJump(e)
2424  if e.jmp==0 then
2425   e.jmpNum=e.jmpNum+1
2426   if e.jmpNum<e.jmpDen or
2427     not e.grounded then return end
2428   e.jmpNum=0
2429   e.jmp=1
2430  else
2431   -- continue jump
2432   e.jmp=e.jmp+1
2433   if e.jmp>#JUMP_DY then
2434    -- end jump
2435    e.jmp=0
2436   else
2437    local dy=JUMP_DY[e.jmp]
2438    if EntCanMove(e,e.x,e.y+dy) then
2439     e.y=e.y+dy
2440    else
2441     e.jmp=0
2442    end
2443   end
2444  end
2445 end
2446
2447 function BehFall(e)
2448  e.grounded=not EntCanMove(e,e.x,e.y+1)
2449  if not e.grounded and e.jmp==0 then
2450   e.y=e.y+1
2451  end
2452 end
2453
2454 function BehTakeDmg(e,dtype)
2455  if not ArrayContains(e.dtypes,dtype) then
2456   return
2457  end
2458  e.hp=e.hp-1
2459  if e.hp>0 then return end
2461  -- drop loot?
2462  local roll=Rnd(0,99)
2463  -- give bonus probability to starting
2464  -- levels (decrease roll value)
2465  roll=Max(Iif2(Game.lvlNo<2,roll-50,
2466    Game.lvlNo<4,roll-25,roll),0)
2467  if roll<e.lootp then
2468   local i=Rnd(1,#e.loot)
2469   i=Min(Max(i,1),#e.loot)
2472   ShallowMerge(l,{moveDy=-1,moveDx=0,
2473     moveDen=1,moveT=8})
2474  end
2475 end
2476
2477 function BehPoints(e)
2479  AddScore(e.value or 50,e.x+4,e.y-4)
2480 end
2481
2482 function BehHurt(e)
2483  HandlePlrHurt()
2484 end
2485
2486 function BehLiftInit(e)
2487  -- lift top and bottom y:
2488  local a=C*FetchTile(
2489    T.META_A,e.x//C)
2490  local b=C*FetchTile(
2491    T.META_B,e.x//C)
2492  if a>b then
2493   e.boty=a
2494   e.topy=b
2495   e.dir=1
2496  else
2497   e.topy=a
2498   e.boty=b
2499   e.dir=-1
2500  end
2501  e.coll=CR.FULL
2502 end
2503
2504 function BehLift(e)
2505  e.liftNum=e.liftNum+1
2506  if e.liftNum<e.liftDen then return end
2507  e.liftNum=0
2508  e.y=e.y+e.dir
2509  if e.dir>0 and e.y>e.boty or
2510    e.dir<0 and e.y<e.topy then
2511   e.dir=-e.dir
2512  end
2513 end
2514
2515 function BehLiftColl(e)
2516  -- Lift hit player. Just nudge the player
2517  Plr.nudgeY=Iif(e.y>Plr.y,-1,1)
2518 end
2519
2520 function BehShootInit(e)
2521  e.shootNum=Rnd(0,e.shootDen-1)
2522 end
2523
2524 function BehShoot(e)
2525  e.shootNum=e.shootNum+1
2526  if e.shootNum<30 then
2527   e.sprite=e.shootSpr or e.sprite
2528  end
2529  if e.shootNum<e.shootDen then return end
2530  e.shootNum=0
2532    e.shootEid or EID.FIREBALL,e.x,e.y)
2533  e.sprite=e.shootSpr or e.sprite
2534  shot.moveDx=
2535    Iif(shot.moveDx==nil,0,shot.moveDx)
2536  shot.moveDy=
2537    Iif(shot.moveDy==nil,0,shot.moveDy)
2538
2539  if e.aim==AIM.HORIZ then
2540   shot.moveDx=(Plr.x>e.x and 1 or -1)*
2541    Abs(shot.moveDx)
2542  elseif e.aim==AIM.VERT then
2543   shot.moveDy=(Plr.y>e.y and 1 or -1)*
2544    Abs(shot.moveDy)
2545  elseif e.aim==AIM.FULL then
2546   local tx=Plr.x-shot.x
2547   local ty=Plr.y-shot.y
2548   local mag=math.sqrt(tx*tx+ty*ty)
2549   local spd=math.sqrt(
2550     shot.moveDx*shot.moveDx+
2551     shot.moveDy*shot.moveDy)
2552   shot.moveDx=math.floor(0.5+tx*spd/mag)
2553   shot.moveDy=math.floor(0.5+ty*spd/mag)
2554   if shot.moveDx==0 and shot.moveDy==0 then
2555    shot.moveDx=-1
2556   end
2557  end
2558 end
2559
2560 function BehCrumble(e)
2561  if not e.crumbling then
2562   -- check if player on tile
2563   if Plr.x<e.x-8 then return end
2564   if Plr.x>e.x+8 then return end
2565   -- check if player is standing on it
2566   local pr=RectXLate(
2567     GetPlrCr(),Plr.x,Plr.y)
2568   local er=RectXLate(e.coll,e.x,e.y-1)
2569   e.crumbling=RectIsct(pr,er)
2570  end
2571
2572  if e.crumbling then
2573   -- count down to destruction
2574   e.cd=e.cd-1
2575   e.sprite=Iif(e.cd>66,S.CRUMBLE,
2576     Iif(e.cd>33,S.CRUMBLE_2,S.CRUMBLE_3))
2577   if e.cd<0 then e.dead=true end
2578  end
2579 end
2580
2581 function BehTtl(e)
2582  e.ttl=e.ttl-1
2583  if e.ttl <= 0 then e.dead = true end
2584 end
2585
2586 function BehDmgEnemy(e)
2587  local fr=RectXLate(FIRE.COLL,e.x,e.y)
2588  for i=1,#Ents do
2589   local ent=Ents[i]
2590   local er=RectXLate(ent.coll,ent.x,ent.y)
2591   if e~=ent and RectIsct(fr,er) and
2592     EntHasBeh(ent,BE.VULN) then
2593    -- ent hit by player fire
2594    HandleDamage(ent,Plr.plane>0 and
2595      DMG.PLANE_FIRE or DMG.FIRE)
2597   end
2598  end
2599 end
2600
2601 function BehGrantFirePwupColl(e)
2602  Plr.firePwup=FIRE.DUR
2604  Snd(SND.PWUP)
2605 end
2606
2607 function BehGrantSuperPwupColl(e)
2608  Plr.super=true
2610  Snd(SND.PWUP)
2611 end
2612
2613 function BehReplace(e)
2614  local d=Abs(e.x-Plr.x)
2615  if d<e.replDist then
2616   EntRepl(e,e.replEid,e.replData)
2617  end
2618 end
2619
2620 function BehChestInit(e)
2621  -- ent on top of chest is the contents
2622  local etop=GetEntAt(e.x,e.y-C)
2623  if etop then
2624   e.cont=etop.eid
2626  else
2627   e.cont=S.FOOD.LEAF
2628  end
2629  -- check multiplier
2630  e.mul=MetaVal(
2631   mget(e.x//C,e.y//C-2),1)
2632  e.open=false
2633 end
2634
2635 function BehChestDmg(e)
2636  if e.open then return end
2637  SpawnParts(PFX.POP,e.x+4,e.y+4,14)
2638  Snd(SND.OPEN);
2639  e.anim=nil
2640  e.sprite=S.CHEST_OPEN
2641  e.open=true
2642  local by=e.y-C
2643  local ty=e.y-2*C
2644  local lx=e.x-C
2645  local cx=e.x
2646  local rx=e.x+C
2647  local c=e.cont
2649  if e.mul>1 then EntAdd(c,cx,ty) end
2650  if e.mul>2 then EntAdd(c,lx,by) end
2651  if e.mul>3 then EntAdd(c,rx,by) end
2652  if e.mul>4 then EntAdd(c,lx,ty) end
2653  if e.mul>5 then EntAdd(c,rx,ty) end
2654 end
2655
2656 function BehTplafInit(e)
2657  e.phase=MetaVal(FetchEntTag(e),0)
2658 end
2659
2660 function BehTplaf(e)
2661  local UNIT=40     -- in ticks
2662  local PHASE_LEN=3 -- in units
2663  local uclk=e.phase+Game.t//UNIT
2664  local open=((uclk//PHASE_LEN)%2==0)
2665  local tclk=e.phase*UNIT+Game.t
2666  e.sprite=Iif2(
2667   (tclk%(UNIT*PHASE_LEN)<=6),
2668   S.TPLAF_HALF,open,S.TPLAF,S.TPLAF_OFF)
2669  e.sol=Iif(open,SOL.HALF,SOL.NOT)
2670 end
2671
2672 function BehDashInit(e)
2673  assert(EntHasBeh(e,BE.MOVE))
2674  e.origAnim=e.anim
2675  e.origMoveDen=e.moveDen
2676 end
2677
2678 function BehDash(e)
2679  local dashing=e.cdd<e.ddur
2680  e.cdd=(e.cdd+1)%e.cdur
2681  if dashing then
2682   e.anim=e.dashAnim or e.origAnim
2683   e.moveDen=e.origMoveDen
2684  else
2685   e.anim=e.origAnim
2686   e.moveDen=99999  -- don't move
2687  end
2688 end
2689
2690 function BehSignInit(e)
2691  e.msg=MetaVal(FetchEntTag(e),0)
2692 end
2693
2694 function BehSignColl(e)
2695  Plr.signMsg=e.msg
2696  -- if starting to read sign, lock player
2697  -- for a short while
2698  if Plr.signC==0 then
2699   Plr.locked=100
2700  end
2701  -- increase cycle counter by 2 because
2702  -- it gets decreased by 1 every frame
2703  Plr.signC=Min(Plr.signC+2,
2704    SIGN_C_MAX)
2705 end
2706
2707 function BehOneUp(e)
2709  Plr.lives=Plr.lives+1
2710  Snd(SND.ONEUP)
2711 end
2712
2713 function BehFlag(e)
2714  local rx=e.x+C
2715  if Plr.respX<rx then
2716   Plr.respX=rx
2717   Plr.respY=e.y
2718  end
2719  Snd(SND.PWUP)
2720  EntRepl(e,EID.FLAG_T)
2721 end
2722
2723 function BehReplOnGnd(e)
2724  if e.grounded then
2725   EntRepl(e,e.replEid,e.replData)
2726  end
2727 end
2728
2729 function BehPop(e)
2731  SpawnParts(PFX.POP,e.x+4,e.y+4,e.clr)
2732 end
2733
2734 function BehBoardPlane(e)
2736  Plr.plane=PLANE.START_FUEL
2737  Plr.y=e.y-3*C
2738  Snd(SND.PLANE)
2739 end
2740
2741 function BehFuel(e)
2743  Plr.plane=Plr.plane+PLANE.FUEL_INC
2744  Snd(SND.PWUP)
2745 end
2746
2747 ---------------------------------------
2748 -- ENTITY BEHAVIORS
2749 ---------------------------------------
2750 BE={
2751  MOVE={
2752   data={
2753    -- move denominator (moves every
2754    -- this many frames)
2755    moveDen=5,
2756    moveNum=0, -- numerator, counts up
2757    -- 1=moving right, -1=moving left
2758    moveDx=-1,
2759    moveDy=0,
2760    moveHitMode=MOVE_HIT.BOUNCE,
2761    -- if >0, waits until player is less
2762    -- than this dist away to start motion
2763    moveWaitPlr=0,
2764    -- if >=0, how many ticks to move
2765    -- for (after that, stop).
2766    moveT=-1,
2767   },
2768   update=BehMove,
2769  },
2770  FALL={
2771   data={grounded=false,jmp=0},
2772   update=BehFall,
2773  },
2774  FLIP={
2775   data={flipNum=0,flipDen=20},
2776   update=BehFlip,
2777  },
2778  FACEPLR={update=BehFacePlr},
2779  JUMP={
2780   data={jmp=0,jmpNum=0,jmpDen=50},
2781   update=BehJump,
2782  },
2783  VULN={ -- can be damaged by player
2784   data={hp=1,
2785    -- damage types that can hurt this.
2786    dtypes={DMG.MELEE,DMG.FIRE,DMG.PLANE_FIRE},
2787    -- loot drop probability (0-100)
2788    lootp=0,
2789    -- possible loot to drop (EIDs)
2790    loot={EID.FOOD.A},
2791   },
2792   dmg=BehTakeDmg,
2793  },
2794  SHOOT={
2795   data={shootNum=0,shootDen=100,
2796    aim=AIM.NONE},
2797   init=BehShootInit,
2798   update=BehShoot,
2799  },
2800  UPDOWN={
2801   -- yamp is amplitude of y movement
2802   data={yamp=16,dy=-1,yden=3,ynum=0},
2803   init=BehUpDownInit,
2804   update=BehUpDown,
2805  },
2806  POINTS={
2807   data={value=50},
2808   coll=BehPoints,
2809  },
2810  HURT={ -- can hurt player
2811   coll=BehHurt
2812  },
2813  LIFT={
2814   data={liftNum=0,liftDen=3},
2815   init=BehLiftInit,
2816   update=BehLift,
2817   coll=BehLiftColl,
2818  },
2819  CRUMBLE={
2820   -- cd: countdown to crumble
2821   data={cd=50,coll=CR.FULL,crumbling=false},
2822   update=BehCrumble,
2823  },
2824  TTL={  -- time to live (auto destroy)
2825   data={ttl=150},
2826   update=BehTtl,
2827  },
2828  DMG_ENEMY={ -- damage enemies
2829   update=BehDmgEnemy,
2830  },
2831  GRANT_FIRE={
2832   coll=BehGrantFirePwupColl,
2833  },
2834  REPLACE={
2835  -- replaces by another ent when plr near
2836  -- replDist: distance from player
2837  -- replEid: EID to replace by
2838   data={replDist=50,replEid=EID.LEAF},
2839   update=BehReplace,
2840  },
2841  CHEST={
2842   init=BehChestInit,
2843   dmg=BehChestDmg,
2844  },
2845  TPLAF={
2846   init=BehTplafInit,
2847   update=BehTplaf,
2848  },
2849  DASH={
2850   data={
2851    ddur=20, -- dash duration
2852    cdur=60, -- full cycle duration
2853    cdd=0, -- cycle counter
2854   },
2855   init=BehDashInit,
2856   update=BehDash,
2857  },
2858  GRANT_SUPER={
2859   coll=BehGrantSuperPwupColl,
2860  },
2861  SIGN={
2862   init=BehSignInit,
2863   coll=BehSignColl
2864  },
2865  ONEUP={coll=BehOneUp},
2866  FLAG={coll=BehFlag},
2867  REPL_ON_GND={
2868   -- replace EID when grounded
2869   -- replData -- extra data to add to
2870   data={replEid=EID.LEAF},
2871   update=BehReplOnGnd
2872  },
2873  POP={update=BehPop},
2874  PLANE={coll=BehBoardPlane},
2875  FUEL={coll=BehFuel},
2876 }
2877
2878 ---------------------------------------
2879 -- ENTITY BEHAVIOR TABLE
2880 ---------------------------------------
2881 EBT={
2882  [EID.EN.SLIME]={
2883   data={
2884     hp=1,moveDen=3,clr=11,noFall=true,
2885     lootp=20,loot={EID.FOOD.A},
2886   },
2887   beh={BE.MOVE,BE.FALL,BE.VULN,BE.HURT},
2888  },
2889
2890  [EID.EN.HSLIME]={
2891   data={replDist=50,replEid=EID.EN.SLIME},
2892   beh={BE.REPLACE},
2893  },
2894
2895  [EID.EN.A]={
2896   data={
2897     hp=1,moveDen=5,clr=14,flipDen=120,
2898     lootp=30,
2899     loot={EID.FOOD.A,EID.FOOD.B},
2900   },
2901   beh={BE.MOVE,BE.JUMP,BE.FALL,BE.VULN,
2902    BE.HURT,BE.FLIP},
2903  },
2904
2905  [EID.EN.B]={
2906   data={
2907     hp=1,moveDen=5,clr=13,
2908     lootp=30,
2909     loot={EID.FOOD.A,EID.FOOD.B,
2910       EID.FOOD.C},
2911   },
2912   beh={BE.JUMP,BE.FALL,BE.VULN,BE.HURT,
2913     BE.FACEPLR},
2914  },
2915
2916  [EID.EN.DEMON]={
2917   data={hp=1,moveDen=5,clr=7,
2918    aim=AIM.HORIZ,
2919    shootEid=EID.FIREBALL,
2920    shootSpr=S.EN.DEMON_THROW,
2921    lootp=60,
2922    loot={EID.FOOD.C,EID.FOOD.D}},
2923   beh={BE.JUMP,BE.FALL,BE.SHOOT,
2924    BE.HURT,BE.FACEPLR,BE.VULN},
2925  },
2926
2927  [EID.EN.SDEMON]={
2928   data={hp=1,moveDen=5,clr=7,
2929    flipDen=50,
2930    shootEid=EID.SNOWBALL,
2931    shootSpr=S.EN.SDEMON_THROW,
2932    aim=AIM.HORIZ,
2933    lootp=75,
2934    loot={EID.FOOD.C,EID.FOOD.D}},
2935   beh={BE.JUMP,BE.FALL,BE.SHOOT,
2936    BE.MOVE,BE.FLIP,BE.VULN,BE.HURT},
2937  },
2938
2939  [EID.EN.PDEMON]={
2940   data={hp=1,clr=11,flipDen=50,
2941    shootEid=EID.PLASMA,
2942    shootSpr=S.EN.PDEMON_THROW,
2943    aim=AIM.FULL,
2944    lootp=80,
2945    loot={EID.FOOD.D}},
2946   beh={BE.JUMP,BE.FALL,BE.SHOOT,
2947    BE.FLIP,BE.VULN,BE.HURT},
2948  },
2949
2950  [EID.EN.BAT]={
2951   data={hp=1,moveDen=2,clr=9,flipDen=60,
2952     lootp=40,
2953     loot={EID.FOOD.A,EID.FOOD.B}},
2954   beh={BE.MOVE,BE.FLIP,BE.VULN,BE.HURT},
2955  },
2956
2957  [EID.EN.FISH]={
2958   data={
2959     hp=1,moveDen=3,clr=9,flipDen=120,
2960     lootp=40,
2961     loot={EID.FOOD.A,EID.FOOD.B},
2962   },
2963   beh={BE.MOVE,BE.FLIP,BE.VULN,
2964    BE.HURT},
2965  },
2966
2967  [EID.EN.FISH2]={
2968   data={hp=1,clr=12,moveDen=1,
2969    lootp=60,
2970    loot={EID.FOOD.B,EID.FOOD.C}},
2971   beh={BE.MOVE,BE.DASH,BE.VULN,BE.HURT},
2972  },
2973
2974  [EID.FIREBALL]={
2975   data={hp=1,moveDen=2,clr=7,
2976     coll=CR.BALL,
2977     moveHitMode=MOVE_HIT.DIE},
2978   beh={BE.MOVE,BE.HURT,BE.TTL},
2979  },
2980
2981  [EID.PLASMA]={
2982   data={hp=1,moveDen=2,clr=7,
2983     moveDx=2,
2984     coll=CR.BALL,
2985     moveHitMode=MOVE_HIT.NONE},
2986   beh={BE.MOVE,BE.HURT,BE.TTL},
2987  },
2988
2989  [EID.SNOWBALL]={
2990   data={hp=1,moveDen=1,clr=15,
2991     coll=CR.BALL,
2992     moveHitMode=MOVE_HIT.DIE},
2993   beh={BE.MOVE,BE.FALL,BE.VULN,BE.HURT},
2994  },
2995
2996  [EID.LIFT]={
2997   data={sol=SOL.FULL},
2998   beh={BE.LIFT},
2999  },
3000
3001  [EID.CRUMBLE]={
3002   data={
3003    sol=SOL.FULL,clr=14,
3004    -- only take melee and plane fire dmg
3005    dtypes={DMG.MELEE,DMG.PLANE_FIRE},
3006   },
3007   beh={BE.CRUMBLE,BE.VULN},
3008  },
3009
3010  [EID.PFIRE]={
3011   data={
3012    moveDx=1,moveDen=1,ttl=80,
3013    moveHitMode=MOVE_HIT.DIE,
3014    coll=FIRE.COLL,
3015   },
3016   beh={BE.MOVE,BE.TTL,BE.DMG_ENEMY},
3017  },
3018
3019  [EID.FIRE_PWUP]={
3020   beh={BE.GRANT_FIRE},
3021  },
3022
3023  [EID.SPIKE]={
3024   data={coll=CR.FULL},
3025   beh={BE.HURT},
3026  },
3027
3028  [EID.CHEST]={
3029   data={coll=CR.FULL,
3030    sol=SOL.FULL},
3031   beh={BE.CHEST},
3032  },
3033
3034  [EID.TPLAF]={
3035   data={sol=SOL.HALF,
3036    coll=CR.TOP},
3037   beh={BE.TPLAF},
3038  },
3039
3040  [EID.EN.DASHER]={
3041   data={hp=1,clr=12,moveDen=1,noFall=true,
3042    dashAnim={S.EN.DASHER,315},
3043    lootp=60,
3044    loot={EID.FOOD.B,EID.FOOD.C}},
3045   beh={BE.MOVE,BE.DASH,BE.VULN,BE.HURT},
3046  },
3047
3048  [EID.EN.VBAT]={
3049   data={hp=1,clr=14,yden=2,
3050     lootp=40,
3051     loot={EID.FOOD.B,EID.FOOD.C}},
3052   beh={BE.UPDOWN,BE.VULN,BE.HURT},
3053  },
3054
3055  [EID.SUPER_PWUP]={beh={BE.GRANT_SUPER}},
3056  [EID.SIGN]={beh={BE.SIGN}},
3057  [EID.FLAG]={beh={BE.FLAG}},
3058
3059  [EID.ICICLE]={
3060   data={replEid=EID.ICICLE_F,replDist=8},
3061   beh={BE.REPLACE},
3062  },
3063
3064  [EID.ICICLE_F]={
3065   data={replEid=EID.POP,replData={clr=15}},
3066   beh={BE.FALL,BE.HURT,BE.REPL_ON_GND}
3067  },
3068
3069  [EID.SICICLE]={
3070   data={replEid=EID.SICICLE_F,replDist=8},
3071   beh={BE.REPLACE},
3072  },
3073
3074  [EID.SICICLE_F]={
3075   data={replEid=EID.POP,replData={clr=14}},
3076   beh={BE.FALL,BE.HURT,BE.REPL_ON_GND}
3077  },
3078
3079  [EID.POP]={beh={BE.POP}},
3080  [EID.PLANE]={beh={BE.PLANE}},
3081  [EID.FUEL]={beh={BE.FUEL}},
3082
3083  [EID.FOOD.LEAF]={
3084    data={value=50,coll=CR.FOOD},
3085    beh={BE.POINTS}},
3086  [EID.FOOD.A]={
3087    data={value=100,coll=CR.FOOD},
3088    beh={BE.POINTS}},
3089  [EID.FOOD.B]={
3090    data={value=200,coll=CR.FOOD},
3091    beh={BE.POINTS}},
3092  [EID.FOOD.C]={
3093    data={value=500,coll=CR.FOOD},
3094    beh={BE.POINTS}},
3095  [EID.FOOD.D]={
3096    data={value=1000,coll=CR.FOOD},
3097    beh={BE.POINTS}},
3098 }
3099
3100 ---------------------------------------
3101 -- DEBUG MENU
3102 ---------------------------------------
3103 function DbgTic()
3104  if Plr.dbgResp then
3105   cls(1)
3106   print(Plr.dbgResp)
3107   if btnp(4) then
3108    Plr.dbgResp=nil
3109   end
3110   return
3111  end
3112
3113  Game.dbglvl=Game.dbglvl or 1
3114
3115  if btnp(3) then
3116   Game.dbglvl=Iif(Game.dbglvl+1>#LVL,1,Game.dbglvl+1)
3117  elseif btnp(2) then
3118   Game.dbglvl=Iif(Game.dbglvl>1,Game.dbglvl-1,#LVL)
3119  end
3120
3122   {t="(Close)",f=DbgClose},
3123   {t="Warp to test lvl",f=DbgWarpTest},
3124   {t="Warp to L"..Game.dbglvl,f=DbgWarp},
3125   {t="End lvl",f=DbgEndLvl},
3126   {t="Grant super pwup",f=DbgSuper},
3127   {t="Fly mode "..
3128     Iif(Plr.dbgFly,"OFF","ON"),f=DbgFly},
3129   {t="Invuln mode "..
3130     Iif(Plr.invuln and Plr.invuln>0,
3131         "OFF","ON"),
3132     f=DbgInvuln},
3133   {t="Unpack L"..Game.dbglvl,f=DbgUnpack},
3134   {t="Pack L"..Game.dbglvl,f=DbgPack},
3135   {t="Clear PMEM",f=DbgPmem},
3136   {t="Win the game",f=DbgWin},
3137   {t="Lose the game",f=DbgLose},
3138  }
3139  cls(5)
3140  print("DEBUG")
3141
3142  rect(110,0,140,16,11)
3143  print("DBG LVL:",120,4,3)
3144  print(LVL[Game.dbglvl].name,170,4)
3145
3146  Plr.dbgSel=Plr.dbgSel or 1
3147  for i=1,#menu do
3149    Plr.dbgSel==i and 15 or 0)
3150  end
3151  if btnp(0) then
3152   Plr.dbgSel=Iif(Plr.dbgSel>1,
3154  elseif btnp(1) then
3156    Plr.dbgSel+1,1)
3157  elseif btnp(4) then
3159  end
3160 end
3161
3162 function DbgClose() Plr.dbg=false end
3163
3164 function DbgSuper() Plr.super=true end
3165
3166 function DbgEndLvl()
3167  EndLvl()
3168  Plr.dbg=false
3169 end
3170
3171 function DbgPmem() pmem(0,0) end
3172
3173 function DbgWarp()
3174  StartLvl(Game.dbglvl)
3175 end
3176
3177 function DbgWarpNext()
3178  StartLvl(Game.lvlNo+1)
3179 end
3180
3181 function DbgWarpTest()
3182  StartLvl(#LVL)
3183 end
3184
3185 function DbgUnpack()
3186  UnpackLvl(Game.dbglvl,UMODE.EDIT)
3187  sync()
3188  Plr.dbgResp="Unpacked & synced L"..Game.dbglvl
3189 end
3190
3191 function DbgPack()
3192  local succ=PackLvl(Game.dbglvl)
3193  --MapClear(0,0,LVL_LEN,ROWS)
3194  sync()
3195  Plr.dbgResp=Iif(succ,
3196    "Packed & synced L"..Game.dbglvl,
3197    "** ERROR packing L"..Game.dbglvl)
3198 end
3199
3200 function DbgFly()
3201  Plr.dbgFly=not Plr.dbgFly
3202  Plr.dbgResp="Fly mode "..Iif(Plr.dbgFly,
3203   "ON","OFF")
3204 end
3205
3206 function DbgInvuln()
3207  Plr.invuln=Iif(Plr.invuln>0,0,9999999)
3208  Plr.dbgResp="Invuln mode "..Iif(
3209   Plr.invuln>0,"ON","OFF")
3210 end
3211
3212 function DbgWin()
3213  SetMode(M.WIN)
3214  Plr.dbg=false
3215 end
3216
3217 function DbgLose()
3218  SetMode(M.GAMEOVER)
3219  Plr.dbg=false
3220 end
3221
3222 ---------------------------------------
3223 -- UTILITIES
3224 ---------------------------------------
3225 function Iif(cond,t,f)
3226  if cond then return t else return f end
3227 end
3228
3229 function Iif2(cond,t,cond2,t2,f2)
3230  if cond then return t end
3231  return Iif(cond2,t2,f2)
3232 end
3233
3234 function Iif3(cond,t,cond2,t2,cond3,t3,f3)
3235  if cond then return t end
3236  return Iif2(cond2,t2,cond3,t3,f3)
3237 end
3238
3239 function Iif4(cond,t,cond2,t2,cond3,t3,
3240    cond4,t4,f4)
3241  if cond then return t end
3242  return Iif3(cond2,t2,cond3,t3,cond4,t4,f4)
3243 end
3244
3245 function ArrayContains(a,val)
3246  for i=1,#a do
3247   if a[i]==val then return true end
3248  end
3249  return false
3250 end
3251
3252 function Lpad(value, width)
3253  local s=value..""
3254  while string.len(s) < width do
3255   s="0"..s
3256  end
3257  return s
3258 end
3259
3260 function RectXLate(r,dx,dy)
3261  return {x=r.x+dx,y=r.y+dy,w=r.w,h=r.h}
3262 end
3263
3264 -- rects have x,y,w,h
3265 function RectIsct(r1,r2)
3266  return
3267   r1.x+r1.w>r2.x and r2.x+r2.w>r1.x and
3268   r1.y+r1.h>r2.y and r2.y+r2.h>r1.y
3269 end
3270
3271 function DeepCopy(t)
3272  if type(t)~="table" then return t end
3273  local r={}
3274  for k,v in pairs(t) do
3275   if type(v)=="table" then
3276    r[k]=DeepCopy(v)
3277   else
3278    r[k]=v
3279   end
3280  end
3281  return r
3282 end
3283
3284 -- if preserve, fields that already exist
3285 -- in the target won't be overwritten
3286 function ShallowMerge(target,src,
3287   preserve)
3288  if not src then return end
3289  for k,v in pairs(src) do
3290   if not preserve or not target[k] then
3291    target[k]=DeepCopy(src[k])
3292   end
3293  end
3294 end
3295
3296 function MapCopy(sc,sr,dc,dr,w,h)
3297  for r=0,h-1 do
3298   for c=0,w-1 do
3299    mset(dc+c,dr+r,mget(sc+c,sr+r))
3300   end
3301  end
3302 end
3303
3304 function MapClear(dc,dr,w,h)
3305  for r=0,h-1 do
3306   for c=0,w-1 do
3307    mset(dc+c,dr+r,0)
3308   end
3309  end
3310 end
3311
3312 function MapColsEqual(c1,c2,r)
3313  for i=0,ROWS-1 do
3314   if mget(c1,r+i)~=mget(c2,r+i) then
3315    return false
3316   end
3317  end
3318  return true
3319 end
3320
3321 function MetaVal(t,deflt)
3322  return Iif(
3323   t>=T.META_NUM_0 and t<=T.META_NUM_0+12,
3324   t-T.META_NUM_0,deflt)
3325 end
3326
3327 -- finds marker m on column c of level
3328 -- return row of marker, -1 if not found
3329 function FetchTile(m,c,nowarn)
3330  for r=0,ROWS-1 do
3331   if LvlTile(c,r)==m then
3332    if erase then SetLvlTile(c,r,0) end
3333    return r
3334   end
3335  end
3336  if not nowarn then
3338  end
3339  return -1
3340 end
3341
3342 -- Gets the entity's "tag marker",
3343 -- that is the marker tile that's sitting
3344 -- just above it. Also erases it.
3345 -- If no marker found, returns 0
3346 function FetchEntTag(e)
3347  local t=mget(e.x//C,e.y//C-1)
3348  if t>=T.FIRST_META then
3349   mset(e.x//C,e.y//C-1,0)
3350   return t
3351  else
3352   return 0
3353  end
3354 end
3355
3356 function Max(x,y) return math.max(x,y) end
3357 function Min(x,y) return math.min(x,y) end
3358 function Abs(x,y) return math.abs(x,y) end
3359 function Rnd(lo,hi) return math.random(lo,hi) end
3360 function Rnd01() return math.random() end
3361 function RndSeed(s) return math.randomseed(s) end
3362 function Ins(tbl,e) return table.insert(tbl,e) end
3363 function Rem(tbl,e) return table.remove(tbl,e) end
3364 function Sin(a) return math.sin(a) end
3365 function Cos(a) return math.cos(a) end
```

[[Category::TIC80]]