├── .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 | 
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 | 
19 |
20 | Each isle has its distinct look and feel, with different challenges and
21 | enemies:
22 |
23 | 
24 |
25 | 
26 |
27 | 
28 |
29 | 
30 |
31 | 
32 |
33 | 
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 |
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 =~ /^/ and system("cat head.inc.html");
8 | $line =~ /^ 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.xMAX_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.y0,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=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 dispx1000 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=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 sx45,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.moveNum0
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.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#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 rollb 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.liftNum0 and e.y>e.boty or
2510 | e.dir<0 and e.yPlr.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.shootNume.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.xe.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 d1 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.cdd0, 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 |
--------------------------------------------------------------------------------