Difference between revisions of "8-bit panda"

From Media Design: Networked & Lens-Based wiki
Jump to navigation Jump to search
Line 7: Line 7:
 
Find the cheat code !
 
Find the cheat code !
  
Line 684 is, finally, the TIC function (main entry point of the program). Notice that the first line is a call to CheckDbg.
+
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 [https://github.com/nesbox/TIC-80/wiki/key-map key-map].
 
 
* HINT: Line 693 of code + https://github.com/nesbox/TIC-80/wiki/key-map
 
 
 
  
 
<source lang="lua" line>
 
<source lang="lua" line>

Revision as of 09:02, 1 February 2021

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
 214   ROADS={13,14,15,30,31,46,47},
 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,
 333  [14]=0xdad45e, [15]=0xdeeed6,
 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()
 684  CheckDbgMenu()
 685  if Plr.dbg then
 686   DbgTic()
 687   return
 688  end
 689  Game.t=Game.t+1
 690  TICF[Game.m]()
 691 end
 692 
 693 function CheckDbgMenu()
 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
 829  AdjustScroll() 
 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
 839   AdvanceLvl()
 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)
 942  AdjustRespawnPos()
 943 end
 944 
 945 function AdjustRespawnPos()
 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)
1038  UpdateSwimState()
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 
1141 function UpdateSwimState()
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)
1178  local e=EntAdd(EID.PFIRE,x,y)
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 
1291 function AdjustScroll()
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 
1300 function AddToast(points,x,y)
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)
1316 function AddScore(points,tx,ty)
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
1328   AddToast(points,tx,ty)
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 
1341 function EntAdd(newEid,newX,newY)
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)
1386  e.dead=true
1387  local newE=EntAdd(eid,e.x,e.y)
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 
1400 function EntAddBeh(e,beh)
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 
1490 function AdvanceLvl()
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={
1557   rad=4,
1558   count=15,
1559   speed=4,
1560   fall=true,
1561   ttl=15
1562  },
1563  FW={ -- fireworks
1564   rad=3,
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
1576   local r=Rnd01()*fx.rad
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 
1686 function WldHasRoadAt(c,r)
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
1895     TanimAdd(c,r)
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 
1958 function TanimAdd(c,r)
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)
2019    EntAdd(t,dstc*C,r*C)
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
2220   Plr.scoreDisp.text=Lpad(Plr.score,6)
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)
2279  local rad=Max(0,t//2-2) -- radius
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
2385   e.dead=true
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
2460  e.dead=true
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)
2470   local l=EntAdd(e.loot[i],e.x,e.y-4)
2471   EntAddBeh(l,BE.MOVE)
2472   ShallowMerge(l,{moveDy=-1,moveDx=0,
2473     moveDen=1,moveT=8})
2474  end
2475 end
2476 
2477 function BehPoints(e)
2478  e.dead=true
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
2531  local shot=EntAdd(
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)
2596    e.dead=true
2597   end
2598  end
2599 end
2600 
2601 function BehGrantFirePwupColl(e)
2602  Plr.firePwup=FIRE.DUR
2603  e.dead=true
2604  Snd(SND.PWUP)
2605 end
2606 
2607 function BehGrantSuperPwupColl(e)
2608  Plr.super=true
2609  e.dead=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
2625   etop.dead=true
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
2648  EntAdd(c,cx,by)
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)
2708  e.dead=true
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)
2730  e.dead=true
2731  SpawnParts(PFX.POP,e.x+4,e.y+4,e.clr)
2732 end
2733 
2734 function BehBoardPlane(e)
2735  e.dead=true
2736  Plr.plane=PLANE.START_FUEL
2737  Plr.y=e.y-3*C
2738  Snd(SND.PLANE)
2739 end
2740 
2741 function BehFuel(e)
2742  e.dead=true
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 
3121  local menu={
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
3148   print(menu[i].t,10,10+i*10,
3149    Plr.dbgSel==i and 15 or 0)
3150  end
3151  if btnp(0) then
3152   Plr.dbgSel=Iif(Plr.dbgSel>1,
3153    Plr.dbgSel-1,#menu)
3154  elseif btnp(1) then
3155   Plr.dbgSel=Iif(Plr.dbgSel<#menu,
3156    Plr.dbgSel+1,1)
3157  elseif btnp(4) then
3158   (menu[Plr.dbgSel].f)()
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
3337   trace("Marker not found "..m.." @"..c)
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