forked from icegoat9/picohaven
-
Notifications
You must be signed in to change notification settings - Fork 1
/
picohaven_v11a.lua
2896 lines (2646 loc) · 97.6 KB
/
picohaven_v11a.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--PICOhaven v1.1a
--by icegoat, Aug '22
--(mostly under-the-hood changes from v1.0 release Oct '21)
--this file is run through a script to strip comments and
-- whitespace and then included by the main picohaven .p8 cart,
-- so these comments do not appear in the released cart
--see also picohaven_source.md as reference for
-- main state machine, sprite flags, and global variables
--instructions, updates, and web-playable version:
-- https://www.lexaloffle.com/bbs/?tid=45105
--this file and related files and dev notes are available in a
-- github repo: https://github.com/icegoat9/picohaven
---- Code organization:
----- 1) core game init/update/draw
----- 2) main game state machine
----- 3) pre-combat states (new turn, choose cards, etc)
----- 4) action/combat loop
----- 4a) enemy action loop
----- 4b) player action loop
----- 5) post-combat states (cleanup, etc)
----- 6) main UI draw loops and card draw functions
----- 7) custom sprite-based font and print functions
----- 8) menu-draw and related functions
----- 9) miscellaneous helper functions
----- x) pause menu items [deprecated]
----- 10) data string -> datastructure parsing + loading
----- 11) inits and databases
----- 12) profile / character sheet
----- 13) splash screen / intro
----- 14) levelup, upgrades
----- 15) town and retirement
----- 16) debugging + testing functions
----- 17) pathfinding (A*)
----- 18) load/save
-----
----- 1) core game init/update/draw functions, including animation queue
-----
function _init()
--debugmode=false
--logmsgq=true
--if (logmsgq) printh('\n*** new cart run ***','msgq.txt')
--godmode=true
dlvl=2 --starting dungeon #
initmspr()
initglobals()
initdbs()
initpersist()
initlevel()
--interpret pink (color 14) as transparent for sprites, black is not transparent
palt(0b0000000000000010)
music(1,1000)
changestate("splash")
end
function _draw()
--if wipe>0 a "screenwipe" is in progress, only update part of screen
local s=128-2*wipe
clip(wipe,wipe,s,s)
_drwstate() --different function depending on state, set by state's init fn
clip()
end
--lint:shake,msgreview,_updprev
function _update60()
if animt<1 then
--if a move/attack square-to-square animation is in progress,
-- only update that frame by frame until complete
_updonlyanim()
else
--normal update routine
shake=0 --turn off screenshake if it was on (e.g. due to "*2" mod card drawn)
--move killed enemies off-screen (but only once any attack animations
-- being processed via _updonlyanim() have completed)
-- (they will then be deleted from actor[] at end of turn)
for a in all(actor) do
if (a.hp<=0 and a!=p) a.x=-99
end
--check for entering msgreview state, potentially from many different states
-- (as of v1.00b, only one state actually has msgreviewenabled==true, so this is unnecessarily general)
if btnp(❎) and not msgreview and msgreviewenabled then
msgreview,_updprev,_updstate=true,_updstate,_updscrollmsg
else --TODO: is this else redundant, could instead always run _updstate()?
_updstate() --different function depending on state, set by state's init fn
end
end
_updtimers()
end
--regardless-of-state animation updates:
-- update global timer tick, animation frame, screenwipe, message scrolling
function _updtimers()
--common frame animation timer ticks
fram+=1
afram=flr(fram/act_td)%4
--if screenwipe in progress, continue it
wipe=max(0,wipe-5)
--every msg_td # of frames, scroll msgbox 1px
if fram % msg_td==0 and #msgq>4 and msg_yd<(#msgq-4)*6 and not msgreview then
msg_yd+=1
end
end
--run actor move/attack animations w/o user input until done
-- kicked off by setting common animation timer animt to 0
-- this function then gradually increases it 0->1 (=done)
function _updonlyanim()
animt=min(animt+animtd,1)
for a in all(actor) do
--pixel offsets to draw each sprite at relative to its 8*x,8*y starting location
a.ox,a.oy=a.sox*(1-animt),a.soy*(1-animt)
if animt==1 then
a.sox,a.soy=0,0
--delete ephemeral 'actors' that are not really player/enemy actors (e.g. "damage number" sprites)--
-- they were only created and added to actor[] to reuse this code to animate them
if (a.ephem) del(actor,a)
end
end
end
-----
----- 2) main game state machine
-----
--the core of the state machine is to call changestate() rather
-- then directly edit the 'state' variable. this function calls
-- a relevant init() function (which updates update and draw functions)
-- and resets some key globals to standard values to avoid need
-- to reset them in every state's init function
--lint: selx,sely,seln,msg_x0,msg_w,selvalid,showmapsel,msgreviewenabled
function changestate(_state,_wipe)
prevstate=state
state=_state
selvalid,showmapsel=false,false
selx,sely,seln=1,1,1
msgreviewenabled=false
--screen wipe on every state change, unless passed _wipe==0
wipe = _wipe or 63
--reset msgbox x + width to defaults
msg_x0,msg_w=0,map_w
--run specific init function defined in initglobals()
if (initfn[_state]) initfn[state]()
end
-- a simple wait-for-🅾️-to-continue loop used as update in various states
function _upd🅾️()
---if (showmapsel) selxy_update_clamped(10,10,0,0)
if (btnp(🅾️)) changestate(nextstate)
end
-----
----- 3) the "pre-combat" states
-----
---- state: new level
--lint: mapmsg
function initnewlevel()
initlevel()
--play theme music, though don't restart music if it's already playing from splash screen
if (prevstate!="splash") music(0)
mapmsg=pretxt[dlvl]
addmsg("\fc🅾️\f6:begin")
nextstate,_updstate,_drwstate="newturn",_upd🅾️,_drawlvltxt
end
--display the pre- or post-level story text in the map frame
--TODO? merge into drawmain (since similar) + use a global to set whether map or text is displayed
-- but: that would become less clear, might only save ~15tok
function _drawlvltxt()
clsrect(0)
drawstatus()
drawmapframe()
printwrap(mapmsg,21,4,10,6)
drawheadsup()
drawmsgbox()
end
---- state: new turn
function initnewturn()
clrmsg()
addmsg("\f7----- new round -----\narrows:inspect enemies\n\fc🅾️\f6:choose action cards")
selx,sely,showmapsel=p.x,p.y,true
_updstate,_drwstate=_updnewturn,_drawmain
end
function _updnewturn()
selxy_update_clamped(10,10,0,0) --11x11 map
if (btnp(🅾️)) changestate("choosecards")
end
--shared function used in many states to let player use
-- arrows to move selection box in x or y, clamped to an allowable range
function selxy_update_clamped(xmax,ymax,xmin,ymin)
--set default xmin,ymin values of 1 if not passed to save a
-- few tokens by omitting them in function calls (this is why they are
-- listed last as function parameters, so they'll default to nil if omitted)
--this approach is used widely in code to set default parameters
xmin,ymin = xmin or 1, ymin or 1
--loop checking which button is pressed
for i=1,4 do
if btnp(i-1) then
selx+=dirx[i]
sely+=diry[i]
break --only allow one button to be enabled at once, no "diagonal" moves
end
end
selx,sely=min(max(xmin,selx),xmax),min(max(ymin,sely),ymax)
--item #n in an x,y grid of items
--TODO?: also clamp seln to a max value? (not currently needed)
seln=(selx-1)*ymax+sely
end
---- state: choose cards
--lint: tpdeck
function initchoosecards()
--create a semi-local copy of pdeck (that adds the "rest"
-- and "confirm" virtual cards that aren't in deck and shouldn't
-- show up in character profile view of decklist)
tpdeck={}
for crd in all(pdeck) do
add(tpdeck,crd)
end
--add "long rest" card (see init fns)
--NOTE: hard-coded to be last entry in pdeckmaster[])
refresh(longrestcrd)
add(tpdeck,longrestcrd)
--add "confirm" option, implemented as a card
add(tpdeck,splt(";confirm;1;\nconfirm\n\n\f6confirm\nthe two\nselected\ncards"))
addmsg("select 2 cards to play\n(or rest+card to burn)\n\fc🅾️\f6:select\n\fc❎\f6:review map")
p.crds={}
_updstate,_drwstate=_updhand,_drawhand
end
--"selecting cards from hand" update function
function _updhand()
selxy_update_clamped(2,(#tpdeck+1)\2)
--if tpdeck has an odd number of cards, don't let selector move
-- to the unused (bottom of column 2) location
--TODO:build this into selxy_update_clamped() instead?
if (seln>#tpdeck) sely-=1
if btnp(🅾️) then
local selc=tpdeck[seln]
--card[3]=status (0=in hand, 1=discarded, 2=burned), see other comments and .md docs
if selc[3]==0 then
--card not discarded/burned, can select
if indextable(p.crds,selc) then
--card was already selected: deselect
del(p.crds,selc)
else
--select card
if selc[2]=="rest" then
--clear other selections
p.crds={}
end
if seln==#tpdeck then
--if last entry "confirm" selected, move ahead with card selection
-- NOTE: that "confirm" can only be selected if it is enabled
-- (card[3]==0) which is only set if 2 cards are selected
--set these cards to 'discarded' now even before we get to playing them
-- (so a "burn random undiscarded card to avoid damage"
-- trigger before player turn can't use them)
for c in all(p.crds) do
c[3]=1
end
pdeckbld(p.crds)
changestate("precombat")
elseif #p.crds<2 then
--if a new card is selected (and <2 already selected)
add(p.crds,selc)
if tutorialmode then
-- if (#p.crds==1) addmsg("\f7initiative\f6 will be \f7"..selc[1].."\f6.\n (low #s act earlier)\nnow select 2nd card.")
if (#p.crds==1) addmsg("now select 2nd card.")
if (#p.crds==2) addmsg("select \f7confirm\f6 if done.")
end
end
end
--enable "confirm" button if and only if 2 cards selected,
-- otherwise set it to "discarded" mode to grey it out
tpdeck[#tpdeck][3] = #p.crds==2 and 0 or 1
end
elseif btnp(❎) then
-- review map... by jumping back to newturn state
changestate("newturn")
end
end
function _drawhand()
clsrect(5)
print("\f6your deck:\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\*f \*7 \+fdlegend:",8,14)
--drawcard("in hand",94,99,0)
drawcard("discard",94,108,1)
drawcard("burned",94,118,2)
--rect(4,15,80,93,13)
--split deck into two columns to display
local tp1,tp2=splitarr(tpdeck)
drawcardsellists({tp1,tp2},0,19,p.crds,9)
--tip on initiative setting
-- if (#p.crds<1) printmspr("\f61st card chosen\nsets \f7initiative\f6\n for turn◆",67,4)
if (#p.crds<1) printmspr("\f61st card chosen\nsets \f7initiative,\f6\nlow:act first◆",61,3)
drawmsgbox()
end
--create list of options-for-turn to display on player
-- box in HUD, from selected cards
--lint: restburnmsg
function pdeckbld(clist)
--if first card is "rest", only play that and burn other
if clist[1][2]=="rest" then
--message will be displayed later in turn, when you play rest
restburnmsg="\f8burned\f6 ["..clist[2][2].."]"
clist[2][3]=2
deli(clist,2)
else
--add default alternate actions 😐2/█2 to options for turn
-- (unless certain items held that modify these)
add(clist,{0,hasitem("swift") and "😐3" or "😐2"})
add(clist,{0,hasitem("barbs") and "█2∧" or "█2"})
end
end
---- state: precombat
--lint: initi
function initprecombat()
--draw enemy cards for turn
selectenemyactions()
--ilist[]: global sorted-by-initiative list of actors
--initi: "who in ilist[] is acting next"?
ilist,initi=initiativelist(),1
-- --OBSOLETE section: removed to save tokens
-- --as tutorial, list actor initiatives
-- str="enemy actions drawn\n"
-- if tutorialmode then
-- str="initiatives for round:\n"
-- for i,il in ipairs(ilist) do
-- str..=" "..il[3]..":\+40"..il[1]
-- if (i!=#ilist) str..=","
-- if (i%2==0 or i==#ilist) str..="\n"
-- end
-- end
-- addmsg(str.."\fc🅾️\f6:continue to combat")
--TODO: decide if these tutorial tips are worth all the tokens / redundancy
addmsg("enemy actions drawn...")
if tutorialmode then
addmsg(ilist[1][3].." will act first\n with initiative \f7"..ilist[1][1])
end
addmsg("\fc🅾️\f6:continue to combat")
nextstate,_updstate,_drwstate="actloop",_upd🅾️,_drawmain
end
--draw random action card for each enemy type and set
-- relevant global variables to use this turn
function selectenemyactions()
local etypes=activeenemytypes()
for et in all(etypes) do
et.crds = rnd(enemydecks[et.id])
et.init = et.crds[1][1]
end
for a in all(actor) do
--add link to crds for each individual enemy
--TODO: rethink this and remove redundancy of both enemy type and enemy
-- having .init and .crds, but complicated by player not
-- having a .type (enemy type)
if (a.type) a.crds=a.type.crds
a.init=a.crds[1][1]
a.crdi=1 --index of card to play next for this enemy
end
end
--generate list of active enemy types (link to enemytypes[] entries)
-- from list of actors (only want one entry per type even if many instances of an enemy type)
function activeenemytypes()
local etypes={}
for a in all(actor) do
if (a!=p and not(indextable(etypes,a.type))) add(etypes,a.type)
end
return etypes
end
-----
----- 4) the "actor action" / combat states
-----
---- NOTE: see picohaven_source.md for a diagram of the
---- state machine: many interconnected actionloop states
---- general action loop state (which will step through each actor, enemy and player)
function initactloop()
_updstate=_updactloop
_drwstate=_drawmain --could comment out to save 3 tokens since hasn't changed since last state (but that's risky/brittle to future state flow changes)
end
--each time _updactloop() is called, it runs once and
-- dispatches to a specific player or enemy's action function (based on value of initi)
--lint: actorn
function _updactloop()
if p.hp<=0 then
loselevel()
return
end
if initi>#ilist then
--all actors have acted
changestate("cleanup",0)
return
end
actorn=ilist[initi][2] --current actor from ordered-by-initiative list
local a=actor[actorn]
initi+=1 --increment index to actor, for the _next_ time this function runs
if (a.hp<1) return --if actor dead, silently skip its turn
--NOTE: below tutorial note commented out to save ~20tokens
--if (tutorialmode) addmsg("@ initiative "..ilist[initi-1][1]..": "..a.name)
if a==p and p.crds[1]==longrestcrd then
--special case: long rests always run (even if stunned), w/o player interaction needed
longrest()
p.stun=nil --in case player stunned. TODO: move into longrest() perhaps?
elseif a.stun then
--skip turn if stunned
addmsg(a.name..": ▥, turn skipped")
a.stun=nil
else
if (a==p) then
changestate("actplayerpre",0)
else
changestate("actenemy",0)
end
end
end
-----
----- 4a) the enemy action loop states
-----
---- state: actenemy
function initactenemy()
_updstate=_updactenemy
_drwstate=_drawmain --could comment out to save 3 tokens since hasn't changed since last state (but that's risky/brittle to future state flow changes)
end
--execute one enemy action from enemy card
--(will increment global actor.crdi and run this multiple times if enemy has multiple actions)
function _updactenemy()
local e=actor[actorn]
--if all cards played, done, advance to next actor
if e.crdi>#e.crds then
changestate("actloop",0)
return
end
--generate current "card to play"'s data structure, set global e.crd
e.crd=parsecard(e.crds[e.crdi][2])
--advance index for next time
e.crdi+=1
if e.crd.act==sh("m") then
--if current action is a move, and enemy will have a ranged attack as the following action,
-- store that attack range so move can stop once it's within range
--NOTE: crdi below now refers to the 'next' card because of the +=1 above
if e.crdi<=#e.crds then
local nextcrd=parsecard(e.crds[e.crdi][2])
if (nextcrd.act==sh("a")) e.crd.rng=nextcrd.rng
end
end
runcard(e) --execute specific enemy action
end
-- actor "a" summons its summon (and loses 2 hp)
-- (written for only enemy summoning, but could be extended for player summons in future chapter of game)
function summon(a)
local smn=a.type.summon
local neighb=valid_emove_neighbors(a,true) --valid adjacent squares to summon into
if #neighb>0 then
local smnxy=rnd(neighb)
initenemy(smn,smnxy.x,smnxy.y)
addmsg(a.name.." \f8calls\f6 "..enemytype[smn].name.." (\f8-2♥\f6)")
--hard-coded that summoning always inflicts 2dmg to self
dmgactor(a,2)
end
end
-- moderately complex process of pathfinding for enemy moves (many sub-functions called)
function enemymoveastar(e)
--basic enemy A* move, trimmed to allowable move distance:
mvq = trimmv(pathfind(e,p),e.crd.val,e.crd.rng)
--if no motion would happen via the normal "can pass thorugh allies" routing,
-- enemy could be stuck behind one of its allies-- in this case, try routing
-- with "allies block motion" which may produce a useful "route around" behavior
if not mvq or #mvq<=1 then
mvq = trimmv(pathfind(e,p,false,true),e.crd.val,e.crd.rng)
end
--animate move until done (then will return to actenemy for next enemy action)
changestate("animmovestep",0)
end
--trim down an ideal unlimited-steps enemy move to a goal,
-- by stopping once either enemy is within
-- range (with LOS) or enemy has used up move rating for turn
function trimmv(_mvq,mvval,rng)
if (not _mvq) return _mvq
local trimto
for i,xy in ipairs(_mvq) do
local v=validmove(xy.x,xy.y,true)
if i==1 or v and i<=(mvval+1) then --equivalent to 'i==1 or (v and ...)'
trimto=i
--if xy is within range (1 unless ranged attack) and has LOS, trim here, skip rest of for loops
if (dst(xy,p)<=rng and pseudolos(xy,p)) break
end
end
return {unpack(_mvq,1,trimto)}
end
-- --WIP more complex pathfinding algorithm (on hold for lack of tokens,
-- -- and current draft is buggy)
-- --Plan A* moves to all four cells adjacent to player, determine which of these
-- -- moves is 'best' (if none are adjacent or in range of a ranged attack, which
-- -- partial move ends with the shortest path to the player in a future turn?)
--function enemymoveastaradvanced(e)
-- --bug: this routing allows enemy to move _through_ player to an open spot on other side
-- --minor bug: enemies don't always route around
-- --This is ~50 tokens more than a simpler single A* call
-- local potential_goals=valid_emove_neighbors(p,true)
-- bestdst,mvq=99,{}
-- for goal in all(potential_goals) do
-- local m=find_path(e,goal,dst,valid_emove_neighbors)
-- m=trimmv(m,e.crd.val,e.crd.rng)
-- if m then --if non-nil path returned
-- --how many steps would it take from this path's
-- -- endpoint to reach player in future?
-- local d=#find_path(m[#m],p,dst,valid_emove_neighbors)
-- if d<bestdst then
-- bestdst,mvq=d,m
-- end
-- end
-- end
-- changestate("animmovestep",0)
--end
-- general "valid move?" function for all actors
function validmove(x,y,endat,jmp,actorn,allyblocks)
--endat: if true, validate ending at this
-- spot (otherwise checking pass-through)
--jmp: jumping (can pass over some obstacles and enemies if not ending at this location)
--allyblocks: do enemies' allies block their movement?
-- (by default enemies can pass through though not end moves on allies)
--actorn: axtor[] index of moving actor (1: player)
--unjumpable obstacles (walls, fog)
if (fget(mget(x,y),1) or isfogoroffboard(x,y)) return false
--obstacle w/o jump (or even w/ jump can't end at)
if (fget(mget(x,y),2) and (endat or not jmp)) return false
--can't walk through actors, except enemies through their allies
-- or jumping past them
local ai=actorat(x,y)
--can't end on actor (except, actors can end of self i.e. 0 move)
if (endat and ai>0 and actorn!=ai) return false
--by default, enemies can pass through allies
-- (unless we pass the 'ally blocks moves' flag,
-- used to break out of some routing deadlocks)
if ((allyblocks or actorn==1) and ai>1 and not jmp) return false
return true
end
-- return list of "valid-move adjacent neighbors to (node.x,node.y)"
-- used in A* pathfind(), for example
function valid_emove_neighbors(node,endat,jmp,allyblocks)
--see parameter descriptions in validmove()
local neighbors = {}
for i=1,4 do
local tx,ty=node.x+dirx[i], node.y+diry[i]
if validmove(tx,ty,endat,jmp,nil,allyblocks) then
add(neighbors, xylst(tx,ty))
end
end
return neighbors
end
----wrapper to above allowing jmp, to pass to A* pathfind
----Note: moved to inline anonymous function since only used once in program
--function valid_emove_neighbors_jmp(node)
-- return valid_emove_neighbors(node,false,true)
--end
----wrapper allowing enemies to move through allies, for A* calls
----Note: moved to inline anonymous function since only used once in program
--function valid_emove_neighbors_allyblocks(node)
-- return valid_emove_neighbors(node,false,false,true)
--end
--execute attack described in attacker's card a.crd, against defender d
function runattack(a,d)
--a = attacker, d = defender (in actor[] list)
local crd=a.crd
--save values before modifier card drawn
local basestun,basewound=crd.stun,crd.wound
local dmg=crd.val
--draw attack mod card (currently player-only)
if a==p then
local mod=modcard()
addmsg("you draw modifier \f7"..mod)
if (tutorialmode) addmsg(" (custom dmg mod deck)")
if mod=="*2" then
dmg*=2
shufflemod()
--do something for emphasis (larger mod sprite with sspr? slow down
-- dmg animation? screen shake? all of the above?)
--tried many, commented out most. for now, just screen shake
shake=3 --screenshake of 3 pixels
-- animtd=0.03 --slow down animation (todo: reset elsewhere)
-- msg_td=99 --slow down msgbox (todo: reset elsewhere)
-- addmsg("") --add blank line
-- queueanim(nil,d.x,d.y-1,a.x,a.y-1,156) --draw a 'x2' cursor
elseif mod=="/2" then
dmg\=2
shufflemod()
else
--check for mod card conditions
if mod[-1]=="▥" then
crd.stun=true
mod=sub(mod,1,#mod-1)
elseif mod[-1]=="∧" then
crd.wound=true
mod=sub(mod,1,#mod-1)
end
--modify damage via mod
dmg+=tonum(mod)
end
end
-- below runs for all actors
sfx(12)
-- do damage and effects
local msg=a.name.." █ "..d.name..":"
if d==p and hasitem("shld",true) then
p.shld+=2
addmsg("\f7great shield used\f6: +★2")
end
if d.shld>0 then
msg..=dmg.."-"..d.shld.."★:"
dmg=max(0,dmg-d.shld)
end
msg..="\f8-"..dmg.."♥\f6"
if a.crd.stun then
msg..="▥"
d.stun=true
end
if a.crd.wound then
msg..="∧"
d.wound=true
end
--reset card .stun and .wound-- only relevant if a
-- multi-target attack AND .stun/.wound were applied
-- by a modifier card (so should not necessarily be
-- applied ot all targets)
crd.stun,crd.wound=basestun,basewound
addmsg(msg)
--prepare attack animation
local aspr=144+dmg
if (dmg>9) aspr=154
queueanim(nil,d.x,d.y,a.x,a.y,aspr)
dmgactor(d,dmg)
end
--draw player attack modifier card
-- (and maintain a discard pile and dwindling deck)
function modcard()
if #pmoddeck==0 then
shufflemod()
end
local c = rnd(pmoddeck)
add(pmoddiscard,c)
del(pmoddeck,c)
return c
end
--try to have enemy attack
function enemyattack(e)
if dst(e,p) <= e.crd.rng and pseudolos(e,p) then
runattack(e,p)
-- else
-- addmsg(e.name.." cannot attack") --debug, removed to reduce message spam + tokens
end
end
function healactor(a,val)
local heal=min(val,a.maxhp-a.hp)
a.hp+=heal
addmsg(a.name.." healed \f8+"..heal.."♥")
a.wound=nil
end
--damage actor and check death, etc
function dmgactor(a,val)
a.hp-=val
if a.hp<=0 then
if a==p then
if hasitem("life",true) then
a.hp,a.wound=1,false
addmsg("\f7your life charm glows\n and you survive @ 1hp")
else
-- burn random card in hand to negate dmg
local crd=rnd(cardsleft())
if crd then
crd[3]=2
a.hp+=val
addmsg("you \f8burn\f6 a random card\n\f8[\f6"..crd[2].."\f8]\f6 to avoid death")
end
--TODO? if no cards in hard, burn 2 from discard pile?
-- (niche rare option, defer for now)
end
else
--sfx(5)
--TODO: move this to separate function?
addmsg("\f7"..a.name.." is defeated!")
--drop coin, but won't be visible until check in _update60() removes
-- enemy sprite from play area
if (tutorialmode) addmsg(" and drops a coin (●)")
local cspr=57
local m=mget(a.x,a.y)
-- if there's already 1 or 2 coins, update to 2-3 coin stack...
if (m>=57 and m<=58) cspr=m+1
if (m==59) cspr=m
mset(a.x,a.y,cspr)
p.xp+=1
end
end
end
-----
----- 4b) player actions
-----
--first time entering actplayer for turn
function initactplayerpre()
p.actionsleft=2
changestate("actplayer",0)
end
--each time entering actplayer (typically runs twice/turn,
-- for 1st + 2nd actions fof turn, but also runs after 'undo', etc)
function initactplayer()
--checks for ended-on-trap-with-jump-on-prev-move,
-- since that wouldn't be caught during animmovestep
checktriggers(p)
if (p.actionsleft == 0) then
p.crds,p.init=nil --assignment with misssing values sets p.init to default of nil
changestate("actloop",0)
return
end
addmsg("\fc⬆️⬇️,🅾️\f6:choose card "..3-p.actionsleft)
if (tutorialmode) addmsg(" or dflt act █2 / 😐2 ◆")
_updstate=_updactplayer
_drwstate=_drawmain --could comment out to save 3 tokens since hasn't changed since last state (but that's risky/brittle to future state flow changes)
end
--loops in this routine until card selected and 🅾️, then runs that card
-- (and then the called function typically changes state to actplayer
-- when done, to rerun initactplayer above before running this again)
--lint: crdplayed,crdplayedpos
function _updactplayer()
selxy_update_clamped(1,#p.crds) --let them select one of p.crds
if btnp(🅾️) then
--crd = the card table (initiative, string, status)
--note: if card in players deck, this is a reference to an entry in pdeck
-- so edits to crd (e.g. changing crd[3] to discard or burn it) also edit the original in pdeck for future turns
local crd=p.crds[sely]
--global copy to restore if needed for an undo
crdplayed,crdplayedpos=crd,indextable(p.crds,crd)
--parse just the action string into data structure
p.crd=parsecard(crd[2])
--special-case modification of range if has googles item
-- (TODO? more clear if done in parsecard?)
if (hasitem("goggl") and p.crd.rng and p.crd.rng>1) p.crd.rng+=2
p.actionsleft -= 1
runcard(p)
--note: card was already set to 'discarded' (crd[3]=1) back
-- when cards were chosen from hand
if (p.crd.burn) crd[3]=2 --burn card instead
del(p.crds,crd) --delete from list of cards shown in UI
end
end
--execute the card that has been parsed into a.crd
-- (where a is a reference to an entry in actor[])
-- (called by _updactplayer() or _updactenemy()
function runcard(a)
local crd=a.crd
--if (godmode and a==p) crd.val,crd.rng=9,9 --obsolete 'god mode'
if crd.act==sh("m") then --a move action
if a==p then
changestate("actplayermove",0)
else
enemymoveastar(a)
end
elseif crd.aoe==8 then --specific player AoE attack 'all adjacent' where no UI interaction to select targets is needed
--TODO? generalize for multiple different AoE attacks (aoepat[#]?)
-- but AoE attacks w/ selectable targets/directions would need to
-- happen in an interactive attack mode like "actplayerattack"
--TODO? maybe merge w/ handling of HAIL special AoE attack?
--list of the 8 (x,y) offsets relative to player hit by this
-- 'all surrounding enemeies' AoE, hard-coded as string for minimal tokens
local aoepat=splt3d("x;-1;y;-1|x;0;y;-1|x;1;y;-1|x;-1;y;0|x;1;y;0|x;-1;y;1|x;0;y;1|x;1;y;1",true)
foreach(aoepat,pdeltatoabs) --modifies aoepat in place
foreach(aoepat,pattackxy) --run attack for each AoE square
changestate("actplayer",0)
elseif crd.act==sh("a") then --standard attack
if a==p then
changestate("actplayerattack",0) --UI for target selection
else
enemyattack(a) --run enemy attack for actor a
end
else --other simpler actions without UI/selection
--Note: currently each action is a ssumed to only do one thing,
-- e.g. move, attack, heal, or so on.
--TODO: implement code to allow player heal/shield actions
-- attached to a move/attack? not worth tokens for chapter 1
if (crd.act==sh("h")) healactor(a,crd.val)
if crd.act==sh("s") then
a.shld+=crd.val
addmsg(a.name.." ★+"..crd.val)
elseif crd.act==sh("l") and a==p then
addmsg("looting treasure @➡️"..crd.val)
rangeloot(crd.val)
elseif crd.act=="hail▒" then --special player attack
foreach(inrngxy(p,crd.rng),pattackxy)
elseif crd.act=="howl" then --special enemy attack
addmsg(a.name.." howls.. \f8-1♥,▥")
dmgactor(p,1)
p.stun=true
elseif crd.act=="call" then
summon(a)
end
if (a==p) changestate("actplayer",0)
end
if (crd.burn) p.xp+=2 --using burned cards adds xp
end
--one-off function (could inline where called in foreach() above,
-- to save a few tokens since used in only one place)
-- to take an {x,y} delta relative to the player
-- and transform it to absolute positions
function pdeltatoabs(xy)
xy.x+=p.x
xy.y+=p.y
end
--have player attack a square (occupied or not)
--can be called directly for single attack or passed to foreach() for multi attacks
function pattackxy(xy)
local ai=actorat(xy.x,xy.y)
if ai>1 then
runattack(p,actor[ai])
else
--no enemy in target square, queue empty attack animation
queueanim(nil,xy.x,xy.y,p.x,p.y,2)
end
end
--return all {x=x,y=y} cells within range r of actor a
-- (and on map, within LOS, not fogged, etc)
function inrngxy(a,r)
local inrng={}
for i=-r,r do
for j=-r,r do
local tx,ty=a.x+i,a.y+j
local txy=xylst(tx,ty)
if (not isfogoroffboard(tx,ty) and dst(a,txy)<=r and pseudolos(a,txy)) add(inrng,txy)
end
end
return inrng
end
function longrest()
p.actionsleft=0
addmsg("you take a \f7long rest\f6:")
--refresh discarded and items
foreach(pdeck,refresh)
foreach(pitems,refresh)
healactor(p,3)
--note: burning of the card selected along with 'rest' was done
-- earlier in pdeckbld(), so that p.crds doesn't show that card before p's turn)
-- now display the burn message configured back in pdeckbld()
addmsg(restburnmsg)
end
--loot treasure (for player) at x,y
--TODO: reduce code?
function loot(x,y)
local m=mget(x,y)
if fget(m,5) then
if m>=57 and m<=59 then --1 to 3 stacked coin(s)
-- sfx(1) --loot sfx removed to save tokens and because didn't seem to add much
local gp=gppercoin * (m-56)
p.gold+=gp
addmsg("picked up "..gp.."● (gold)")
elseif m==37 then --chest
if dlvl==15 then
lootedchests+=1
-- sfx(2)
addmsg("you find a map piece!")
else --random chest treasure, options depend on difficulty level
local tr=rnd(rndtreasures[difficulty])
local tt,tv=tr[1],tr[2]
if tt=="g" then
p.gold+=tv
-- sfx(2)
addmsg("you find "..tv.."●!")
elseif tt=="d" then
-- sfx(3)
addmsg("chest is trapped! \f8-"..tv.."♥")
dmgactor(p,tv)
end
end
end
mset(x,y,33)
end
end
--loot all treasures within rng r of player (no enemies currently loot)
-- note: inrngxy() checks unfogged, in LOS, etc so you won't
-- loot through walls
function rangeloot(r)
for xy in all(inrngxy(p,r)) do
loot(xy.x,xy.y)
end
end
---- state actplayermove (interactive player move action)
function initactplayermove()
showmapsel=true --show selection box on map
selx,sely=p.x,p.y
mvq={xylst(selx,sely)} --initialize move queue with current player location
--TODO: find 12 tokens to add back in the '(jump)' message,
-- removed to fill other last-minute token needs
--local msg="move up to "..p.crd.val
if (hasitem("belt")) p.crd.jmp=true
--if (p.crd.jmp) msg..=" (jump)"
--addmsg(msg)
addmsg("move up to "..p.crd.val)
if (tutorialmode) addmsg(" (\fc🅾️\f6:confirm, \fc❎\f6:undo)")
_updstate=_updactplayermove
_drwstate=_drawmain --could comment out to save 3 tokens since hasn't changed since last state (but that's risky/brittle to future state flow changes)
end
--player interactively builds step-by-step move queue
-- (not only destination: path matters due to traps or other triggers)
function _updactplayermove()
local selx0,sely0=selx,sely
selxy_update_clamped(10,10,0,0)
--if player moved the cursor, try to move:
if selx!=selx0 or sely!=sely0 then
--NOTE: commented out lines tried to streamline code,
-- but can't compare equality on two lists easily?
--local selxy=xylst(selx,sely)
--if #mvq>=2 and mvq[#mvq-1]==selxy then
if #mvq>=2 and mvq[#mvq-1].x==selx and mvq[#mvq-1].y==sely then
--if player moved back to previous location, trim move queue
deli(mvq,#mvq)
elseif #mvq>p.crd.val or not validmove(selx,sely,false,p.crd.jmp,1) then
--if move not valid (obstacle/actor unless jumping,
-- or beyond player move range), cancel that move
selx,sely=selx0,sely0
else
--valid move step, add to move queue
--note: still might not be valid location to _end_ a move on,
-- e.g. obstacle, but that's checked when 🅾️ pressed
add(mvq,xylst(selx,sely)) --or pass in selxy if set above (not currently implemented)
end
end
-- it's only valid to _end_ move here if it's a passable hex within range
-- (global selvalid also affects how selection cursor is drawn, dashed or solid)
selvalid = (#mvq-1) <= p.crd.val and validmove(selx,sely,true,false,1)
if btnp(🅾️) then
if selvalid then
if (#mvq>1) sfx(11)
--kick off move and animation
--NOTE: animmovestep will also update p.x,p.y to move along the
-- move queue as a side effect, not intuitive
changestate("animmovestep",0)
else
addmsg("invalid move")
end
elseif btnp(❎) then
undoactplayer()
end
end
--try to restore state and undo card selection
-- (for move and attack actions not completed)