├── .gitignore ├── LICENSE.txt ├── README.md ├── export ├── body.inc.html ├── end.inc.html ├── exportcart.sh ├── head.inc.html └── inject_html_snippets.pl ├── images ├── 2x │ ├── play1.png │ ├── play2.png │ ├── play3.png │ ├── play4.png │ ├── play5.png │ ├── play6.png │ ├── title.png │ └── world.png └── 4x │ ├── play1.png │ ├── play2.png │ ├── play3.png │ ├── play4.png │ ├── play5.png │ ├── play6.png │ ├── title.png │ └── world.png └── source ├── panda.lua ├── panda.tic └── run_tic.sh /.gitignore: -------------------------------------------------------------------------------- 1 | tic.exe 2 | panda-export.tic 3 | panda.html 4 | cartridge.html 5 | *.swp 6 | 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Bruno Oliveira 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 8 Bit Panda 2 | A retro-style panda platformer game. 3 | 4 | Copyright © 2017 Bruno Oliveira - brunotc@gmail.com 5 | 6 | Follow me on Twitter: [@btco_code](http://twitter.com/btco_code) 7 | 8 | ![Title screen](https://github.com/btco/panda/blob/master/images/2x/title.png?raw=true) 9 | 10 | 8 Bit Panda is a classic platformer for the [TIC-80](http://tic.computer) 11 | fantasy console. You can move, run, jump and get powerups. It has 17 levels 12 | distributed across 6 major areas (isles). 13 | 14 | You can play it online here: 15 | * [TIC-80 site](https://tic.computer/play?cart=188) 16 | * [itch.io page](https://btco.itch.io/8-bit-panda) 17 | 18 | ![World map screen](https://github.com/btco/panda/blob/master/images/2x/world.png?raw=true) 19 | 20 | Each isle has its distinct look and feel, with different challenges and 21 | enemies: 22 | 23 | ![Gameplay screenshot 1](https://github.com/btco/panda/blob/master/images/2x/play1.png?raw=true) 24 | 25 | ![Gameplay screenshot 2](https://github.com/btco/panda/blob/master/images/2x/play2.png?raw=true) 26 | 27 | ![Gameplay screenshot 3](https://github.com/btco/panda/blob/master/images/2x/play3.png?raw=true) 28 | 29 | ![Gameplay screenshot 4](https://github.com/btco/panda/blob/master/images/2x/play4.png?raw=true) 30 | 31 | ![Gameplay screenshot 5](https://github.com/btco/panda/blob/master/images/2x/play5.png?raw=true) 32 | 33 | ![Gameplay screenshot 6](https://github.com/btco/panda/blob/master/images/2x/play6.png?raw=true) 34 | 35 | The source code is written in LUA and the game art was entirely created 36 | using the TIC-80 editor, including music. 37 | 38 | If you'd like to know more details about how the game works and the development process, [this article](https://medium.com/@btco_code/writing-a-platformer-for-the-tic-80-virtual-console-6fa737abe476) may interest you. 39 | 40 | # License 41 | 42 | This software is distributed under the Apache License, version 2.0. 43 | 44 | Copyright (c) 2017 Bruno Oliveira 45 | 46 | Licensed under the Apache License, Version 2.0 (the "License"); 47 | you may not use this file except in compliance with the License. 48 | You may obtain a copy of the License at 49 | 50 | http://www.apache.org/licenses/LICENSE-2.0 51 | 52 | Unless required by applicable law or agreed to in writing, software 53 | distributed under the License is distributed on an "AS IS" BASIS, 54 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 55 | See the License for the specific language governing permissions and 56 | limitations under the License. 57 | 58 | -------------------------------------------------------------------------------- /export/body.inc.html: -------------------------------------------------------------------------------- 1 |

8 Bit Panda

2 | 3 | 13 | -------------------------------------------------------------------------------- /export/end.inc.html: -------------------------------------------------------------------------------- 1 |
2 | Copyright © Bruno Oliveira

3 | For more info:
4 | github.com/btco/panda 5 |
6 | -------------------------------------------------------------------------------- /export/exportcart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if uname | grep -iq darwin; then 4 | TIC='/Applications/tic.app/Contents/MacOS/tic' 5 | else 6 | TIC='../tic.exe' 7 | fi 8 | 9 | if ! [ -f "$TIC" ]; then 10 | echo "** not found: $TIC." 11 | exit 1 12 | fi 13 | 14 | echo "Creating export cartridge." 15 | cp -vf ../source/panda.tic panda-export.tic 16 | echo "Injecting LUA code." 17 | echo "panda-export.tic generated." 18 | echo 19 | echo "ATTENTION" 20 | echo "We will now launch TIC. In TIC, use the EXPORT command to generate HTML." 21 | echo "Call the file 'cartridge.html' and save to THIS DIRECTORY ($PWD)." 22 | echo "Press ENTER" 23 | read foo 24 | rm -vf panda.html 25 | "$TIC" panda-export.tic -code ../source/panda.lua 26 | 27 | if ! [ -f "cartridge.html" ]; then 28 | echo "*** cartridge.html not found." 29 | exit 1 30 | fi 31 | 32 | echo "Found cartridge.html." 33 | echo "Injecting our HTML snippets..." 34 | perl inject_html_snippets.pl panda.html 35 | 36 | echo "Generated panda.html." 37 | 38 | -------------------------------------------------------------------------------- /export/head.inc.html: -------------------------------------------------------------------------------- 1 | 2 | 8 Bit Panda 3 | 30 | -------------------------------------------------------------------------------- /export/inject_html_snippets.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | while ($line = <>) { 4 | $line =~ /^<\/body/ and system("cat end.inc.html"); 5 | $line =~ // and $line =~ /<\/title>/ and next; 6 | print $line; 7 | $line =~ /^<head>/ and system("cat head.inc.html"); 8 | $line =~ /^<body/ and system("cat body.inc.html"); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /images/2x/play1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/2x/play1.png -------------------------------------------------------------------------------- /images/2x/play2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/2x/play2.png -------------------------------------------------------------------------------- /images/2x/play3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/2x/play3.png -------------------------------------------------------------------------------- /images/2x/play4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/2x/play4.png -------------------------------------------------------------------------------- /images/2x/play5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/2x/play5.png -------------------------------------------------------------------------------- /images/2x/play6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/2x/play6.png -------------------------------------------------------------------------------- /images/2x/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/2x/title.png -------------------------------------------------------------------------------- /images/2x/world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/2x/world.png -------------------------------------------------------------------------------- /images/4x/play1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/4x/play1.png -------------------------------------------------------------------------------- /images/4x/play2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/4x/play2.png -------------------------------------------------------------------------------- /images/4x/play3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/4x/play3.png -------------------------------------------------------------------------------- /images/4x/play4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/4x/play4.png -------------------------------------------------------------------------------- /images/4x/play5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/4x/play5.png -------------------------------------------------------------------------------- /images/4x/play6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/4x/play6.png -------------------------------------------------------------------------------- /images/4x/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/4x/title.png -------------------------------------------------------------------------------- /images/4x/world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/images/4x/world.png -------------------------------------------------------------------------------- /source/panda.lua: -------------------------------------------------------------------------------- 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 3366 | 3367 | -------------------------------------------------------------------------------- /source/panda.tic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btco/panda/3de3ae6cd7696606b855845c5b0ebb8cacd99dcd/source/panda.tic -------------------------------------------------------------------------------- /source/run_tic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if uname | grep -iq darwin; then 4 | TIC_DIR="$HOME/Library/Application Support/com.nesbox.tic/TIC-80" 5 | CMD="open /Applications/tic.app" 6 | else 7 | TIC_DIR="/c/Users/bruno/AppData/Roaming/com.nesbox.tic/TIC-80" 8 | CMD="../tic.exe" 9 | fi 10 | 11 | echo "TIC cartridge dir: $TIC_DIR" 12 | echo "Copying panda.tic to $TIC_DIR/panda-dev.tic." 13 | 14 | cp -vf panda.tic "$TIC_DIR/panda-dev.tic" 15 | 16 | if ! [ -f "$TIC_DIR/panda-dev.tic" ]; then 17 | echo "*** Copy appears to have failed." 18 | exit 1 19 | fi 20 | 21 | echo "Starting TIC." 22 | echo "Use 'load panda-dev' to load the cart." 23 | $CMD 24 | 25 | echo -n "Done editing cart? Copy it back to working dir? [Y/n] " 26 | read ans 27 | if [ "$ans" = "Y" -o "$ans" = "y" -o "$ans" = "" ]; then 28 | echo "Copying it back..." 29 | cp -vf "$TIC_DIR/panda-dev.tic" panda.tic 30 | mv "$TIC_DIR/panda-dev.tic" "$TIC_DIR/panda-dev.bak" 31 | echo "Done copying." 32 | fi 33 | 34 | --------------------------------------------------------------------------------