Permalink
Cannot retrieve contributors at this time
3366 lines (3038 sloc)
63.5 KB
| -- title: 8 Bit Panda | |
| -- author: Bruno Oliveira | |
| -- desc: A panda platformer | |
| -- script: lua | |
| -- saveid: eightbitpanda | |
| -- | |
| -- WARNING: this file must be kept under | |
| -- 64kB (TIC-80 limit)! | |
| NAME="8-BIT PANDA" | |
| C=8 | |
| ROWS=17 | |
| COLS=30 | |
| SCRW=240 | |
| SCRH=136 | |
| -- jump sequence (delta y at each frame) | |
| JUMP_DY={-3,-3,-3,-3,-2,-2,-2,-2, | |
| -1,-1,0,0,0,0,0} | |
| -- swimming seq (implemented as a "jump") | |
| SWIM_JUMP_DY={-2,-2,-1,-1,-1,-1,0,0,0,0,0} | |
| RESURF_DY={-3,-3,-2,-2,-1,-1,0,0,0,0,0} | |
| -- attack sequence (1=preparing, | |
| -- 2=attack,3=recovery) | |
| ATK_SEQ={1,1,1,1,2,3,3,3} | |
| -- die sequence (dx,dy) | |
| DIE_SEQ={{-1,-1},{-2,-2},{-3,-3},{-4,-4}, | |
| {-5,-5},{-6,-5},{-7,-4},{-8,-3},{-8,-2}, | |
| {-8,1},{-8,3},{-8,5},{-8,9},{-8,13}, | |
| {-8,17},{-8,21},{-8,26},{-8,32},{-8,39} | |
| } | |
| -- display x coords in which | |
| -- to keep the player (for scrolling) | |
| SX_MIN=50 | |
| SX_MAX=70 | |
| -- entity/tile solidity | |
| SOL={ | |
| NOT=0, -- not solid | |
| HALF=1, -- only when going down, | |
| -- allows movement upward. | |
| FULL=2, -- fully solid | |
| } | |
| FIRE={ | |
| -- duration of fire powerup | |
| DUR=1000, | |
| -- time between successive fires. | |
| INTERVAL=20, | |
| -- offset from player pos | |
| OFFY=2,OFFX=7,OFFX_FLIP=-2, | |
| OFFX_PLANE=14,OFFY_PLANE=8, | |
| -- projectile collision rect | |
| COLL={x=0,y=0,w=3,h=3}, | |
| } | |
| -- Tiles | |
| -- 0: empty | |
| -- 1-79: static solid blocks | |
| -- 80-127: decorative | |
| -- 128-239: entities | |
| -- 240-255: special markers | |
| T={ | |
| EMPTY=0, | |
| -- platform that's only solid when | |
| -- going down, but allows upward move | |
| HPLAF=4, | |
| SURF=16, | |
| WATER=32, | |
| WFALL=48, | |
| TARMAC=52, -- (where plane can land). | |
| -- sprite id above which tiles are | |
| -- non-solid decorative elements | |
| FIRST_DECO=80, | |
| -- level-end gate components | |
| GATE_L=110,GATE_R=111, | |
| GATE_L2=142,GATE_R=143, | |
| -- tile id above which tiles are | |
| -- representative of entities, not bg | |
| FIRST_ENT=128, | |
| -- tile id above which tiles have special | |
| -- meanings | |
| FIRST_META=240, | |
| -- number markers (used for level | |
| -- packing and annotations). | |
| META_NUM_0=240, | |
| -- followed by nums 1-9. | |
| -- A/B markers (entity-specific meaning) | |
| META_A=254, | |
| META_B=255 | |
| } | |
| -- Autocomplete of tiles patterns. | |
| -- Auto filled when top left map tile | |
| -- is present. | |
| TPAT={ | |
| [85]={w=2,h=2}, | |
| [87]={w=2,h=2}, | |
| [94]={w=2,h=2}, | |
| [89]={w=2,h=2}, | |
| } | |
| -- solidity of tiles (overrides) | |
| TSOL={ | |
| [T.EMPTY]=SOL.NOT, | |
| [T.HPLAF]=SOL.HALF, | |
| [T.SURF]=SOL.NOT, | |
| [T.WATER]=SOL.NOT, | |
| [T.WFALL]=SOL.NOT, | |
| } | |
| -- animated tiles | |
| TANIM={ | |
| [T.SURF]={T.SURF,332}, | |
| [T.WFALL]={T.WFALL,333,334,335}, | |
| } | |
| -- sprites | |
| S={ | |
| PLR={ -- player sprites | |
| STAND=257, | |
| WALK1=258, | |
| WALK2=259, | |
| JUMP=273, | |
| SWING=276, | |
| SWING_C=260, | |
| HIT=277, | |
| HIT_C=278, | |
| DIE=274, | |
| SWIM1=267,SWIM2=268, | |
| -- overlays for fire powerup | |
| FIRE_BAMBOO=262, -- bamboo powerup | |
| FIRE_F=265, -- suit, front | |
| FIRE_P=266, -- suit, profile | |
| FIRE_S=284, -- suit, swimming | |
| -- overlays for super panda powerup | |
| SUPER_F=281, -- suit, front | |
| SUPER_P=282, -- suit, profile | |
| SUPER_S=283, -- suit, swimming | |
| }, | |
| EN={ -- enemy sprites | |
| A=176, | |
| B=177, | |
| DEMON=178, | |
| DEMON_THROW=293, | |
| SLIME=180, | |
| BAT=181, | |
| HSLIME=182, -- hidden slime | |
| DASHER=183, | |
| VBAT=184, | |
| SDEMON=185, -- snow demon | |
| SDEMON_THROW=300, | |
| PDEMON=188, -- plasma demon | |
| PDEMON_THROW=317, | |
| FISH=189, | |
| FISH2=190, | |
| }, | |
| -- crumbling block | |
| CRUMBLE=193,CRUMBLE_2=304,CRUMBLE_3=305, | |
| FIREBALL=179, | |
| FIRE_1=263,FIRE_2=264, | |
| LIFT=192, | |
| PFIRE=263, -- player fire (bamboo) | |
| FIRE_PWUP=129, | |
| -- background mountains | |
| BGMNT={DIAG=496,FULL=497}, | |
| SCRIM=498, -- also 499,500 | |
| SPIKE=194, | |
| CHEST=195,CHEST_OPEN=311, | |
| -- timed platform (opens and closes) | |
| TPLAF=196,TPLAF_HALF=312,TPLAF_OFF=313, | |
| SUPER_PWUP=130, | |
| SIGN=197, | |
| SNOWBALL=186, | |
| FLAG=198, | |
| FLAG_T=326, -- flag after taken | |
| ICICLE=187, -- icicle while hanging | |
| ICICLE_F=303, -- icicle falling | |
| PLANE=132, -- plane (item) | |
| AVIATOR=336, -- aviator sprite (3x2) | |
| AVIATOR_PROP_1=339, -- propeller anim | |
| AVIATOR_PROP_2=340, -- propeller anim | |
| PLASMA=279, -- plasma ball | |
| SICICLE=199, -- stone-themed icicle, | |
| -- while hanging | |
| SICICLE_F=319, -- stone-themed icicle, | |
| -- while falling | |
| FUEL=200, -- fuel item | |
| IC_FUEL=332, -- icon for HUD | |
| TINY_NUM_00=480, -- "00" sprite | |
| TINY_NUM_50=481, -- "50" sprite | |
| TINY_NUM_R1=482, -- 1-10, right aligned | |
| -- food items | |
| FOOD={LEAF=128,A=133,B=134,C=135,D=136}, | |
| SURF1=332,SURF2=333, -- water surface fx | |
| -- world map tiles | |
| WLD={ | |
| -- tiles that player can walk on | |
| ROADS={13,14,15,30,31,46,47}, | |
| -- level tiles | |
| LVL1=61,LVL2=62,LVL3=63, | |
| LVLF=79, -- finale level | |
| -- "cleared level" tile | |
| LVLC=463, | |
| }, | |
| -- Special EIDs that don't correspond to | |
| -- sprites. ID must be > 512 | |
| POP=600, -- entity that dies immediately | |
| -- with a particle effect | |
| } | |
| -- Sprite numbers also function as entity | |
| -- IDs. For readability we write S.FOO | |
| -- when it's a sprite but EID.FOO when | |
| -- it identifies an entity type. | |
| EID=S | |
| -- anims for each entity ID | |
| ANIM={ | |
| [EID.EN.A]={S.EN.A,290}, | |
| [EID.EN.B]={S.EN.B,291}, | |
| [EID.EN.DEMON]={S.EN.DEMON,292}, | |
| [EID.EN.SLIME]={S.EN.SLIME,295}, | |
| [EID.EN.BAT]={S.EN.BAT,296}, | |
| [EID.FIREBALL]={S.FIREBALL,294}, | |
| [EID.FOOD.LEAF]={S.FOOD.LEAF,288,289}, | |
| [EID.PFIRE]={S.PFIRE,264}, | |
| [EID.FIRE_PWUP]={S.FIRE_PWUP,306,307}, | |
| [EID.EN.HSLIME]={S.EN.HSLIME,297}, | |
| [EID.SPIKE]={S.SPIKE,308}, | |
| [EID.CHEST]={S.CHEST,309,310}, | |
| [EID.EN.DASHER]={S.EN.DASHER,314}, | |
| [EID.EN.VBAT]={S.EN.VBAT,298}, | |
| [EID.SUPER_PWUP]={S.SUPER_PWUP,320,321}, | |
| [EID.EN.SDEMON]={S.EN.SDEMON,299}, | |
| [EID.SNOWBALL]={S.SNOWBALL,301}, | |
| [EID.FOOD.D]={S.FOOD.D,322,323,324}, | |
| [EID.FLAG]={S.FLAG,325}, | |
| [EID.ICICLE]={S.ICICLE,302}, | |
| [EID.SICICLE]={S.SICICLE,318}, | |
| [EID.PLANE]={S.PLANE,327,328,329}, | |
| [EID.EN.PDEMON]={S.EN.PDEMON,316}, | |
| [EID.PLASMA]={S.PLASMA,280}, | |
| [EID.FUEL]={S.FUEL,330,331}, | |
| [EID.EN.FISH]={S.EN.FISH,368}, | |
| [EID.EN.FISH2]={S.EN.FISH2,369}, | |
| } | |
| PLANE={ | |
| START_FUEL=2000, | |
| MAX_FUEL=4000, | |
| FUEL_INC=1000, | |
| FUEL_BAR_W=50 | |
| } | |
| -- modes | |
| M={ | |
| BOOT=0, | |
| TITLE=1, -- title screen | |
| TUT=2, -- instructions | |
| RESTORE=3, -- prompting to restore game | |
| WLD=4, -- world map | |
| PREROLL=5, -- "LEVEL X-Y" banner | |
| PLAY=6, | |
| DYING=7, -- die anim | |
| EOL=8, -- end of level | |
| GAMEOVER=9, | |
| WIN=10, -- beat entire game | |
| } | |
| -- collider rects | |
| CR={ | |
| PLR={x=2,y=0,w=4,h=8}, | |
| AVIATOR={x=-6,y=2,w=18,h=10}, | |
| -- default | |
| DFLT={x=2,y=0,w=4,h=8}, | |
| FULL={x=0,y=0,w=8,h=8}, | |
| -- small projectiles | |
| BALL={x=2,y=2,w=3,h=3}, | |
| -- just top rows | |
| TOP={x=0,y=0,w=8,h=2}, | |
| -- player attack | |
| ATK={x=6,y=0,w=7,h=8}, | |
| -- what value to use for x instead if | |
| -- player is flipped (facing left) | |
| ATK_FLIP_X=-5, | |
| FOOD={x=1,y=1,w=6,h=6}, | |
| } | |
| -- max dist entity to update it | |
| ENT_MAX_DIST=220 | |
| -- EIDs to always update regardless of | |
| -- distance. | |
| ALWAYS_UPDATED_EIDS={ | |
| -- lifts need to always be updated for | |
| -- position determinism. | |
| [EID.LIFT]=true | |
| } | |
| -- player damage types | |
| DMG={ | |
| MELEE=0, -- melee attack | |
| FIRE=1, -- fire from fire powerup | |
| PLANE_FIRE=2, -- fire from plane | |
| } | |
| -- default palette | |
| PAL={ | |
| [0]=0x000000, [1]=0x402434, | |
| [2]=0x30346d, [3]=0x4a4a4a, | |
| [4]=0x854c30, [5]=0x346524, | |
| [6]=0xd04648, [7]=0x757161, | |
| [8]=0x34446d, [9]=0xd27d2c, | |
| [10]=0x8595a1, [11]=0x6daa2c, | |
| [12]=0x1ce68d, [13]=0x6dc2ca, | |
| [14]=0xdad45e, [15]=0xdeeed6, | |
| } | |
| -- music tracks | |
| BGM={A=0,B=1,EOL=2,C=3,WLD=4,TITLE=5, | |
| FINAL=6,WIN=7} | |
| -- bgm for each mode (except M.PLAY, which | |
| -- is special) | |
| BGMM={ | |
| [M.TITLE]=BGM.TITLE, | |
| [M.WLD]=BGM.WLD, | |
| [M.EOL]=BGM.EOL, | |
| [M.WIN]=BGM.WIN, | |
| } | |
| -- map data is organized in pages. | |
| -- Each page is 30x17. TIC80 has 64 map | |
| -- pages laid out as an 8x8 grid. We | |
| -- number them in reading order, so 0 | |
| -- is top left, 63 is bottom right. | |
| -- Level info. | |
| -- Levels in the cart are packed | |
| -- (RLE compressed). When a level is loaded, | |
| -- it gets unpacked to the top 8 map pages | |
| -- (0,0-239,16). | |
| -- palor: palette overrides | |
| -- pkstart: map page where packed | |
| -- level starts. | |
| -- pklen: length of level. Entire level | |
| -- must be on same page row, can't | |
| -- span multiple page rows. | |
| LVL={ | |
| { | |
| name="1-1",bg=2, | |
| palor={}, | |
| pkstart=8,pklen=3, | |
| mus=BGM.A, | |
| }, | |
| { | |
| name="1-2",bg=0, | |
| palor={[8]=0x102428}, | |
| pkstart=11,pklen=2, | |
| mus=BGM.B, | |
| }, | |
| { | |
| name="1-3",bg=2, | |
| pkstart=13,pklen=3, | |
| mus=BGM.C, | |
| save=true, | |
| }, | |
| { | |
| name="2-1",bg=1, | |
| palor={[8]=0x553838}, | |
| pkstart=16,pklen=3, | |
| mus=BGM.A, | |
| }, | |
| { | |
| name="2-2",bg=0, | |
| palor={[8]=0x553838}, | |
| pkstart=19,pklen=2, | |
| snow={clr=2}, | |
| mus=BGM.B, | |
| }, | |
| { | |
| name="2-3",bg=1, | |
| palor={[8]=0x553838}, | |
| pkstart=21,pklen=3, | |
| mus=BGM.C, | |
| save=true, | |
| }, | |
| { | |
| name="3-1",bg=2, | |
| palor={[8]=0x7171ae}, | |
| pkstart=24,pklen=3, | |
| snow={clr=10}, | |
| mus=BGM.A, | |
| }, | |
| { | |
| name="3-2",bg=0, | |
| palor={[8]=0x3c3c50}, | |
| pkstart=27,pklen=2, | |
| snow={clr=10}, | |
| mus=BGM.B, | |
| }, | |
| { | |
| name="3-3",bg=2, | |
| palor={[8]=0x7171ae}, | |
| pkstart=29,pklen=3, | |
| mus=BGM.C, | |
| save=true, | |
| }, | |
| { | |
| name="4-1",bg=2, | |
| palor={[2]=0x443c14,[8]=0x504410}, | |
| pkstart=32,pklen=3, | |
| mus=BGM.A, | |
| }, | |
| { | |
| name="4-2",bg=2, | |
| palor={[2]=0x443c14,[8]=0x504410}, | |
| pkstart=35,pklen=2, | |
| mus=BGM.B, | |
| }, | |
| { | |
| name="4-3",bg=2, | |
| palor={[2]=0x443c14,[8]=0x504410}, | |
| pkstart=37,pklen=3, | |
| mus=BGM.C, | |
| save=true, | |
| }, | |
| { | |
| name="5-1",bg=1, | |
| palor={[8]=0x553838}, | |
| pkstart=40,pklen=3, | |
| mus=BGM.A, | |
| }, | |
| { | |
| name="5-2",bg=1, | |
| palor={[8]=0x553838}, | |
| pkstart=43,pklen=2, | |
| mus=BGM.B, | |
| }, | |
| { | |
| name="5-3",bg=1, | |
| palor={[8]=0x553838}, | |
| pkstart=45,pklen=3, | |
| mus=BGM.C, | |
| save=true, | |
| }, | |
| { | |
| name="6-1",bg=0, | |
| palor={[8]=0x303030}, | |
| pkstart=48,pklen=3, | |
| mus=BGM.FINAL, | |
| snow={clr=8}, | |
| }, | |
| { | |
| name="6-2",bg=0, | |
| palor={[8]=0x303030}, | |
| pkstart=51,pklen=5, | |
| mus=BGM.FINAL, | |
| snow={clr=8}, | |
| }, | |
| } | |
| -- length of unpacked level, in cols | |
| -- 240 means the top 8 map pages | |
| LVL_LEN=240 | |
| -- sound specs | |
| SND={ | |
| KILL={sfxid=62,note=30,dur=5}, | |
| JUMP={sfxid=61,note=30,dur=4}, | |
| SWIM={sfxid=61,note=50,dur=3}, | |
| ATTACK={sfxid=62,note=40,dur=4}, | |
| POINT={sfxid=60,note=60,dur=5,speed=3}, | |
| DIE={sfxid=63,note=18,dur=20,speed=-1}, | |
| HURT={sfxid=63,note="C-4",dur=4}, | |
| PWUP={sfxid=60,note=45,dur=15,speed=-2}, | |
| ONEUP={sfxid=60,note=40,dur=60,speed=-3}, | |
| PLANE={sfxid=59,note="C-4",dur=70,speed=-3}, | |
| OPEN={sfxid=62,note="C-3",dur=4,speed=-2}, | |
| } | |
| -- world map consts | |
| WLD={ | |
| -- foreground tile page | |
| FPAGE=61, | |
| -- background tile page | |
| BPAGE=62, | |
| } | |
| -- WLD point of interest types | |
| POI={ | |
| LVL=0, | |
| } | |
| -- settings | |
| Sett={ | |
| snd=true, | |
| mus=true | |
| } | |
| -- game state | |
| Game={ | |
| -- mode | |
| m=M.BOOT, | |
| -- ticks since mode start | |
| t=0, | |
| -- current level# we're playing | |
| lvlNo=0, | |
| lvl=nil, -- shortcut to LVL[lvlNo] | |
| -- scroll offset in current level | |
| scr=0, | |
| -- auto-generated background mountains | |
| bgmnt=nil, | |
| -- snow flakes (x,y pairs). These don't | |
| -- change, we just shift when rendering. | |
| snow=nil, | |
| -- highest level cleared by player, | |
| -- -1 if no level cleared | |
| topLvl=-1, | |
| } | |
| -- world map state | |
| Wld={ | |
| -- points of interest (levels, etc) | |
| pois={}, | |
| -- savegame start pos (maps start level | |
| -- to col,row) | |
| spos={}, | |
| plr={ | |
| -- start pos | |
| x0=-1,y0=-1, | |
| -- player pos, in pixels not row/col | |
| x=0,y=0, | |
| -- player move dir, if moving. Will move | |
| -- until plr arrives at next cell | |
| dx=0,dy=0, | |
| -- last move dir | |
| ldx=0,ldy=0, | |
| -- true iff player facing left | |
| flipped=false, | |
| } | |
| } | |
| -- player | |
| Plr={} -- deep-copied from PLR_INIT_STATE | |
| PLR_INIT_STATE={ | |
| lives=3, | |
| x=0,y=0, -- current pos | |
| dx=0,dy=0, -- last movement | |
| flipped=false, -- if true, is facing left | |
| jmp=0, -- 0=not jumping, otherwise | |
| -- it's the cur jump frame | |
| jmpSeq=JUMP_DY, -- set during jump | |
| grounded=false, | |
| swim=false, | |
| -- true if plr is near surface of water | |
| surf=false, | |
| -- attack state. 0=not attacking, | |
| -- >0 indexes into ATK_SEQ | |
| atk=0, | |
| -- die animation frame, 0=not dying | |
| -- indexes into DIE_SEQ | |
| dying=0, | |
| -- nudge (movement resulting from | |
| -- collisions) | |
| nudgeX=0,nudgeY=0, | |
| -- if >0, has fire bamboo powerup | |
| -- and this is the countdown to end | |
| firePwup=0, | |
| -- if >0 player has fired bamboo. | |
| -- This is ticks until player can fire | |
| -- again. | |
| fireCd=0, | |
| -- if >0, is invulnerable for this | |
| -- many ticks, 0 if not invulnerable | |
| invuln=0, | |
| -- if true, has the super panda powerup | |
| super=false, | |
| -- if != 0, player is being dragged | |
| -- horizontally (forced to move in that | |
| -- direction -- >0 is right, <0 is left) | |
| -- The abs value is how many frames | |
| -- this lasts for. | |
| drag=0, | |
| -- the sign message (index) the player | |
| -- is currently reading. | |
| signMsg=0, | |
| -- sign cycle counter: 1 when just | |
| -- starting to read sign, increases. | |
| -- when player stops reading sign, | |
| -- decreases back to 0. | |
| signC=0, | |
| -- respawn pos, 0,0 if unset | |
| respX=-1,respY=-1, | |
| -- if >0, the player is on the plane | |
| -- and this is the fuel left (ticks). | |
| plane=0, | |
| -- current score | |
| score=0, | |
| -- for performance, we keep the | |
| -- stringified score ready for display | |
| scoreDisp={text=nil,value=-1}, | |
| -- time (Game.t) when score last changed | |
| scoreMt=-999, | |
| -- if >0, player is blocked from moving | |
| -- for that many frames. | |
| locked=0, | |
| } | |
| -- max cycle counter for signs | |
| SIGN_C_MAX=10 | |
| -- sign texts. | |
| SIGN_MSGS={ | |
| [0]={ | |
| l1="Green bamboo protects", | |
| l2="against one enemy attack.", | |
| }, | |
| [1]={ | |
| l1="Yellow bamboo allows you to throw", | |
| l2="bamboo shoots (limited time).", | |
| }, | |
| [2]={ | |
| l1="Pick up leaves and food to get", | |
| l2="points. 10,000 = extra life.", | |
| }, | |
| [4]={ | |
| l1="Bon voyage!", | |
| l2="Don't run out of fuel.", | |
| }, | |
| } | |
| -- entities | |
| Ents={} | |
| -- particles | |
| Parts={} | |
| -- score toasts | |
| Toasts={} | |
| -- animated tiles, for quick lookup | |
| -- indexed by COLUMN. | |
| -- Tanims[c] is a list of integers | |
| -- indicating rows of animated tiles. | |
| Tanims={} | |
| function SetMode(m) | |
| Game.m=m | |
| Game.t=0 | |
| if m~=M.PLAY and m~=M.DYING and | |
| m~=M.EOL then | |
| ResetPal() | |
| end | |
| UpdateMus() | |
| end | |
| function UpdateMus() | |
| if Game.m==M.PLAY then | |
| PlayMus(Game.lvl.mus) | |
| else | |
| PlayMus(BGMM[Game.m] or -1) | |
| end | |
| end | |
| function TIC() | |
| CheckDbgMenu() | |
| if Plr.dbg then | |
| DbgTic() | |
| return | |
| end | |
| Game.t=Game.t+1 | |
| TICF[Game.m]() | |
| end | |
| function CheckDbgMenu() | |
| if not btn(6) then | |
| Game.dbgkc=0 | |
| return | |
| end | |
| if btnp(0) then | |
| Game.dbgkc=10+(Game.dbgkc or 0) | |
| end | |
| if btnp(1) then | |
| Game.dbgkc=1+(Game.dbgkc or 0) | |
| end | |
| if Game.dbgkc==42 then Plr.dbg=true end | |
| end | |
| function Boot() | |
| ResetPal() | |
| WldInit() | |
| SetMode(M.TITLE) | |
| end | |
| -- restores default palette with | |
| -- the given overrides. | |
| function ResetPal(palor) | |
| for c=0,15 do | |
| local clr=PAL[c] | |
| if palor and palor[c] then | |
| clr=palor[c] | |
| end | |
| poke(0x3fc0+c*3+0,(clr>>16)&255) | |
| poke(0x3fc0+c*3+1,(clr>>8)&255) | |
| poke(0x3fc0+c*3+2,clr&255) | |
| end | |
| end | |
| function TitleTic() | |
| ResetPal() | |
| cls(2) | |
| local m=MapPageStart(63) | |
| map(m.c,m.r,30,17,0,0,0) | |
| spr(S.PLR.WALK1+(time()//128)%2,16,104,0) | |
| rect(0,0,240,24,5) | |
| print(NAME,88,10) | |
| rect(0,24,240,1,15) | |
| rect(0,26,240,1,5) | |
| rect(0,SCRH-8,SCRW,8,0) | |
| print("github.com/btco/panda",60,SCRH-7,7) | |
| if (time()//512)%2>0 then | |
| print("- PRESS 'Z' TO START -",65,84,15) | |
| end | |
| RendSpotFx(COLS//2,ROWS//2,Game.t) | |
| if btnp(4) then | |
| SetMode(M.RESTORE) | |
| end | |
| end | |
| function RestoreTic() | |
| local saveLvl=pmem(0) or 0 | |
| if saveLvl<1 then | |
| StartGame(1) | |
| return | |
| end | |
| Game.restoreSel=Game.restoreSel or 0 | |
| cls(0) | |
| local X=40 | |
| local Y1=30 | |
| local Y2=60 | |
| print("CONTINUE (LEVEL ".. | |
| LVL[saveLvl].name ..")",X,Y1) | |
| print("START NEW GAME",X,Y2) | |
| spr(S.PLR.STAND,X-20, | |
| Iif(Game.restoreSel>0,Y2,Y1)) | |
| if btnp(0) or btnp(1) then | |
| Game.restoreSel= | |
| Iif(Game.restoreSel>0,0,1) | |
| elseif btnp(4) then | |
| StartGame(Game.restoreSel>0 and | |
| 1 or saveLvl) | |
| end | |
| end | |
| function TutTic() | |
| cls(0) | |
| if Game.tutdone then | |
| StartLvl(1) | |
| return | |
| end | |
| local p=MapPageStart(56) | |
| map(p.c,p.r,COLS,ROWS) | |
| print("CONTROLS",100,10) | |
| print("JUMP",56,55); | |
| print("ATTACK",72,90); | |
| print("MOVE",160,50); | |
| print("10,000 PTS = EXTRA LIFE",60,110,3); | |
| if Game.t>150 and 0==((Game.t//16)%2) then | |
| print("- Press Z to continue -",60,130) | |
| end | |
| if Game.t>150 and btnp(4) then | |
| Game.tutdone=true | |
| StartLvl(1) | |
| end | |
| end | |
| function WldTic() | |
| WldUpdate() | |
| WldRend() | |
| end | |
| function PrerollTic() | |
| cls(0) | |
| print("LEVEL "..Game.lvl.name,100,40) | |
| spr(S.PLR.STAND,105,60) | |
| print("X " .. Plr.lives,125,60) | |
| if Game.t>60 then | |
| SetMode(M.PLAY) | |
| end | |
| end | |
| function PlayTic() | |
| if Plr.dbgFly then | |
| UpdateDbgFly() | |
| else | |
| UpdatePlr() | |
| UpdateEnts() | |
| UpdateParts() | |
| DetectColl() | |
| ApplyNudge() | |
| CheckEndLvl() | |
| end | |
| AdjustScroll() | |
| Rend() | |
| if Game.m==M.PLAY then | |
| RendSpotFx((Plr.x-Game.scr)//C, | |
| Plr.y//C,Game.t) | |
| end | |
| end | |
| function EolTic() | |
| if Game.t>160 then | |
| AdvanceLvl() | |
| return | |
| end | |
| Rend() | |
| print("LEVEL CLEAR",85,20) | |
| end | |
| function DyingTic() | |
| Plr.dying=Plr.dying+1 | |
| if Game.t>100 then | |
| if Plr.lives>1 then | |
| Plr.lives=Plr.lives-1 | |
| SetMode(M.WLD) | |
| else | |
| SetMode(M.GAMEOVER) | |
| end | |
| else | |
| Rend() | |
| end | |
| end | |
| function GameOverTic() | |
| cls(0) | |
| print("GAME OVER!",92,50) | |
| if Game.t>150 then | |
| SetMode(M.TITLE) | |
| end | |
| end | |
| function WinTic() | |
| cls(0) | |
| Game.scr=0 | |
| local m=MapPageStart(57) | |
| map(m.c,m.r, | |
| math.min(30,(Game.t-300)//8),17,0,0,0) | |
| print("THE END!",100, | |
| math.max(20,SCRH-Game.t//2)) | |
| print("Thanks for playing!",70, | |
| math.max(30,120+SCRH-Game.t//2)) | |
| if Game.t%100==0 then | |
| SpawnParts(PFX.FW,Rnd(40,SCRW-40), | |
| Rnd(40,SCRH-40),Rnd(2,15)) | |
| end | |
| UpdateParts() | |
| RendParts() | |
| if Game.t>1200 and btnp(4) then | |
| SetMode(M.TITLE) | |
| end | |
| end | |
| TICF={ | |
| [M.BOOT]=Boot, | |
| [M.TITLE]=TitleTic, | |
| [M.TUT]=TutTic, | |
| [M.RESTORE]=RestoreTic, | |
| [M.WLD]=WldTic, | |
| [M.PREROLL]=PrerollTic, | |
| [M.PLAY]=PlayTic, | |
| [M.DYING]=DyingTic, | |
| [M.GAMEOVER]=GameOverTic, | |
| [M.EOL]=EolTic, | |
| [M.WIN]=WinTic, | |
| } | |
| function StartGame(startLvlNo) | |
| Game.topLvl=startLvlNo-1 | |
| Plr=DeepCopy(PLR_INIT_STATE) | |
| -- put player at the right start pos | |
| local sp=Wld.spos[startLvlNo] or | |
| {x=Wld.plr.x0,y=Wld.plr.y0} | |
| Wld.plr.x=sp.x | |
| Wld.plr.y=sp.y | |
| SetMode(M.WLD) | |
| end | |
| function StartLvl(lvlNo) | |
| local oldLvlNo=Game.lvlNo | |
| Game.lvlNo=lvlNo | |
| Game.lvl=LVL[lvlNo] | |
| Game.scr=0 | |
| local old=Plr | |
| Plr=DeepCopy(PLR_INIT_STATE) | |
| -- preserve lives, score | |
| Plr.lives=old.lives | |
| Plr.score=old.score | |
| Plr.super=old.super | |
| if oldLvlNo==lvlNo then | |
| Plr.respX=old.respX | |
| Plr.respY=old.respY | |
| end | |
| SetMode(M.PREROLL) | |
| Ents={} | |
| Parts={} | |
| Toasts={} | |
| Tanims={} | |
| UnpackLvl(lvlNo,UMODE.GAME) | |
| GenBgMnt() | |
| GenSnow() | |
| ResetPal(Game.lvl.palor) | |
| AdjustRespawnPos() | |
| end | |
| function AdjustRespawnPos() | |
| if Plr.respX<0 then return end | |
| for i=1,#Ents do | |
| local e=Ents[i] | |
| if e.eid==EID.FLAG and e.x<Plr.respX then | |
| EntRepl(e,EID.FLAG_T) | |
| end | |
| end | |
| Plr.x=Plr.respX | |
| Plr.y=Plr.respY | |
| end | |
| -- generates background mountains. | |
| function GenBgMnt() | |
| local MAX_Y=12 | |
| local MIN_Y=2 | |
| -- min/max countdown to change direction: | |
| local MIN_CD=2 | |
| local MAX_CD=6 | |
| Game.bgmnt={} | |
| RndSeed(Game.lvlNo) | |
| local y=Rnd(MIN_Y,MAX_Y) | |
| local dy=1 | |
| local cd=Rnd(MIN_CD,MAX_CD) | |
| for i=1,LVL_LEN do | |
| Ins(Game.bgmnt,{y=y,dy=dy}) | |
| cd=cd-1 | |
| if cd<=0 or y+dy<MIN_Y or y+dy>MAX_Y then | |
| -- keep same y but change direction | |
| cd=Rnd(MIN_CD,MAX_CD) | |
| dy=-dy | |
| else | |
| y=y+dy | |
| end | |
| end | |
| RndSeed(time()) | |
| end | |
| function GenSnow() | |
| if not Game.lvl.snow then | |
| Game.snow=nil | |
| return | |
| end | |
| Game.snow={} | |
| for r=0,ROWS-1,2 do | |
| for c=0,COLS-1,2 do | |
| Ins(Game.snow,{ | |
| x=c*C+Rnd(-8,8), | |
| y=r*C+Rnd(-8,8) | |
| }) | |
| end | |
| end | |
| end | |
| -- Whether player is on solid ground. | |
| function IsOnGround() | |
| return not CanMove(Plr.x,Plr.y+1) | |
| end | |
| -- Get level tile at given point | |
| function LvlTileAtPt(x,y) | |
| return LvlTile(x//C,y//C) | |
| end | |
| -- Get level tile. | |
| function LvlTile(c,r) | |
| if c<0 or c>=LVL_LEN then return 0 end | |
| if r<0 then return 0 end | |
| -- bottom-most tile repeats infinitely | |
| -- below (to allow player to swim | |
| -- when bottom tile is water). | |
| if r>=ROWS then r=ROWS-1 end | |
| return mget(c,r) | |
| end | |
| function SetLvlTile(c,r,t) | |
| if c<0 or c>=LVL_LEN then return false end | |
| if r<0 or r>=ROWS then return false end | |
| mset(c,r,t) | |
| end | |
| function UpdatePlr() | |
| local oldx=Plr.x | |
| local oldy=Plr.y | |
| Plr.plane=Max(Plr.plane-1,0) | |
| Plr.fireCd=Max(Plr.fireCd-1,0) | |
| Plr.firePwup=Max(Plr.firePwup-1,0) | |
| Plr.invuln=Max(Plr.invuln-1,0) | |
| Plr.drag=Iif2(Plr.drag>0,Plr.drag-1, | |
| Plr.drag<0,Plr.drag+1,0) | |
| Plr.signC=Max(Plr.signC-1,0) | |
| Plr.locked=Max(Plr.locked-1,0) | |
| UpdateSwimState() | |
| local swimmod=Plr.swim and Game.t%2 or 0 | |
| if (Plr.plane==0 and Plr.jmp==0 and | |
| not IsOnGround()) then | |
| -- fall | |
| Plr.y=Plr.y+1-swimmod | |
| end | |
| -- check if player fell into pit | |
| if Plr.y>SCRH+8 then | |
| StartDying() | |
| return | |
| end | |
| -- horizontal movement | |
| local dx=0 | |
| local dy=0 | |
| local wantLeft=Plr.locked==0 and | |
| Iif(Plr.drag==0,btn(2),Plr.drag<0) | |
| local wantRight=Plr.locked==0 and | |
| Iif(Plr.drag==0,btn(3),Plr.drag>0) | |
| local wantJmp=Plr.locked==0 and | |
| Plr.plane==0 and btnp(4) and Plr.drag==0 | |
| local wantAtk=Plr.locked==0 and | |
| btnp(5) and Plr.drag==0 | |
| if wantLeft then | |
| dx=-1+swimmod | |
| -- plane doesn't flip | |
| Plr.flipped=true | |
| elseif wantRight then | |
| dx=1-swimmod | |
| Plr.flipped=false | |
| end | |
| -- vertical movement (plane only) | |
| dy=dy+Iif2(Plr.plane>0 and btn(0) and | |
| Plr.y>8,-1, | |
| Plr.plane>0 and btn(1) and | |
| Plr.y<SCRH-16,1,0) | |
| -- is player flipped (facing left?) | |
| Plr.flipped=Iif3( | |
| Plr.plane>0,false,btn(2),true, | |
| btn(3),false,Plr.flipped) | |
| TryMoveBy(dx,dy) | |
| Plr.grounded=Plr.plane==0 and IsOnGround() | |
| local canJmp=Plr.grounded or Plr.swim | |
| -- jump | |
| if wantJmp and canJmp then | |
| Plr.jmp=1 | |
| Plr.jmpSeq=Plr.surf and | |
| RESURF_DY or | |
| (Plr.swim and SWIM_JUMP_DY or | |
| JUMP_DY) | |
| Snd(Plr.surf and SND.JUMP or | |
| Plr.swim and SND.SWIM or SND.JUMP) | |
| -- TODO play swim snd if swim | |
| end | |
| if Plr.jmp>#Plr.jmpSeq then | |
| -- end jump | |
| Plr.jmp=0 | |
| elseif Plr.jmp>0 then | |
| local ok=TryMoveBy( | |
| 0,Plr.jmpSeq[Plr.jmp]) | |
| -- if blocked, cancel jump | |
| Plr.jmp=ok and Plr.jmp+1 or 0 | |
| end | |
| -- attack | |
| if Plr.atk==0 then | |
| if wantAtk then | |
| -- start attack sequence | |
| if Plr.plane==0 then Plr.atk=1 end | |
| Snd(SND.ATTACK) | |
| TryFire() | |
| end | |
| elseif Plr.atk>#ATK_SEQ then | |
| -- end of attack sequence | |
| Plr.atk=0 | |
| else | |
| -- advance attack sequence | |
| Plr.atk=Plr.atk+1 | |
| end | |
| -- check plane landing | |
| if Plr.plane>0 then CheckTarmac() end | |
| Plr.dx=Plr.x-oldx | |
| Plr.dy=Plr.y-oldy | |
| end | |
| function IsWater(t) | |
| return t==T.WATER or t==T.SURF or | |
| t==T.WFALL | |
| end | |
| function UpdateSwimState() | |
| local wtop=IsWater( | |
| LvlTileAtPt(Plr.x+4,Plr.y+1)) | |
| local wbottom=IsWater( | |
| LvlTileAtPt(Plr.x+4,Plr.y+7)) | |
| local wtop2=IsWater( | |
| LvlTileAtPt(Plr.x+4,Plr.y-8)) | |
| Plr.swim=wtop and wbottom | |
| -- is plr near surface? | |
| Plr.surf=wbottom and not wtop2 | |
| end | |
| function UpdateDbgFly() | |
| local d=Iif(btn(4),5,1) | |
| if btn(0) then Plr.y=Plr.y-d end | |
| if btn(1) then Plr.y=Plr.y+d end | |
| if btn(2) then Plr.x=Plr.x-d end | |
| if btn(3) then Plr.x=Plr.x+d end | |
| if btn(5) then Plr.dbgFly=false end | |
| end | |
| function TryFire() | |
| if Plr.firePwup<1 and Plr.plane==0 then | |
| return | |
| end | |
| if Plr.fireCd>0 then return end | |
| Plr.fireCd=FIRE.INTERVAL | |
| local x=Plr.x | |
| if Plr.plane==0 then | |
| x=x+(Plr.flipped and | |
| FIRE.OFFX_FLIP or FIRE.OFFX) | |
| else | |
| -- end of plane | |
| x=x+FIRE.OFFX_PLANE | |
| end | |
| local y=Plr.y+Iif(Plr.plane>0, | |
| FIRE.OFFY_PLANE,FIRE.OFFY) | |
| local e=EntAdd(EID.PFIRE,x,y) | |
| e.moveDx=Plr.plane>0 and 2 or | |
| (Plr.flipped and -1 or 1) | |
| e.ttl=Plr.plane>0 and e.ttl//2 or e.ttl | |
| end | |
| function ApplyNudge() | |
| Plr.y=Plr.y+Plr.nudgeY | |
| Plr.x=Plr.x+Plr.nudgeX | |
| Plr.nudgeX=0 | |
| Plr.nudgeY=0 | |
| end | |
| function TryMoveBy(dx,dy) | |
| if CanMove(Plr.x+dx,Plr.y+dy) then | |
| Plr.x=Plr.x+dx | |
| Plr.y=Plr.y+dy | |
| return true | |
| end | |
| return false | |
| end | |
| function GetPlrCr() | |
| return Iif(Plr.plane>0,CR.AVIATOR,CR.PLR) | |
| end | |
| -- Check if plr can move to given pos. | |
| function CanMove(x,y) | |
| local dy=y-Plr.y | |
| local pcr=GetPlrCr() | |
| local r=CanMoveEx(x,y,pcr,dy) | |
| if not r then return false end | |
| -- check if would bump into solid ent | |
| local pr=RectXLate(pcr,x,y) | |
| for i=1,#Ents do | |
| local e=Ents[i] | |
| local effSolid=(e.sol==SOL.FULL) or | |
| (e.sol==SOL.HALF and dy>0 and | |
| Plr.y+5<e.y) -- (HACK) | |
| if effSolid then | |
| local er=RectXLate(e.coll,e.x,e.y) | |
| if RectIsct(pr,er) then | |
| return false | |
| end | |
| end | |
| end | |
| return true | |
| end | |
| function EntCanMove(e,x,y) | |
| return CanMoveEx(x,y,e.coll,y-e.y) | |
| end | |
| function GetTileSol(t) | |
| local s=TSOL[t] | |
| -- see if an override is present. | |
| if s~=nil then return s end | |
| -- default: | |
| return Iif(t>=T.FIRST_DECO,SOL.NOT,SOL.FULL) | |
| end | |
| -- x,y: candidate pos; cr: collision rect | |
| -- dy: y direction of movement | |
| function CanMoveEx(x,y,cr,dy) | |
| local x1=x+cr.x | |
| local y1=y+cr.y | |
| local x2=x1+cr.w-1 | |
| local y2=y1+cr.h-1 | |
| -- check all tiles touched by the rect | |
| local startC=x1//C | |
| local endC=x2//C | |
| local startR=y1//C | |
| local endR=y2//C | |
| for c=startC,endC do | |
| for r=startR,endR do | |
| local sol=GetTileSol(LvlTile(c,r)) | |
| if sol==SOL.FULL then return false end | |
| end | |
| end | |
| -- special case: check for half-solidity | |
| -- tiles. Only solid when standing on | |
| -- top of them (y2%C==0) and going | |
| -- down (dy>0). | |
| local sA=GetTileSol(LvlTileAtPt(x1,y2)) | |
| local sB=GetTileSol(LvlTileAtPt(x2,y2)) | |
| if dy>0 and (sA==SOL.HALF or | |
| sB==SOL.HALF) and | |
| y2%C==0 then return false end | |
| return true | |
| end | |
| function EntWouldFall(e,x) | |
| return EntCanMove(e,x,e.y+1) | |
| end | |
| -- check if player landed plane on tarmac | |
| function CheckTarmac() | |
| local pr=RectXLate( | |
| CR.AVIATOR,Plr.x,Plr.y) | |
| local bottom=pr.y+pr.h+1 | |
| local t1=LvlTileAtPt(pr.x,bottom) | |
| local t2=LvlTileAtPt(pr.x+pr.w,bottom) | |
| if t1==T.TARMAC and t2==T.TARMAC then | |
| -- landed | |
| Plr.plane=0 | |
| SpawnParts(PFX.POP,Plr.x+4,Plr.y,14) | |
| -- TODO: more vfx, sfx | |
| end | |
| end | |
| function AdjustScroll() | |
| local dispx=Plr.x-Game.scr | |
| if dispx>SX_MAX then | |
| Game.scr=Plr.x-SX_MAX | |
| elseif dispx<SX_MIN then | |
| Game.scr=Plr.x-SX_MIN | |
| end | |
| end | |
| function AddToast(points,x,y) | |
| local rem=points%100 | |
| if points>1000 or (rem~=50 and rem~=0) then | |
| return | |
| end | |
| local sp2=rem==50 and S.TINY_NUM_50 or | |
| S.TINY_NUM_00 | |
| local sp1=points>=100 and | |
| (S.TINY_NUM_R1-1+points//100) or 0 | |
| Ins(Toasts,{ | |
| x=Iif(points>=100,x-8,x-12), | |
| y=y,ttl=40,sp1=sp1,sp2=sp2}) | |
| end | |
| -- tx,ty: position where to show toast | |
| -- (optional) | |
| function AddScore(points,tx,ty) | |
| local old=Plr.score | |
| Plr.score=Plr.score+points | |
| Plr.scoreMt=Game.t | |
| if (old//10000)<(Plr.score//10000) then | |
| Snd(SND.ONEUP) | |
| Plr.lives=Plr.lives+1 | |
| -- TODO: vfx | |
| else | |
| Snd(SND.POINT) | |
| end | |
| if tx and ty then | |
| AddToast(points,tx,ty) | |
| end | |
| end | |
| function StartDying() | |
| SetMode(M.DYING) | |
| Snd(SND.DIE) | |
| Plr.dying=1 -- start die anim | |
| Plr.super=false | |
| Plr.firePwup=0 | |
| Plr.plane=0 | |
| end | |
| function EntAdd(newEid,newX,newY) | |
| local e={ | |
| eid=newEid, | |
| x=newX, | |
| y=newY | |
| } | |
| Ins(Ents,e) | |
| EntInit(e) | |
| return e | |
| end | |
| function EntInit(e) | |
| -- check if we have an animation for it | |
| if ANIM[e.eid] then | |
| e.anim=ANIM[e.eid] | |
| e.sprite=e.anim[1] | |
| else | |
| -- default to static sprite image | |
| e.sprite=e.eid | |
| end | |
| -- whether ent sprite is flipped | |
| e.flipped=false | |
| -- collider rect | |
| e.coll=CR.DFLT | |
| -- solidity (defaults to not solid) | |
| e.sol=SOL.NOT | |
| -- EBT entry | |
| local ebte=EBT[e.eid] | |
| -- behaviors | |
| e.beh=ebte and ebte.beh or {} | |
| -- copy initial behavior data to entity | |
| for _,b in pairs(e.beh) do | |
| ShallowMerge(e,b.data) | |
| end | |
| -- overlay the entity-defined data. | |
| if ebte and ebte.data then | |
| ShallowMerge(e,ebte.data) | |
| end | |
| -- call the entity init funcs | |
| for _,b in pairs(e.beh) do | |
| if b.init then b.init(e) end | |
| end | |
| end | |
| function EntRepl(e,eid,data) | |
| e.dead=true | |
| local newE=EntAdd(eid,e.x,e.y) | |
| if data then | |
| ShallowMerge(newE,data) | |
| end | |
| end | |
| function EntHasBeh(e,soughtBeh) | |
| for _,b in pairs(e.beh) do | |
| if b==soughtBeh then return true end | |
| end | |
| return false | |
| end | |
| function EntAddBeh(e,beh) | |
| if EntHasBeh(e,beh) then return end | |
| -- note: can't mutate the original | |
| -- e.beh because it's a shared ref. | |
| e.beh=DeepCopy(e.beh) | |
| ShallowMerge(e,beh.data,true) | |
| Ins(e.beh,beh) | |
| end | |
| function UpdateEnts() | |
| -- iterate backwards so we can delete | |
| for i=#Ents,1,-1 do | |
| local e=Ents[i] | |
| UpdateEnt(e) | |
| if e.dead then | |
| -- delete | |
| Rem(Ents,i) | |
| end | |
| end | |
| end | |
| function UpdateEnt(e) | |
| if not ALWAYS_UPDATED_EIDS[e.eid] and | |
| Abs(e.x-Plr.x)>ENT_MAX_DIST then | |
| -- too far, don't update | |
| return | |
| end | |
| -- update anim frame | |
| if e.anim then | |
| e.sprite=e.anim[1+(time()//128)%#e.anim] | |
| end | |
| -- run update behaviors | |
| for _,b in pairs(e.beh) do | |
| if b.update then b.update(e) end | |
| end | |
| end | |
| function GetEntAt(x,y) | |
| for i=1,#Ents do | |
| local e=Ents[i] | |
| if e.x==x and e.y==y then return e end | |
| end | |
| return nil | |
| end | |
| -- detect collisions | |
| function DetectColl() | |
| -- player rect | |
| local pr=RectXLate(GetPlrCr(), | |
| Plr.x,Plr.y) | |
| -- attack rect | |
| local ar=nil | |
| if ATK_SEQ[Plr.atk]==2 then | |
| -- player is attacking, so check if | |
| -- entity was hit by attack | |
| ar=RectXLate(CR.ATK,Plr.x,Plr.y) | |
| if Plr.flipped then | |
| ar.x=Plr.x+CR.ATK_FLIP_X | |
| end | |
| end | |
| for i=1,#Ents do | |
| local e=Ents[i] | |
| local er=RectXLate(e.coll,e.x,e.y) | |
| if RectIsct(pr,er) then | |
| -- collision between player and ent | |
| HandlePlrColl(e) | |
| elseif ar and RectIsct(ar,er) then | |
| -- ent hit by player attack | |
| HandleDamage(e,DMG.MELEE) | |
| end | |
| end | |
| end | |
| function CheckEndLvl() | |
| local t=LvlTileAtPt( | |
| Plr.x+C//2,Plr.y+C//2) | |
| if t==T.GATE_L or t==T.GATE_R or | |
| t==T.GATE_L2 or t==T.GATE_R2 then | |
| EndLvl() | |
| end | |
| end | |
| function EndLvl() | |
| Game.topLvl=Max( | |
| Game.topLvl,Game.lvlNo) | |
| SetMode(M.EOL) | |
| end | |
| function AdvanceLvl() | |
| -- save game if we should | |
| if Game.lvl.save then | |
| pmem(0,Max(pmem(0) or 0, | |
| Game.lvlNo+1)) | |
| end | |
| if Game.lvlNo>=#LVL then | |
| -- end of game. | |
| SetMode(M.WIN) | |
| else | |
| -- go back to map. | |
| SetMode(M.WLD) | |
| end | |
| end | |
| -- handle collision w/ given ent | |
| function HandlePlrColl(e) | |
| for _,b in pairs(e.beh) do | |
| if b.coll then b.coll(e) end | |
| if e.dead then break end | |
| end | |
| end | |
| function HandleDamage(e,dtype) | |
| for _,b in pairs(e.beh) do | |
| if b.dmg then b.dmg(e,dtype) end | |
| if e.dead then | |
| SpawnParts(PFX.POP,e.x+4,e.y+4,e.clr) | |
| Snd(SND.KILL) | |
| break | |
| end | |
| end | |
| end | |
| function HandlePlrHurt() | |
| if Plr.invuln>0 then return end | |
| if Plr.plane==0 and Plr.super then | |
| Snd(SND.HURT) | |
| Plr.super=false | |
| Plr.invuln=100 | |
| Plr.drag=Iif(Plr.dx>=0,-10,10) | |
| Plr.jmp=0 | |
| else | |
| StartDying() | |
| end | |
| end | |
| function Snd(spec) | |
| if not Sett.snd then return end | |
| sfx(spec.sfxid,spec.note,spec.dur, | |
| 0,spec.vol or 15,spec.speed or 0) | |
| end | |
| function PlayMus(musid) | |
| if Sett.mus or musid==-1 then | |
| music(musid) | |
| end | |
| end | |
| --------------------------------------- | |
| -- PARTICLES | |
| --------------------------------------- | |
| -- possible effects | |
| PFX={ | |
| POP={ | |
| rad=4, | |
| count=15, | |
| speed=4, | |
| fall=true, | |
| ttl=15 | |
| }, | |
| FW={ -- fireworks | |
| rad=3, | |
| count=40, | |
| speed=1, | |
| fall=false, | |
| ttl=100 | |
| } | |
| } | |
| -- fx=one of the effects in PFX | |
| -- cx,cy=center, clr=the color | |
| function SpawnParts(fx,cx,cy,clr) | |
| for i=1,fx.count do | |
| local r=Rnd01()*fx.rad | |
| local phi=Rnd01()*math.pi*2 | |
| local part={ | |
| x=cx+r*Cos(phi), | |
| y=cy+r*Sin(phi), | |
| vx=fx.speed*Cos(phi), | |
| vy=fx.speed*Sin(phi), | |
| fall=fx.fall, | |
| ttl=fx.ttl, | |
| age=0, | |
| clr=clr | |
| } | |
| Ins(Parts,part) | |
| end | |
| end | |
| function UpdateParts() | |
| -- iterate backwards so we can delete | |
| for i=#Parts,1,-1 do | |
| local p=Parts[i] | |
| p.age=p.age+1 | |
| if p.age>=p.ttl then | |
| -- delete | |
| Rem(Parts,i) | |
| else | |
| p.x=p.x+p.vx | |
| p.y=p.y+p.vy+(p.fall and p.age//2 or 0) | |
| end | |
| end | |
| end | |
| function RendParts() | |
| for i,p in pairs(Parts) do | |
| pix(p.x-Game.scr,p.y,p.clr) | |
| end | |
| end | |
| --------------------------------------- | |
| -- WLD MAP | |
| --------------------------------------- | |
| -- convert "World W-L" into index | |
| function Wl(w,l) return (w-1)*3+l end | |
| -- Init world (runs once at start of app). | |
| function WldInit() | |
| for r=0,ROWS-1 do | |
| for c=0,COLS-1 do | |
| local t=WldFgTile(c,r) | |
| local lval=WldLvlVal(t) | |
| if t==T.META_A then | |
| -- player start pos | |
| Wld.plr.x0=c*C | |
| Wld.plr.y0=(r-1)*C | |
| elseif t==T.META_B then | |
| local mv=WldGetTag(c,r) | |
| -- savegame start pos | |
| Wld.spos[Wl(mv,1)]={ | |
| x=c*C,y=(r-1)*C} | |
| elseif lval>0 then | |
| local mv=WldGetTag(c,r) | |
| -- It's a level tile. | |
| local poi={c=c,r=r, | |
| t=POI.LVL,lvl=Wl(mv,lval)} | |
| Ins(Wld.pois,poi) | |
| end | |
| end | |
| end | |
| end | |
| -- Looks around tc,tr for a numeric tag. | |
| function WldGetTag(tc,tr) | |
| for r=tr-1,tr+1 do | |
| for c=tc-1,tc+1 do | |
| local mv=MetaVal(WldFgTile(c,r),0) | |
| if mv>0 then | |
| return mv | |
| end | |
| end | |
| end | |
| trace("No WLD tag @"..tc..","..tr) | |
| return 0 | |
| end | |
| -- Returns the value (1, 2, 3) of a WLD | |
| -- level tile. | |
| function WldLvlVal(t) | |
| return Iif4(t==S.WLD.LVLF,1, | |
| t==S.WLD.LVL1,1, | |
| t==S.WLD.LVL2,2, | |
| t==S.WLD.LVL3,3,0) | |
| end | |
| function WldFgTile(c,r) | |
| return MapPageTile(WLD.FPAGE,c,r) | |
| end | |
| function WldBgTile(c,r) | |
| return MapPageTile(WLD.BPAGE,c,r) | |
| end | |
| function WldPoiAt(c,r) | |
| for i=1,#Wld.pois do | |
| local poi=Wld.pois[i] | |
| if poi.c==c and poi.r==r then | |
| return poi | |
| end | |
| end | |
| return nil | |
| end | |
| function WldHasRoadAt(c,r) | |
| local t=WldFgTile(c,r) | |
| for i=1,#S.WLD.ROADS do | |
| if S.WLD.ROADS[i]==t then | |
| return true | |
| end | |
| end | |
| return false | |
| end | |
| function WldUpdate() | |
| local p=Wld.plr -- shorthand | |
| if p.dx~=0 or p.dy~=0 then | |
| -- Just move. | |
| p.x=p.x+p.dx | |
| p.y=p.y+p.dy | |
| if p.x%C==0 and p.y%C==0 then | |
| -- reached destination. | |
| p.ldx=p.dx | |
| p.ldy=p.dy | |
| p.dx=0 | |
| p.dy=0 | |
| end | |
| return | |
| end | |
| if btn(0) then WldTryMove(0,-1) end | |
| if btn(1) then WldTryMove(0,1) end | |
| if btn(2) then WldTryMove(-1,0) end | |
| if btn(3) then WldTryMove(1,0) end | |
| Wld.plr.flipped=Iif( | |
| Iif(Wld.plr.flipped,btn(3),btn(2)), | |
| not Wld.plr.flipped, | |
| Wld.plr.flipped) -- wtf | |
| if btnp(4) then | |
| local poi=WldPoiAt(p.x//C,p.y//C) | |
| if poi and poi.lvl>Game.topLvl then | |
| if poi.lvl==1 then | |
| SetMode(M.TUT) | |
| else | |
| StartLvl(poi.lvl) | |
| end | |
| end | |
| end | |
| end | |
| function WldTryMove(dx,dy) | |
| local p=Wld.plr -- shorthand | |
| -- if we are in locked POI, we can only | |
| -- come back the way we came. | |
| local poi=WldPoiAt(p.x//C,p.y//C) | |
| if not Plr.dbgFly and poi and | |
| poi.lvl>Game.topLvl and | |
| (dx ~= -p.ldx or dy ~= -p.ldy) then | |
| return | |
| end | |
| -- target row,col | |
| local tc=p.x//C+dx | |
| local tr=p.y//C+dy | |
| if WldHasRoadAt(tc,tr) or | |
| WldPoiAt(tc,tr) then | |
| -- Destination is a road or level. | |
| -- Move is valid. | |
| p.dx=dx | |
| p.dy=dy | |
| return | |
| end | |
| end | |
| function WldFgRemapFunc(t) | |
| return t<T.FIRST_META and t or 0 | |
| end | |
| function WldRend() | |
| if Game.m~=M.WLD then return end | |
| cls(2) | |
| rect(0,SCRH-8,SCRW,8,0) | |
| local fp=MapPageStart(WLD.FPAGE) | |
| local bp=MapPageStart(WLD.BPAGE) | |
| -- render map bg | |
| map(bp.c,bp.r,COLS,ROWS,0,0,0,1) | |
| -- render map fg, excluding markers | |
| map(fp.c,fp.r,COLS,ROWS,0,0,0,1, | |
| WldFgRemapFunc) | |
| -- render the "off" version of level | |
| -- tiles on top of cleared levels. | |
| for _,poi in pairs(Wld.pois) do | |
| if poi.lvl<=Game.topLvl then | |
| spr(S.WLD.LVLC,poi.c*C,poi.r*C,0) | |
| end | |
| end | |
| print("SELECT LEVEL TO PLAY", | |
| 70,10) | |
| print("= MOVE",34,SCRH-6) | |
| print("= ENTER LEVEL",98,SCRH-6) | |
| RendSpotFx(Wld.plr.x//C, | |
| Wld.plr.y//C,Game.t) | |
| if 0==(Game.t//16)%2 then | |
| rectb(Wld.plr.x-3,Wld.plr.y-3,13,13,15) | |
| end | |
| spr(S.PLR.STAND,Wld.plr.x,Wld.plr.y,0, | |
| 1,Wld.plr.flipped and 1 or 0) | |
| RendHud() | |
| end | |
| --------------------------------------- | |
| -- LEVEL UNPACKING | |
| --------------------------------------- | |
| -- unpack modes | |
| UMODE={ | |
| GAME=0, -- unpack for gameplay | |
| EDIT=1, -- unpack for editing | |
| } | |
| function MapPageStart(pageNo) | |
| return {c=(pageNo%8)*30,r=(pageNo//8)*17} | |
| end | |
| function MapPageTile(pageNo,c,r,newVal) | |
| local pstart=MapPageStart(pageNo) | |
| if newVal then | |
| mset(c+pstart.c,r+pstart.r,newVal) | |
| end | |
| return mget(c+pstart.c,r+pstart.r) | |
| end | |
| -- Unpacked level is written to top 8 | |
| -- map pages (cells 0,0-239,16). | |
| function UnpackLvl(lvlNo,mode) | |
| local lvl=LVL[lvlNo] | |
| local start=MapPageStart(lvl.pkstart) | |
| local offc=start.c | |
| local offr=start.r | |
| local len=lvl.pklen*30 | |
| local endc=FindLvlEndCol(offc,offr,len) | |
| MapClear(0,0,LVL_LEN,ROWS) | |
| -- next output col | |
| local outc=0 | |
| -- for each col in packed map | |
| for c=offc,endc do | |
| local cmd=mget(c,offr) | |
| local copies=MetaVal(cmd,1) | |
| -- create that many copies of this col | |
| for i=1,copies do | |
| CreateCol(c,outc,offr,mode==UMODE.GAME) | |
| -- advance output col | |
| outc=outc+1 | |
| if outc>=LVL_LEN then | |
| trace("ERROR: level too long: "..lvlNo) | |
| return | |
| end | |
| end | |
| end | |
| -- if in gameplay, expand patterns and | |
| -- remove special markers | |
| -- (first META_A is player start pos) | |
| if mode==UMODE.GAME then | |
| for c=0,LVL_LEN-1 do | |
| for r=0,ROWS-1 do | |
| local t=mget(c,r) | |
| local tpat=TPAT[t] | |
| if tpat then ExpandTpat(tpat,c,r) end | |
| if Plr.x==0 and Plr.y==0 and | |
| t==T.META_A then | |
| -- player start position. | |
| Plr.x=c*C | |
| Plr.y=r*C | |
| end | |
| if t>=T.FIRST_META then | |
| mset(c,r,0) | |
| end | |
| end | |
| end | |
| if Plr.x==0 and Plr.y==0 then | |
| trace("*** start pos UNSET L"..lvlNo) | |
| end | |
| FillWater() | |
| SetUpTanims() | |
| end | |
| end | |
| -- expand tile pattern at c,r | |
| function ExpandTpat(tpat,c,r) | |
| local s=mget(c,r) | |
| for i=0,tpat.w-1 do | |
| for j=0,tpat.h-1 do | |
| mset(c+i,r+j,s+j*16+i) | |
| end | |
| end | |
| end | |
| -- Sets up tile animations. | |
| function SetUpTanims() | |
| for c=0,LVL_LEN-1 do | |
| for r=0,ROWS-1 do | |
| local t=mget(c,r) | |
| if TANIM[t] then | |
| TanimAdd(c,r) | |
| end | |
| end | |
| end | |
| end | |
| function FindLvlEndCol(c0,r0,len) | |
| -- iterate backwards until we find a | |
| -- non-empty col. | |
| for c=c0+len-1,c0,-1 do | |
| for r=r0,r0+ROWS-1 do | |
| if mget(c,r)>0 then | |
| -- rightmost non empty col | |
| return c | |
| end | |
| end | |
| end | |
| return c0 | |
| end | |
| function FillWater() | |
| -- We fill downward from surface tiles, | |
| -- Downward AND upward from water tiles. | |
| local surfs={} -- surface tiles | |
| local waters={} -- water tiles | |
| for c=LVL_LEN-1,0,-1 do | |
| for r=ROWS-1,0,-1 do | |
| if mget(c,r)==T.SURF then | |
| Ins(surfs,{c=c,r=r}) | |
| elseif mget(c,r)==T.WATER then | |
| Ins(waters,{c=c,r=r}) | |
| end | |
| end | |
| end | |
| for i=1,#surfs do | |
| local s=surfs[i] | |
| -- fill water below this tile | |
| FillWaterAt(s.c,s.r,1) | |
| end | |
| for i=1,#waters do | |
| local s=waters[i] | |
| -- fill water above AND below this tile | |
| FillWaterAt(s.c,s.r,-1) | |
| FillWaterAt(s.c,s.r,1) | |
| end | |
| end | |
| -- Fill water starting (but not including) | |
| -- given tile, in the given direction | |
| -- (1:down, -1:up) | |
| function FillWaterAt(c,r0,dir) | |
| local from=r0+dir | |
| local to=Iif(dir>0,ROWS-1,0) | |
| for r=from,to,dir do | |
| if mget(c,r)==T.EMPTY then | |
| mset(c,r,T.WATER) | |
| else | |
| return | |
| end | |
| end | |
| end | |
| function TanimAdd(c,r) | |
| if Tanims[c] then | |
| Ins(Tanims[c],r) | |
| else | |
| Tanims[c]={r} | |
| end | |
| end | |
| -- pack lvl from 0,0-239,16 to the packed | |
| -- level area of the indicated level | |
| function PackLvl(lvlNo) | |
| local lvl=LVL[lvlNo] | |
| local start=MapPageStart(lvl.pkstart) | |
| local outc=start.c | |
| local outr=start.r | |
| local len=lvl.pklen*30 | |
| local endc=FindLvlEndCol(0,0,LVL_LEN) | |
| -- pack | |
| local reps=0 | |
| MapClear(outc,outr,len,ROWS) | |
| for c=0,endc do | |
| if c>0 and MapColsEqual(c,c-1,0) and | |
| reps<12 then | |
| -- increment repeat marker on prev col | |
| local m=mget(outc-1,outr) | |
| m=Iif(m==0,T.META_NUM_0+2,m+1) | |
| mset(outc-1,outr,m) | |
| reps=reps+1 | |
| else | |
| reps=1 | |
| -- copy col to packed level | |
| MapCopy(c,0,outc,outr,1,ROWS) | |
| outc=outc+1 | |
| if outc>=start.c+len then | |
| trace("Capacity exceeded.") | |
| return false | |
| end | |
| end | |
| end | |
| trace("packed "..(endc+1).." -> ".. | |
| (outc+1-start.c)) | |
| return true | |
| end | |
| -- Create map col (dstc,0)-(dstc,ROWS-1) | |
| -- from source col located at | |
| -- (srcc,offr)-(srcc,offr+ROWS-1). | |
| -- if ie, instantiates entities. | |
| function CreateCol(srcc,dstc,offr,ie) | |
| -- copy entire column first | |
| MapCopy(srcc,offr,dstc,0,1,ROWS) | |
| mset(dstc,0,T.EMPTY) -- top cell is empty | |
| if not ie then return end | |
| -- instantiate entities | |
| for r=1,ROWS-1 do | |
| local t=mget(dstc,r) | |
| if t>=T.FIRST_ENT and EBT[t] then | |
| -- entity tile: create entity | |
| mset(dstc,r,T.EMPTY) | |
| EntAdd(t,dstc*C,r*C) | |
| end | |
| end | |
| end | |
| --------------------------------------- | |
| -- RENDERING | |
| --------------------------------------- | |
| function Rend() | |
| RendBg() | |
| if Game.snow then RendSnow() end | |
| RendMap() | |
| RendTanims() | |
| RendEnts() | |
| RendToasts() | |
| if Game.m==M.EOL then RendScrim() end | |
| RendPlr() | |
| RendParts() | |
| RendHud() | |
| RendSign() | |
| end | |
| function RendBg() | |
| local END_R=ROWS | |
| cls(Game.lvl.bg) | |
| local offset=Game.scr//2+50 | |
| -- If i is a col# of mountains (starting | |
| -- at index 1), then its screen pos | |
| -- sx=(i-1)*C-off | |
| -- Solving for i, i=1+(sx+off)/C | |
| -- so at the left of screen, sx=0, we | |
| -- have i=1+off/C | |
| local startI=Max(1,1+offset//C) | |
| local endI=Min( | |
| startI+COLS,#Game.bgmnt) | |
| for i=startI,endI do | |
| local sx=(i-1)*C-offset | |
| local part=Game.bgmnt[i] | |
| for r=part.y,END_R do | |
| local spid=Iif(r==part.y, | |
| S.BGMNT.DIAG,S.BGMNT.FULL) | |
| spr(spid,(i-1)*C-offset,r*C,0,1, | |
| Iif(part.dy>0,1,0)) | |
| end | |
| end | |
| end | |
| function RendSnow() | |
| local dx=-Game.scr | |
| local dy=Game.t//2 | |
| for _,p in pairs(Game.snow) do | |
| local sx=((p.x+dx)%SCRW+SCRW)%SCRW | |
| local sy=((p.y+dy)%SCRH+SCRH)%SCRH | |
| pix(sx,sy,Game.lvl.snow.clr) | |
| end | |
| end | |
| function RendToasts() | |
| for i=#Toasts,1,-1 do | |
| local t=Toasts[i] | |
| t.ttl=t.ttl-1 | |
| if t.ttl<=0 then | |
| Toasts[i]=Toasts[#Toasts] | |
| Rem(Toasts) | |
| else | |
| t.y=t.y-1 | |
| spr(t.sp1,t.x-Game.scr,t.y,0) | |
| spr(t.sp2,t.x-Game.scr+C,t.y,0) | |
| end | |
| end | |
| end | |
| function RendMap() | |
| -- col c is rendered at | |
| -- sx=-Game.scr+c*C | |
| -- Setting sx=0 and solving for c | |
| -- c=Game.scr//C | |
| local c=Game.scr//C | |
| local sx=-Game.scr+c*C | |
| local w=Min(COLS+1,LVL_LEN-c) | |
| if c<0 then | |
| sx=sx+C*(-c) | |
| c=0 | |
| end | |
| map( | |
| -- col,row,w,h | |
| c,0,w,ROWS, | |
| -- sx,sy,colorkey,scale | |
| sx,0,0,1) | |
| end | |
| function RendPlr() | |
| local spid | |
| local walking=false | |
| if Plr.plane>0 then | |
| RendPlane() | |
| return | |
| end | |
| if Plr.dying>0 then | |
| spid=S.PLR.DIE | |
| elseif Plr.atk>0 then | |
| spid= | |
| ATK_SEQ[Plr.atk]==1 and S.PLR.SWING | |
| or S.PLR.HIT | |
| elseif Plr.grounded then | |
| if btn(2) or btn(3) then | |
| spid=S.PLR.WALK1+time()%2 | |
| walking=true | |
| else | |
| spid=S.PLR.STAND | |
| end | |
| elseif Plr.swim then | |
| spid=S.PLR.SWIM1+(Game.t//4)%2 | |
| else | |
| spid=S.PLR.JUMP | |
| end | |
| local sx=Plr.x-Game.scr | |
| local sy=Plr.y | |
| local flip=Plr.flipped and 1 or 0 | |
| -- apply dying animation | |
| if spid==S.PLR.DIE then | |
| if Plr.dying<=#DIE_SEQ then | |
| sx=sx+DIE_SEQ[Plr.dying][1] | |
| sy=sy+DIE_SEQ[Plr.dying][2] | |
| else | |
| sx=-1000 | |
| sy=-1000 | |
| end | |
| end | |
| -- if invulnerable, blink | |
| if Plr.invuln>0 and | |
| 0==(Game.t//4)%2 then return end | |
| spr(spid,sx,sy,0,1,flip) | |
| -- extra sprite for attack states | |
| if spid==S.PLR.SWING then | |
| spr(S.PLR.SWING_C,sx,sy-C,0,1,flip) | |
| elseif spid==S.PLR.HIT then | |
| spr(S.PLR.HIT_C, | |
| sx+(Plr.flipped and -C or C), | |
| sy,0,1,flip) | |
| end | |
| -- draw super panda overlay if player | |
| -- has the super panda powerup | |
| if Plr.super then | |
| local osp=Iif3(Plr.atk>0,S.PLR.SUPER_F, | |
| Plr.swim and not Plr.grounded, | |
| S.PLR.SUPER_S, | |
| walking,S.PLR.SUPER_P,S.PLR.SUPER_F) | |
| spr(osp,sx,Plr.y,0,1,flip) | |
| end | |
| -- draw overlays (blinking bamboo and | |
| -- yellow body) if powerup | |
| if spid~=S.PLR.SWING and Plr.firePwup>0 | |
| and (time()//128)%2==0 then | |
| spr(S.PLR.FIRE_BAMBOO,sx,Plr.y,0,1,flip) | |
| end | |
| if Plr.firePwup>100 or | |
| 1==(Plr.firePwup//16)%2 then | |
| local osp=Iif3(Plr.atk>0,S.PLR.FIRE_F, | |
| Plr.swim and not Plr.grounded, | |
| S.PLR.FIRE_S, | |
| walking,S.PLR.FIRE_P,S.PLR.FIRE_F) | |
| spr(osp,sx,Plr.y,0,1,flip) | |
| end | |
| -- if just respawned, highlight player | |
| if Game.m==M.PLAY and Plr.dying==0 and | |
| Plr.respX>=0 and Game.t<100 and | |
| (Game.t//8)%2==0 then | |
| rectb(Plr.x-Game.scr-2,Plr.y-2, | |
| C+4,C+4,15) | |
| end | |
| end | |
| function RendPlane() | |
| local ybias=(Game.t//8)%2==0 and 1 or 0 | |
| local sx=Plr.x-Game.scr | |
| spr(S.AVIATOR, | |
| sx-C,Plr.y+ybias,0,1,0,0,3,2) | |
| local spid=(Game.t//4)%2==0 and | |
| S.AVIATOR_PROP_1 or S.AVIATOR_PROP_2 | |
| spr(spid,sx+C, | |
| Plr.y+ybias+4,0) | |
| end | |
| function RendHud() | |
| rect(0,0,SCRW,C,3) | |
| if Plr.scoreDisp.value~=Plr.score then | |
| Plr.scoreDisp.value=Plr.score | |
| Plr.scoreDisp.text=Lpad(Plr.score,6) | |
| end | |
| local clr=15 | |
| print(Plr.scoreDisp.text,192,1,clr,true) | |
| print((Game.m==M.WLD and | |
| "WORLD MAP" or | |
| ("LEVEL "..Game.lvl.name)),95,1,7) | |
| spr(S.PLR.STAND,5,0,0) | |
| print("x "..Plr.lives,16,1) | |
| if Plr.plane>0 then | |
| local barw=PLANE.FUEL_BAR_W | |
| local lx=120-barw//2 | |
| local y=8 | |
| local clr=(Plr.plane<800 and | |
| (Game.t//16)%2==0) and 6 or 14 | |
| local clrLo=(clr==14 and 4 or clr) | |
| print("E",lx-7,y,clr) | |
| print("F",lx+barw+1,y,14) | |
| rectb(lx,y,barw,6,clrLo) | |
| local bw=Plr.plane* | |
| (PLANE.FUEL_BAR_W-2)//PLANE.MAX_FUEL | |
| rect(lx+1,y+1,Max(bw,1),4,clr) | |
| pix(lx+barw//4,y+4,clrLo) | |
| pix(lx+barw//2,y+4,clrLo) | |
| pix(lx+barw//2,y+3,clrLo) | |
| pix(lx+3*barw//4,y+4,clrLo) | |
| end | |
| end | |
| function RendEnts() | |
| for i=1,#Ents do | |
| local e=Ents[i] | |
| local sx=e.x-Game.scr | |
| if sx>-C and sx<SCRW then | |
| spr(e.sprite,sx,e.y,0,1, | |
| e.flipped and 1 or 0) | |
| end | |
| end | |
| end | |
| function RendScrim(sp) | |
| sp=sp or Iif3(Game.t>45,0, | |
| Game.t>30,S.SCRIM+2, | |
| Game.t>15,S.SCRIM+1, | |
| S.SCRIM) | |
| for r=0,ROWS-1 do | |
| for c=0,COLS-1 do | |
| spr(sp,c*C,r*C,15) | |
| end | |
| end | |
| end | |
| -- Render spotlight effect. | |
| -- fc,fr: cell at center of effect | |
| -- t: clock (ticks) | |
| function RendSpotFx(fc,fr,t) | |
| local rad=Max(0,t//2-2) -- radius | |
| if rad>COLS then return end | |
| for r=0,ROWS-1 do | |
| for c=0,COLS-1 do | |
| local d=Max(Abs(fc-c), | |
| Abs(fr-r)) | |
| local sa=d-rad -- scrim amount | |
| local spid=Iif2(sa<=0,-1,sa<=3, | |
| S.SCRIM+sa-1,0) | |
| if spid>=0 then | |
| spr(spid,c*C,r*C,15) | |
| end | |
| end | |
| end | |
| end | |
| function RendSign() | |
| if 0==Plr.signC then return end | |
| local w=Plr.signC*20 | |
| local h=Plr.signC*3 | |
| local x=SCRW//2-w//2 | |
| local y=SCRH//2-h//2-20 | |
| local s=SIGN_MSGS[Plr.signMsg] | |
| rect(x,y,w,h,15) | |
| if Plr.signC==SIGN_C_MAX then | |
| print(s.l1,x+6,y+8,0) | |
| print(s.l2,x+6,y+8+C,0) | |
| end | |
| end | |
| -- Rend tile animations | |
| function RendTanims() | |
| local c0=Max(0,Game.scr//C) | |
| local cf=c0+COLS | |
| for c=c0,cf do | |
| local anims=Tanims[c] | |
| if anims then | |
| for i=1,#anims do | |
| local r=anims[i] | |
| local tanim=TANIM[mget(c,r)] | |
| if tanim then | |
| local spid=tanim[ | |
| 1+(Game.t//16)%#tanim] | |
| spr(spid,c*C-Game.scr,r*C) | |
| end | |
| end | |
| end | |
| end | |
| end | |
| -------------------------------------- | |
| -- ENTITY BEHAVIORS | |
| --------------------------------------- | |
| -- move hit modes: what happens when | |
| -- entity hits something solid. | |
| MOVE_HIT={ | |
| NONE=0, | |
| STOP=1, | |
| BOUNCE=2, | |
| DIE=3, | |
| } | |
| -- aim mode | |
| AIM={ | |
| NONE=0, -- just shoot in natural | |
| -- direction of projectile | |
| HORIZ=1, -- adjust horizontal vel to | |
| -- go towards player | |
| VERT=2, -- adjust vertical vel to go | |
| -- towards player | |
| FULL=3, -- adjust horiz/vert to aim | |
| -- at player | |
| } | |
| -- moves horizontally | |
| -- moveDen: every how many ticks to move | |
| -- moveDx: how much to move | |
| -- moveHitMode: what to do on wall hit | |
| -- noFall: if true, flip instead of falling | |
| function BehMove(e) | |
| if e.moveT>0 then e.moveT=e.moveT-1 end | |
| if e.moveT==0 then return end | |
| if e.moveWaitPlr>0 then | |
| if Abs(Plr.x-e.x)>e.moveWaitPlr then | |
| return | |
| else e.moveWaitPlr=0 end | |
| end | |
| e.moveNum=e.moveNum+1 | |
| if e.moveNum<e.moveDen then return end | |
| e.moveNum=0 | |
| if e.noFall and | |
| EntWouldFall(e,e.x+e.moveDx) then | |
| -- flip rather than fall | |
| e.moveDx=-e.moveDx | |
| e.flipped=e.moveDx>0 | |
| elseif e.moveHitMode==MOVE_HIT.NONE or | |
| EntCanMove(e,e.x+e.moveDx,e.y) then | |
| e.x=e.x+(e.moveDx or 0) | |
| e.y=e.y+(e.moveDy or 0) | |
| elseif e.moveHitMode==MOVE_HIT.BOUNCE | |
| then | |
| e.moveDx=-(e.moveDx or 0) | |
| e.flipped=e.moveDx>0 | |
| elseif e.moveHitMode==MOVE_HIT.DIE then | |
| e.dead=true | |
| end | |
| end | |
| -- Moves up/down. | |
| -- e.yamp: amplitude | |
| function BehUpDownInit(e) | |
| e.maxy=e.y | |
| e.miny=e.maxy-e.yamp | |
| end | |
| function BehUpDown(e) | |
| e.ynum=e.ynum+1 | |
| if e.ynum<e.yden then return end | |
| e.ynum=0 | |
| e.y=e.y+e.dy | |
| if e.y<=e.miny then e.dy=1 end | |
| if e.y>=e.maxy then e.dy=-1 end | |
| end | |
| function BehFacePlr(e) | |
| e.flipped=Plr.x>e.x | |
| if e.moveDx then | |
| e.moveDx=Abs(e.moveDx)* | |
| (e.flipped and 1 or -1) | |
| end | |
| end | |
| -- automatically flips movement | |
| -- flipDen: every how many ticks to flip | |
| function BehFlip(e) | |
| e.flipNum=e.flipNum+1 | |
| if e.flipNum<e.flipDen then return end | |
| e.flipNum=0 | |
| e.flipped=not e.flipped | |
| e.moveDx=(e.moveDx and -e.moveDx or 0) | |
| end | |
| function BehJump(e) | |
| if e.jmp==0 then | |
| e.jmpNum=e.jmpNum+1 | |
| if e.jmpNum<e.jmpDen or | |
| not e.grounded then return end | |
| e.jmpNum=0 | |
| e.jmp=1 | |
| else | |
| -- continue jump | |
| e.jmp=e.jmp+1 | |
| if e.jmp>#JUMP_DY then | |
| -- end jump | |
| e.jmp=0 | |
| else | |
| local dy=JUMP_DY[e.jmp] | |
| if EntCanMove(e,e.x,e.y+dy) then | |
| e.y=e.y+dy | |
| else | |
| e.jmp=0 | |
| end | |
| end | |
| end | |
| end | |
| function BehFall(e) | |
| e.grounded=not EntCanMove(e,e.x,e.y+1) | |
| if not e.grounded and e.jmp==0 then | |
| e.y=e.y+1 | |
| end | |
| end | |
| function BehTakeDmg(e,dtype) | |
| if not ArrayContains(e.dtypes,dtype) then | |
| return | |
| end | |
| e.hp=e.hp-1 | |
| if e.hp>0 then return end | |
| e.dead=true | |
| -- drop loot? | |
| local roll=Rnd(0,99) | |
| -- give bonus probability to starting | |
| -- levels (decrease roll value) | |
| roll=Max(Iif2(Game.lvlNo<2,roll-50, | |
| Game.lvlNo<4,roll-25,roll),0) | |
| if roll<e.lootp then | |
| local i=Rnd(1,#e.loot) | |
| i=Min(Max(i,1),#e.loot) | |
| local l=EntAdd(e.loot[i],e.x,e.y-4) | |
| EntAddBeh(l,BE.MOVE) | |
| ShallowMerge(l,{moveDy=-1,moveDx=0, | |
| moveDen=1,moveT=8}) | |
| end | |
| end | |
| function BehPoints(e) | |
| e.dead=true | |
| AddScore(e.value or 50,e.x+4,e.y-4) | |
| end | |
| function BehHurt(e) | |
| HandlePlrHurt() | |
| end | |
| function BehLiftInit(e) | |
| -- lift top and bottom y: | |
| local a=C*FetchTile( | |
| T.META_A,e.x//C) | |
| local b=C*FetchTile( | |
| T.META_B,e.x//C) | |
| if a>b then | |
| e.boty=a | |
| e.topy=b | |
| e.dir=1 | |
| else | |
| e.topy=a | |
| e.boty=b | |
| e.dir=-1 | |
| end | |
| e.coll=CR.FULL | |
| end | |
| function BehLift(e) | |
| e.liftNum=e.liftNum+1 | |
| if e.liftNum<e.liftDen then return end | |
| e.liftNum=0 | |
| e.y=e.y+e.dir | |
| if e.dir>0 and e.y>e.boty or | |
| e.dir<0 and e.y<e.topy then | |
| e.dir=-e.dir | |
| end | |
| end | |
| function BehLiftColl(e) | |
| -- Lift hit player. Just nudge the player | |
| Plr.nudgeY=Iif(e.y>Plr.y,-1,1) | |
| end | |
| function BehShootInit(e) | |
| e.shootNum=Rnd(0,e.shootDen-1) | |
| end | |
| function BehShoot(e) | |
| e.shootNum=e.shootNum+1 | |
| if e.shootNum<30 then | |
| e.sprite=e.shootSpr or e.sprite | |
| end | |
| if e.shootNum<e.shootDen then return end | |
| e.shootNum=0 | |
| local shot=EntAdd( | |
| e.shootEid or EID.FIREBALL,e.x,e.y) | |
| e.sprite=e.shootSpr or e.sprite | |
| shot.moveDx= | |
| Iif(shot.moveDx==nil,0,shot.moveDx) | |
| shot.moveDy= | |
| Iif(shot.moveDy==nil,0,shot.moveDy) | |
| if e.aim==AIM.HORIZ then | |
| shot.moveDx=(Plr.x>e.x and 1 or -1)* | |
| Abs(shot.moveDx) | |
| elseif e.aim==AIM.VERT then | |
| shot.moveDy=(Plr.y>e.y and 1 or -1)* | |
| Abs(shot.moveDy) | |
| elseif e.aim==AIM.FULL then | |
| local tx=Plr.x-shot.x | |
| local ty=Plr.y-shot.y | |
| local mag=math.sqrt(tx*tx+ty*ty) | |
| local spd=math.sqrt( | |
| shot.moveDx*shot.moveDx+ | |
| shot.moveDy*shot.moveDy) | |
| shot.moveDx=math.floor(0.5+tx*spd/mag) | |
| shot.moveDy=math.floor(0.5+ty*spd/mag) | |
| if shot.moveDx==0 and shot.moveDy==0 then | |
| shot.moveDx=-1 | |
| end | |
| end | |
| end | |
| function BehCrumble(e) | |
| if not e.crumbling then | |
| -- check if player on tile | |
| if Plr.x<e.x-8 then return end | |
| if Plr.x>e.x+8 then return end | |
| -- check if player is standing on it | |
| local pr=RectXLate( | |
| GetPlrCr(),Plr.x,Plr.y) | |
| local er=RectXLate(e.coll,e.x,e.y-1) | |
| e.crumbling=RectIsct(pr,er) | |
| end | |
| if e.crumbling then | |
| -- count down to destruction | |
| e.cd=e.cd-1 | |
| e.sprite=Iif(e.cd>66,S.CRUMBLE, | |
| Iif(e.cd>33,S.CRUMBLE_2,S.CRUMBLE_3)) | |
| if e.cd<0 then e.dead=true end | |
| end | |
| end | |
| function BehTtl(e) | |
| e.ttl=e.ttl-1 | |
| if e.ttl <= 0 then e.dead = true end | |
| end | |
| function BehDmgEnemy(e) | |
| local fr=RectXLate(FIRE.COLL,e.x,e.y) | |
| for i=1,#Ents do | |
| local ent=Ents[i] | |
| local er=RectXLate(ent.coll,ent.x,ent.y) | |
| if e~=ent and RectIsct(fr,er) and | |
| EntHasBeh(ent,BE.VULN) then | |
| -- ent hit by player fire | |
| HandleDamage(ent,Plr.plane>0 and | |
| DMG.PLANE_FIRE or DMG.FIRE) | |
| e.dead=true | |
| end | |
| end | |
| end | |
| function BehGrantFirePwupColl(e) | |
| Plr.firePwup=FIRE.DUR | |
| e.dead=true | |
| Snd(SND.PWUP) | |
| end | |
| function BehGrantSuperPwupColl(e) | |
| Plr.super=true | |
| e.dead=true | |
| Snd(SND.PWUP) | |
| end | |
| function BehReplace(e) | |
| local d=Abs(e.x-Plr.x) | |
| if d<e.replDist then | |
| EntRepl(e,e.replEid,e.replData) | |
| end | |
| end | |
| function BehChestInit(e) | |
| -- ent on top of chest is the contents | |
| local etop=GetEntAt(e.x,e.y-C) | |
| if etop then | |
| e.cont=etop.eid | |
| etop.dead=true | |
| else | |
| e.cont=S.FOOD.LEAF | |
| end | |
| -- check multiplier | |
| e.mul=MetaVal( | |
| mget(e.x//C,e.y//C-2),1) | |
| e.open=false | |
| end | |
| function BehChestDmg(e) | |
| if e.open then return end | |
| SpawnParts(PFX.POP,e.x+4,e.y+4,14) | |
| Snd(SND.OPEN); | |
| e.anim=nil | |
| e.sprite=S.CHEST_OPEN | |
| e.open=true | |
| local by=e.y-C | |
| local ty=e.y-2*C | |
| local lx=e.x-C | |
| local cx=e.x | |
| local rx=e.x+C | |
| local c=e.cont | |
| EntAdd(c,cx,by) | |
| if e.mul>1 then EntAdd(c,cx,ty) end | |
| if e.mul>2 then EntAdd(c,lx,by) end | |
| if e.mul>3 then EntAdd(c,rx,by) end | |
| if e.mul>4 then EntAdd(c,lx,ty) end | |
| if e.mul>5 then EntAdd(c,rx,ty) end | |
| end | |
| function BehTplafInit(e) | |
| e.phase=MetaVal(FetchEntTag(e),0) | |
| end | |
| function BehTplaf(e) | |
| local UNIT=40 -- in ticks | |
| local PHASE_LEN=3 -- in units | |
| local uclk=e.phase+Game.t//UNIT | |
| local open=((uclk//PHASE_LEN)%2==0) | |
| local tclk=e.phase*UNIT+Game.t | |
| e.sprite=Iif2( | |
| (tclk%(UNIT*PHASE_LEN)<=6), | |
| S.TPLAF_HALF,open,S.TPLAF,S.TPLAF_OFF) | |
| e.sol=Iif(open,SOL.HALF,SOL.NOT) | |
| end | |
| function BehDashInit(e) | |
| assert(EntHasBeh(e,BE.MOVE)) | |
| e.origAnim=e.anim | |
| e.origMoveDen=e.moveDen | |
| end | |
| function BehDash(e) | |
| local dashing=e.cdd<e.ddur | |
| e.cdd=(e.cdd+1)%e.cdur | |
| if dashing then | |
| e.anim=e.dashAnim or e.origAnim | |
| e.moveDen=e.origMoveDen | |
| else | |
| e.anim=e.origAnim | |
| e.moveDen=99999 -- don't move | |
| end | |
| end | |
| function BehSignInit(e) | |
| e.msg=MetaVal(FetchEntTag(e),0) | |
| end | |
| function BehSignColl(e) | |
| Plr.signMsg=e.msg | |
| -- if starting to read sign, lock player | |
| -- for a short while | |
| if Plr.signC==0 then | |
| Plr.locked=100 | |
| end | |
| -- increase cycle counter by 2 because | |
| -- it gets decreased by 1 every frame | |
| Plr.signC=Min(Plr.signC+2, | |
| SIGN_C_MAX) | |
| end | |
| function BehOneUp(e) | |
| e.dead=true | |
| Plr.lives=Plr.lives+1 | |
| Snd(SND.ONEUP) | |
| end | |
| function BehFlag(e) | |
| local rx=e.x+C | |
| if Plr.respX<rx then | |
| Plr.respX=rx | |
| Plr.respY=e.y | |
| end | |
| Snd(SND.PWUP) | |
| EntRepl(e,EID.FLAG_T) | |
| end | |
| function BehReplOnGnd(e) | |
| if e.grounded then | |
| EntRepl(e,e.replEid,e.replData) | |
| end | |
| end | |
| function BehPop(e) | |
| e.dead=true | |
| SpawnParts(PFX.POP,e.x+4,e.y+4,e.clr) | |
| end | |
| function BehBoardPlane(e) | |
| e.dead=true | |
| Plr.plane=PLANE.START_FUEL | |
| Plr.y=e.y-3*C | |
| Snd(SND.PLANE) | |
| end | |
| function BehFuel(e) | |
| e.dead=true | |
| Plr.plane=Plr.plane+PLANE.FUEL_INC | |
| Snd(SND.PWUP) | |
| end | |
| --------------------------------------- | |
| -- ENTITY BEHAVIORS | |
| --------------------------------------- | |
| BE={ | |
| MOVE={ | |
| data={ | |
| -- move denominator (moves every | |
| -- this many frames) | |
| moveDen=5, | |
| moveNum=0, -- numerator, counts up | |
| -- 1=moving right, -1=moving left | |
| moveDx=-1, | |
| moveDy=0, | |
| moveHitMode=MOVE_HIT.BOUNCE, | |
| -- if >0, waits until player is less | |
| -- than this dist away to start motion | |
| moveWaitPlr=0, | |
| -- if >=0, how many ticks to move | |
| -- for (after that, stop). | |
| moveT=-1, | |
| }, | |
| update=BehMove, | |
| }, | |
| FALL={ | |
| data={grounded=false,jmp=0}, | |
| update=BehFall, | |
| }, | |
| FLIP={ | |
| data={flipNum=0,flipDen=20}, | |
| update=BehFlip, | |
| }, | |
| FACEPLR={update=BehFacePlr}, | |
| JUMP={ | |
| data={jmp=0,jmpNum=0,jmpDen=50}, | |
| update=BehJump, | |
| }, | |
| VULN={ -- can be damaged by player | |
| data={hp=1, | |
| -- damage types that can hurt this. | |
| dtypes={DMG.MELEE,DMG.FIRE,DMG.PLANE_FIRE}, | |
| -- loot drop probability (0-100) | |
| lootp=0, | |
| -- possible loot to drop (EIDs) | |
| loot={EID.FOOD.A}, | |
| }, | |
| dmg=BehTakeDmg, | |
| }, | |
| SHOOT={ | |
| data={shootNum=0,shootDen=100, | |
| aim=AIM.NONE}, | |
| init=BehShootInit, | |
| update=BehShoot, | |
| }, | |
| UPDOWN={ | |
| -- yamp is amplitude of y movement | |
| data={yamp=16,dy=-1,yden=3,ynum=0}, | |
| init=BehUpDownInit, | |
| update=BehUpDown, | |
| }, | |
| POINTS={ | |
| data={value=50}, | |
| coll=BehPoints, | |
| }, | |
| HURT={ -- can hurt player | |
| coll=BehHurt | |
| }, | |
| LIFT={ | |
| data={liftNum=0,liftDen=3}, | |
| init=BehLiftInit, | |
| update=BehLift, | |
| coll=BehLiftColl, | |
| }, | |
| CRUMBLE={ | |
| -- cd: countdown to crumble | |
| data={cd=50,coll=CR.FULL,crumbling=false}, | |
| update=BehCrumble, | |
| }, | |
| TTL={ -- time to live (auto destroy) | |
| data={ttl=150}, | |
| update=BehTtl, | |
| }, | |
| DMG_ENEMY={ -- damage enemies | |
| update=BehDmgEnemy, | |
| }, | |
| GRANT_FIRE={ | |
| coll=BehGrantFirePwupColl, | |
| }, | |
| REPLACE={ | |
| -- replaces by another ent when plr near | |
| -- replDist: distance from player | |
| -- replEid: EID to replace by | |
| data={replDist=50,replEid=EID.LEAF}, | |
| update=BehReplace, | |
| }, | |
| CHEST={ | |
| init=BehChestInit, | |
| dmg=BehChestDmg, | |
| }, | |
| TPLAF={ | |
| init=BehTplafInit, | |
| update=BehTplaf, | |
| }, | |
| DASH={ | |
| data={ | |
| ddur=20, -- dash duration | |
| cdur=60, -- full cycle duration | |
| cdd=0, -- cycle counter | |
| }, | |
| init=BehDashInit, | |
| update=BehDash, | |
| }, | |
| GRANT_SUPER={ | |
| coll=BehGrantSuperPwupColl, | |
| }, | |
| SIGN={ | |
| init=BehSignInit, | |
| coll=BehSignColl | |
| }, | |
| ONEUP={coll=BehOneUp}, | |
| FLAG={coll=BehFlag}, | |
| REPL_ON_GND={ | |
| -- replace EID when grounded | |
| -- replData -- extra data to add to | |
| data={replEid=EID.LEAF}, | |
| update=BehReplOnGnd | |
| }, | |
| POP={update=BehPop}, | |
| PLANE={coll=BehBoardPlane}, | |
| FUEL={coll=BehFuel}, | |
| } | |
| --------------------------------------- | |
| -- ENTITY BEHAVIOR TABLE | |
| --------------------------------------- | |
| EBT={ | |
| [EID.EN.SLIME]={ | |
| data={ | |
| hp=1,moveDen=3,clr=11,noFall=true, | |
| lootp=20,loot={EID.FOOD.A}, | |
| }, | |
| beh={BE.MOVE,BE.FALL,BE.VULN,BE.HURT}, | |
| }, | |
| [EID.EN.HSLIME]={ | |
| data={replDist=50,replEid=EID.EN.SLIME}, | |
| beh={BE.REPLACE}, | |
| }, | |
| [EID.EN.A]={ | |
| data={ | |
| hp=1,moveDen=5,clr=14,flipDen=120, | |
| lootp=30, | |
| loot={EID.FOOD.A,EID.FOOD.B}, | |
| }, | |
| beh={BE.MOVE,BE.JUMP,BE.FALL,BE.VULN, | |
| BE.HURT,BE.FLIP}, | |
| }, | |
| [EID.EN.B]={ | |
| data={ | |
| hp=1,moveDen=5,clr=13, | |
| lootp=30, | |
| loot={EID.FOOD.A,EID.FOOD.B, | |
| EID.FOOD.C}, | |
| }, | |
| beh={BE.JUMP,BE.FALL,BE.VULN,BE.HURT, | |
| BE.FACEPLR}, | |
| }, | |
| [EID.EN.DEMON]={ | |
| data={hp=1,moveDen=5,clr=7, | |
| aim=AIM.HORIZ, | |
| shootEid=EID.FIREBALL, | |
| shootSpr=S.EN.DEMON_THROW, | |
| lootp=60, | |
| loot={EID.FOOD.C,EID.FOOD.D}}, | |
| beh={BE.JUMP,BE.FALL,BE.SHOOT, | |
| BE.HURT,BE.FACEPLR,BE.VULN}, | |
| }, | |
| [EID.EN.SDEMON]={ | |
| data={hp=1,moveDen=5,clr=7, | |
| flipDen=50, | |
| shootEid=EID.SNOWBALL, | |
| shootSpr=S.EN.SDEMON_THROW, | |
| aim=AIM.HORIZ, | |
| lootp=75, | |
| loot={EID.FOOD.C,EID.FOOD.D}}, | |
| beh={BE.JUMP,BE.FALL,BE.SHOOT, | |
| BE.MOVE,BE.FLIP,BE.VULN,BE.HURT}, | |
| }, | |
| [EID.EN.PDEMON]={ | |
| data={hp=1,clr=11,flipDen=50, | |
| shootEid=EID.PLASMA, | |
| shootSpr=S.EN.PDEMON_THROW, | |
| aim=AIM.FULL, | |
| lootp=80, | |
| loot={EID.FOOD.D}}, | |
| beh={BE.JUMP,BE.FALL,BE.SHOOT, | |
| BE.FLIP,BE.VULN,BE.HURT}, | |
| }, | |
| [EID.EN.BAT]={ | |
| data={hp=1,moveDen=2,clr=9,flipDen=60, | |
| lootp=40, | |
| loot={EID.FOOD.A,EID.FOOD.B}}, | |
| beh={BE.MOVE,BE.FLIP,BE.VULN,BE.HURT}, | |
| }, | |
| [EID.EN.FISH]={ | |
| data={ | |
| hp=1,moveDen=3,clr=9,flipDen=120, | |
| lootp=40, | |
| loot={EID.FOOD.A,EID.FOOD.B}, | |
| }, | |
| beh={BE.MOVE,BE.FLIP,BE.VULN, | |
| BE.HURT}, | |
| }, | |
| [EID.EN.FISH2]={ | |
| data={hp=1,clr=12,moveDen=1, | |
| lootp=60, | |
| loot={EID.FOOD.B,EID.FOOD.C}}, | |
| beh={BE.MOVE,BE.DASH,BE.VULN,BE.HURT}, | |
| }, | |
| [EID.FIREBALL]={ | |
| data={hp=1,moveDen=2,clr=7, | |
| coll=CR.BALL, | |
| moveHitMode=MOVE_HIT.DIE}, | |
| beh={BE.MOVE,BE.HURT,BE.TTL}, | |
| }, | |
| [EID.PLASMA]={ | |
| data={hp=1,moveDen=2,clr=7, | |
| moveDx=2, | |
| coll=CR.BALL, | |
| moveHitMode=MOVE_HIT.NONE}, | |
| beh={BE.MOVE,BE.HURT,BE.TTL}, | |
| }, | |
| [EID.SNOWBALL]={ | |
| data={hp=1,moveDen=1,clr=15, | |
| coll=CR.BALL, | |
| moveHitMode=MOVE_HIT.DIE}, | |
| beh={BE.MOVE,BE.FALL,BE.VULN,BE.HURT}, | |
| }, | |
| [EID.LIFT]={ | |
| data={sol=SOL.FULL}, | |
| beh={BE.LIFT}, | |
| }, | |
| [EID.CRUMBLE]={ | |
| data={ | |
| sol=SOL.FULL,clr=14, | |
| -- only take melee and plane fire dmg | |
| dtypes={DMG.MELEE,DMG.PLANE_FIRE}, | |
| }, | |
| beh={BE.CRUMBLE,BE.VULN}, | |
| }, | |
| [EID.PFIRE]={ | |
| data={ | |
| moveDx=1,moveDen=1,ttl=80, | |
| moveHitMode=MOVE_HIT.DIE, | |
| coll=FIRE.COLL, | |
| }, | |
| beh={BE.MOVE,BE.TTL,BE.DMG_ENEMY}, | |
| }, | |
| [EID.FIRE_PWUP]={ | |
| beh={BE.GRANT_FIRE}, | |
| }, | |
| [EID.SPIKE]={ | |
| data={coll=CR.FULL}, | |
| beh={BE.HURT}, | |
| }, | |
| [EID.CHEST]={ | |
| data={coll=CR.FULL, | |
| sol=SOL.FULL}, | |
| beh={BE.CHEST}, | |
| }, | |
| [EID.TPLAF]={ | |
| data={sol=SOL.HALF, | |
| coll=CR.TOP}, | |
| beh={BE.TPLAF}, | |
| }, | |
| [EID.EN.DASHER]={ | |
| data={hp=1,clr=12,moveDen=1,noFall=true, | |
| dashAnim={S.EN.DASHER,315}, | |
| lootp=60, | |
| loot={EID.FOOD.B,EID.FOOD.C}}, | |
| beh={BE.MOVE,BE.DASH,BE.VULN,BE.HURT}, | |
| }, | |
| [EID.EN.VBAT]={ | |
| data={hp=1,clr=14,yden=2, | |
| lootp=40, | |
| loot={EID.FOOD.B,EID.FOOD.C}}, | |
| beh={BE.UPDOWN,BE.VULN,BE.HURT}, | |
| }, | |
| [EID.SUPER_PWUP]={beh={BE.GRANT_SUPER}}, | |
| [EID.SIGN]={beh={BE.SIGN}}, | |
| [EID.FLAG]={beh={BE.FLAG}}, | |
| [EID.ICICLE]={ | |
| data={replEid=EID.ICICLE_F,replDist=8}, | |
| beh={BE.REPLACE}, | |
| }, | |
| [EID.ICICLE_F]={ | |
| data={replEid=EID.POP,replData={clr=15}}, | |
| beh={BE.FALL,BE.HURT,BE.REPL_ON_GND} | |
| }, | |
| [EID.SICICLE]={ | |
| data={replEid=EID.SICICLE_F,replDist=8}, | |
| beh={BE.REPLACE}, | |
| }, | |
| [EID.SICICLE_F]={ | |
| data={replEid=EID.POP,replData={clr=14}}, | |
| beh={BE.FALL,BE.HURT,BE.REPL_ON_GND} | |
| }, | |
| [EID.POP]={beh={BE.POP}}, | |
| [EID.PLANE]={beh={BE.PLANE}}, | |
| [EID.FUEL]={beh={BE.FUEL}}, | |
| [EID.FOOD.LEAF]={ | |
| data={value=50,coll=CR.FOOD}, | |
| beh={BE.POINTS}}, | |
| [EID.FOOD.A]={ | |
| data={value=100,coll=CR.FOOD}, | |
| beh={BE.POINTS}}, | |
| [EID.FOOD.B]={ | |
| data={value=200,coll=CR.FOOD}, | |
| beh={BE.POINTS}}, | |
| [EID.FOOD.C]={ | |
| data={value=500,coll=CR.FOOD}, | |
| beh={BE.POINTS}}, | |
| [EID.FOOD.D]={ | |
| data={value=1000,coll=CR.FOOD}, | |
| beh={BE.POINTS}}, | |
| } | |
| --------------------------------------- | |
| -- DEBUG MENU | |
| --------------------------------------- | |
| function DbgTic() | |
| if Plr.dbgResp then | |
| cls(1) | |
| print(Plr.dbgResp) | |
| if btnp(4) then | |
| Plr.dbgResp=nil | |
| end | |
| return | |
| end | |
| Game.dbglvl=Game.dbglvl or 1 | |
| if btnp(3) then | |
| Game.dbglvl=Iif(Game.dbglvl+1>#LVL,1,Game.dbglvl+1) | |
| elseif btnp(2) then | |
| Game.dbglvl=Iif(Game.dbglvl>1,Game.dbglvl-1,#LVL) | |
| end | |
| local menu={ | |
| {t="(Close)",f=DbgClose}, | |
| {t="Warp to test lvl",f=DbgWarpTest}, | |
| {t="Warp to L"..Game.dbglvl,f=DbgWarp}, | |
| {t="End lvl",f=DbgEndLvl}, | |
| {t="Grant super pwup",f=DbgSuper}, | |
| {t="Fly mode ".. | |
| Iif(Plr.dbgFly,"OFF","ON"),f=DbgFly}, | |
| {t="Invuln mode ".. | |
| Iif(Plr.invuln and Plr.invuln>0, | |
| "OFF","ON"), | |
| f=DbgInvuln}, | |
| {t="Unpack L"..Game.dbglvl,f=DbgUnpack}, | |
| {t="Pack L"..Game.dbglvl,f=DbgPack}, | |
| {t="Clear PMEM",f=DbgPmem}, | |
| {t="Win the game",f=DbgWin}, | |
| {t="Lose the game",f=DbgLose}, | |
| } | |
| cls(5) | |
| print("DEBUG") | |
| rect(110,0,140,16,11) | |
| print("DBG LVL:",120,4,3) | |
| print(LVL[Game.dbglvl].name,170,4) | |
| Plr.dbgSel=Plr.dbgSel or 1 | |
| for i=1,#menu do | |
| print(menu[i].t,10,10+i*10, | |
| Plr.dbgSel==i and 15 or 0) | |
| end | |
| if btnp(0) then | |
| Plr.dbgSel=Iif(Plr.dbgSel>1, | |
| Plr.dbgSel-1,#menu) | |
| elseif btnp(1) then | |
| Plr.dbgSel=Iif(Plr.dbgSel<#menu, | |
| Plr.dbgSel+1,1) | |
| elseif btnp(4) then | |
| (menu[Plr.dbgSel].f)() | |
| end | |
| end | |
| function DbgClose() Plr.dbg=false end | |
| function DbgSuper() Plr.super=true end | |
| function DbgEndLvl() | |
| EndLvl() | |
| Plr.dbg=false | |
| end | |
| function DbgPmem() pmem(0,0) end | |
| function DbgWarp() | |
| StartLvl(Game.dbglvl) | |
| end | |
| function DbgWarpNext() | |
| StartLvl(Game.lvlNo+1) | |
| end | |
| function DbgWarpTest() | |
| StartLvl(#LVL) | |
| end | |
| function DbgUnpack() | |
| UnpackLvl(Game.dbglvl,UMODE.EDIT) | |
| sync() | |
| Plr.dbgResp="Unpacked & synced L"..Game.dbglvl | |
| end | |
| function DbgPack() | |
| local succ=PackLvl(Game.dbglvl) | |
| --MapClear(0,0,LVL_LEN,ROWS) | |
| sync() | |
| Plr.dbgResp=Iif(succ, | |
| "Packed & synced L"..Game.dbglvl, | |
| "** ERROR packing L"..Game.dbglvl) | |
| end | |
| function DbgFly() | |
| Plr.dbgFly=not Plr.dbgFly | |
| Plr.dbgResp="Fly mode "..Iif(Plr.dbgFly, | |
| "ON","OFF") | |
| end | |
| function DbgInvuln() | |
| Plr.invuln=Iif(Plr.invuln>0,0,9999999) | |
| Plr.dbgResp="Invuln mode "..Iif( | |
| Plr.invuln>0,"ON","OFF") | |
| end | |
| function DbgWin() | |
| SetMode(M.WIN) | |
| Plr.dbg=false | |
| end | |
| function DbgLose() | |
| SetMode(M.GAMEOVER) | |
| Plr.dbg=false | |
| end | |
| --------------------------------------- | |
| -- UTILITIES | |
| --------------------------------------- | |
| function Iif(cond,t,f) | |
| if cond then return t else return f end | |
| end | |
| function Iif2(cond,t,cond2,t2,f2) | |
| if cond then return t end | |
| return Iif(cond2,t2,f2) | |
| end | |
| function Iif3(cond,t,cond2,t2,cond3,t3,f3) | |
| if cond then return t end | |
| return Iif2(cond2,t2,cond3,t3,f3) | |
| end | |
| function Iif4(cond,t,cond2,t2,cond3,t3, | |
| cond4,t4,f4) | |
| if cond then return t end | |
| return Iif3(cond2,t2,cond3,t3,cond4,t4,f4) | |
| end | |
| function ArrayContains(a,val) | |
| for i=1,#a do | |
| if a[i]==val then return true end | |
| end | |
| return false | |
| end | |
| function Lpad(value, width) | |
| local s=value.."" | |
| while string.len(s) < width do | |
| s="0"..s | |
| end | |
| return s | |
| end | |
| function RectXLate(r,dx,dy) | |
| return {x=r.x+dx,y=r.y+dy,w=r.w,h=r.h} | |
| end | |
| -- rects have x,y,w,h | |
| function RectIsct(r1,r2) | |
| return | |
| r1.x+r1.w>r2.x and r2.x+r2.w>r1.x and | |
| r1.y+r1.h>r2.y and r2.y+r2.h>r1.y | |
| end | |
| function DeepCopy(t) | |
| if type(t)~="table" then return t end | |
| local r={} | |
| for k,v in pairs(t) do | |
| if type(v)=="table" then | |
| r[k]=DeepCopy(v) | |
| else | |
| r[k]=v | |
| end | |
| end | |
| return r | |
| end | |
| -- if preserve, fields that already exist | |
| -- in the target won't be overwritten | |
| function ShallowMerge(target,src, | |
| preserve) | |
| if not src then return end | |
| for k,v in pairs(src) do | |
| if not preserve or not target[k] then | |
| target[k]=DeepCopy(src[k]) | |
| end | |
| end | |
| end | |
| function MapCopy(sc,sr,dc,dr,w,h) | |
| for r=0,h-1 do | |
| for c=0,w-1 do | |
| mset(dc+c,dr+r,mget(sc+c,sr+r)) | |
| end | |
| end | |
| end | |
| function MapClear(dc,dr,w,h) | |
| for r=0,h-1 do | |
| for c=0,w-1 do | |
| mset(dc+c,dr+r,0) | |
| end | |
| end | |
| end | |
| function MapColsEqual(c1,c2,r) | |
| for i=0,ROWS-1 do | |
| if mget(c1,r+i)~=mget(c2,r+i) then | |
| return false | |
| end | |
| end | |
| return true | |
| end | |
| function MetaVal(t,deflt) | |
| return Iif( | |
| t>=T.META_NUM_0 and t<=T.META_NUM_0+12, | |
| t-T.META_NUM_0,deflt) | |
| end | |
| -- finds marker m on column c of level | |
| -- return row of marker, -1 if not found | |
| function FetchTile(m,c,nowarn) | |
| for r=0,ROWS-1 do | |
| if LvlTile(c,r)==m then | |
| if erase then SetLvlTile(c,r,0) end | |
| return r | |
| end | |
| end | |
| if not nowarn then | |
| trace("Marker not found "..m.." @"..c) | |
| end | |
| return -1 | |
| end | |
| -- Gets the entity's "tag marker", | |
| -- that is the marker tile that's sitting | |
| -- just above it. Also erases it. | |
| -- If no marker found, returns 0 | |
| function FetchEntTag(e) | |
| local t=mget(e.x//C,e.y//C-1) | |
| if t>=T.FIRST_META then | |
| mset(e.x//C,e.y//C-1,0) | |
| return t | |
| else | |
| return 0 | |
| end | |
| end | |
| function Max(x,y) return math.max(x,y) end | |
| function Min(x,y) return math.min(x,y) end | |
| function Abs(x,y) return math.abs(x,y) end | |
| function Rnd(lo,hi) return math.random(lo,hi) end | |
| function Rnd01() return math.random() end | |
| function RndSeed(s) return math.randomseed(s) end | |
| function Ins(tbl,e) return table.insert(tbl,e) end | |
| function Rem(tbl,e) return table.remove(tbl,e) end | |
| function Sin(a) return math.sin(a) end | |
| function Cos(a) return math.cos(a) end | |