-
Notifications
You must be signed in to change notification settings - Fork 1
/
antcopter.nelua
1374 lines (1229 loc) · 37.5 KB
/
antcopter.nelua
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
## pragma{nogc = true, noerrorloc = true}
require '.rivneco'
require 'vector'
require 'string'
require 'math'
##[==[
neco_setup({
width=160, height=160,
tilesize=8,
target_fps=60,
gfx='assets/gfx.ppm',
map='assets/map.lua',
font = {
glyph_xadvance=8,
glyph_yadvance=6,
glyph_xspacing=1,
glyph_yspacing=1,
glyph_width=3,
image='assets/font.ppm'
},
print='ptext',
})
]==]
function math.qsin(x: number): number <inline,nosideeffect>
-- Quadratic sin approximation (imprecise), using the following constrains:
-- f(0) = 0, f(pi) = 0, f(pi/2) = 1
local line = x*#[1/math.pi]#
local stair = math.floor(line)
local saw = line - stair
local wave = 4.0*saw*(1.0-saw)
local signal = (1.0-2.0*(stair - 2.0*math.floor(0.5*line)))
return signal*wave
end
function math.qcos(x: number): number <inline,nosideeffect>
return math.qsin(x + #[math.pi/2]#)
end
--------------------------------------------------------------------------------
-- Vec2
global Vec2 = @record {
x: float32,
y: float32
}
function Vec2.__add(a: auto, b: auto): Vec2 <inline>
## if b.type.is_scalar then
return Vec2{a.x + b, a.y + b}
## elseif a.type.is_scalar then
return Vec2{a + b.x, a + b.y}
## else
return Vec2{a.x + b.x, a.y + b.y}
## end
end
function Vec2.__sub(a: auto, b: auto): Vec2 <inline>
## if b.type.is_scalar then
return Vec2{a.x - b, a.y - b}
## elseif a.type.is_scalar then
return Vec2{a - b.x, a - b.y}
## else
return Vec2{a.x - b.x, a.y - b.y}
## end
end
function Vec2.__mul(a: auto, b: auto): Vec2 <inline>
## if b.type.is_scalar then
return Vec2{a.x * b, a.y * b}
## elseif a.type.is_scalar then
return Vec2{a * b.x, a * b.y}
## else
return Vec2{a.x * b.x, a.y * b.y}
## end
end
function Vec2.__div(a: auto, b: auto): Vec2 <inline>
## if b.type.is_scalar then
local k: float32 = 1 / b
return Vec2{a.x * k, a.y * k}
## elseif a.type.is_scalar then
local k: float32 = 1 / a
return Vec2{k * b.x, k * b.y}
## else
return Vec2{a.x / b.x, a.y / b.y}
## end
end
function Vec2.__len(a: Vec2): float32 <inline>
return math.sqrt(a.x * a.x + a.y * a.y)
end
function Vec2.normalize(a: Vec2): Vec2 <inline>
return a / #a
end
--------------------------------------------------------------------------------
-- Rect
local Rect = @record{
x: float32, y: float32,
w: float32, h: float32,
}
function Rect.translate(self: Rect, delta: Vec2): Rect
return Rect{self.x + delta.x, self.y + delta.y, self.w, self.h}
end
function Rect.center(self: Rect): Vec2
return Vec2{self.x + self.w/2, self.y + self.h/2}
end
function Rect.bottom_center(self: Rect): Vec2
return Vec2{self.x + self.w/2, self.y + self.h}
end
function Rect.right_center(self: Rect): Vec2
return Vec2{self.x + self.w, self.y + self.h/2}
end
function Rect.left_center(self: Rect): Vec2
return Vec2{self.x, self.y + self.h/2}
end
function Rect.overlaps(a: Rect, b: Rect): boolean
return a.x + a.w > b.x and b.x + b.w > a.x and a.y + a.h > b.y and b.y + b.h > a.y
end
--------------------------------------------------------------------------------
-- Hashing utilities
local function union_cast(T: type, x: auto) <inline>
return (@union{v: T, x: #[x.type]#}){x=x}.v
end
local function hash11(v: float32): float32 <inline>
local h: uint32 = union_cast(@uint32, v)
h = 1103515245_u * (h ~ (h>>1_u))
h = h ~ (h>>16_u)
return (h & 0x7fffffff_u)*#[1.0/0x7fffffff]#
end
local function hash22(v: Vec2): Vec2 <inline>
local hx: uint32, hy: uint32 = union_cast(@uint32, v.x), union_cast(@uint32, v.y)
hx, hy = 1103515245_u * (hy ~ (hx>>1_u)), 1103515245_u * (hx ~ (hy>>1_u))
local h: uint32 = 1103515245_u * (hx ~ (hy>>3_u))
h = h ~ (h>>16_u)
return Vec2{h & 0x7fffffff_u, (h*48271_u) & 0x7fffffff_u}*#[1.0/0x7fffffff]#
end
--------------------------------------------------------------------------------
-- Types
local ObjectFlag = @enum(uint8){
Removed = 1 << 0,
Toggled = 1 << 2,
Blockable = 1 << 3,
}
local Object = @record{
pos: Vec2,
id: uint8,
flags: uint8,
}
local ObjectId = @enum(uint8){
Object = 0,
Block = 1,
Box = 2,
Spikes = 3,
Platform = 4,
Fruit = 5,
Wind = 6,
LevelGoal = 7,
UpgradeFruit = 9,
Player = 11,
Friend = 12,
DeadEffect = 64,
PoofEffect = 65,
UpgradeEffect = 66,
PickupEffect = 67,
}
local SoundEffectId = @enum(uint8){
Jump=1,
Collide,
WallSlide,
Glide,
Death,
Upgrade,
Fruit1,
Fruit2,
Step1,
Step2,
GameStart,
GameEnd,
GameOver,
}
local Game = @record{
objects: vector(*Object),
frame: uint32,
shake_stop_frame: uint32,
title_stop_frame: uint32,
start_frame: uint32,
end_frame: uint32,
level_frame: uint32,
music_dispatch_frame: uint32,
deaths: int32,
hearts: int32,
level: uint8,
next_level: uint8,
berries: uint8,
level_berries: uint8,
powerup: boolean,
level_finished: boolean,
music_disabled: boolean,
notes: uint32,
music_tones: [128]uint64,
}
local TILE_SIZE <comptime> = 8
local MAP_SIZE <comptime> = 20
local game: Game
local sb: stringbuilder
function Game:spawn_object(id: int16, pos: Vec2): *Object <forwarddecl> end
--------------------------------------------------------------------------------
-- Sound effects
global function sfx(x: integer)
local desc: ToneDesc
switch x do
case SoundEffectId.Jump then -- jump
desc={start_frequency=400, end_frequency=200, sustain=4, release=2, type=WaveType.NOISE, amplitude=0.3}
case SoundEffectId.Collide then -- collide
desc={start_frequency=250, end_frequency=100, sustain=4, release=2, type=WaveType.NOISE, amplitude=0.3}
case SoundEffectId.WallSlide then -- wall slide
desc={start_frequency=490, sustain=6, release=1, type=WaveType.NOISE, amplitude=0.1}
case SoundEffectId.Glide then -- glide
desc={start_frequency=1200, end_frequency=200, sustain=1, release=1, type=WaveType.PULSE, amplitude=0.2}
case SoundEffectId.Death then -- death
desc={start_frequency=300, end_frequency=200, sustain=4, release=4, type=WaveType.NOISE, amplitude=0.4}
case SoundEffectId.Upgrade then -- upgrade fruit
desc={start_frequency=220, end_frequency=440*2, sustain=112, release=4, type=WaveType.PULSE, amplitude=0.3}
case SoundEffectId.Fruit1 then -- fruit 1
desc={start_frequency=C4*4, sustain=4, release=2, type=WaveType.PULSE, amplitude=0.2}
case SoundEffectId.Fruit2 then -- fruit 2
desc={delay = 1/10, start_frequency=E4*4, sustain=4, release=4, type=WaveType.PULSE, amplitude=0.2}
case SoundEffectId.Step1 then -- step 1
desc={start_frequency=300, end_frequency=400, sustain=1, release=1, type=WaveType.NOISE, amplitude=0.2, pan=-0.3}
case SoundEffectId.Step2 then -- step 2
desc={start_frequency=320, end_frequency=420, sustain=1, release=1, type=WaveType.NOISE, amplitude=0.2, pan=0.3}
case SoundEffectId.GameStart then -- game start
desc={start_frequency=220, end_frequency=440, sustain=60, release=4, type=WaveType.NOISE, amplitude=0.3}
case SoundEffectId.GameEnd then -- game end
desc={start_frequency=220, end_frequency=440*4, sustain=112, release=4, type=WaveType.PULSE, amplitude=0.3}
case SoundEffectId.GameOver then -- game end
desc={start_frequency=220, end_frequency=110, sustain=40, release=4, type=WaveType.SQUARE, amplitude=0.2}
else
return
end
tonex(desc)
end
local bass_notes: []float32 = {
A4, 0, A4, A4, A4, 0, A4, A4, A4, 0, A4, A4, A4, 0, A4, A4,
C5, 0, C5, C5, C5, 0, C5, C5, C5, 0, C5, C5, C5, 0, C5, C5,
D5, 0, D5, D5, D5, 0, D5, D5, D5, 0, D5, D5, D5, 0, D5, D5,
G4, 0, G4, G4, G4, 0, G4, G4, G4, 0, G4, G4, G4, 0, G4, G4,
}
local rhythm_notes: []float32 = {
A4, C5, E5, G5, A4, C5, E5, G5, A4, C5, E5, G5, A4, C5, E5, G5,
C5, E5, G5, A5, C5, E5, G5, A5, C5, E5, G5, A5, C5, E5, G5, A5,
D5, E5, G5, A5, D5, E5, G5, A5, D5, E5, G5, A5, D5, E5, G5, A5,
G4, A4, B4, E5, G4, A4, B4, E5, G4, A4, B4, E5, G4, A4, B4, E5,
}
local lead_notes: []float32 = {
A4, A4, E4, A4, E5, D5, C5, B4, B4, E4, A4, B4, C5, B4, A4, G4,
C5, C5, A4, G4, A4, C5, D5, C5, C5, D5, E5, F5, E5, B4, A4, B4,
D5, D5, C5, B4, A4, G4, A4, B4, B4, E4, G4, A4, B4, B4, A4, A4,
G4, G4, E4, D4, C4, E4, A4, A4, G4, E4, E4, G4, A4, B4, G4, E4,
}
function Game:stop_music()
for _,id in ipairs(self.music_tones) do
stoptone(id)
if id == 0 then break end
end
self.music_dispatch_frame = (self.frame + 12) // 12 * 12
end
function Game:play_music()
local speed = game.powerup and 8 or 12
if self.frame < self.music_dispatch_frame then return end
self.music_dispatch_frame = self.frame + 16*speed
local j = 0
for i=0,<16 do
local delay = (i*speed+2) / 60
if (self.notes // #bass_notes) % 7 ~= 6 then
self.music_tones[j] = tonex{
delay = delay,
start_frequency = bass_notes[self.notes % #bass_notes] / 4,
attack = 1, sustain = speed-2, release = 1,
amplitude = 0.25,
type = WaveType.TRIANGLE,
}
j = j + 1
end
if self.notes >= #rhythm_notes then
if (self.notes // #rhythm_notes) % 2 == 1 then
self.music_tones[j] = tonex{
delay = delay,
start_frequency = rhythm_notes[self.notes % #rhythm_notes],
attack = 1, sustain = speed-2, release = 1,
amplitude = 0.08,
duty_cycle = 0.75,
type = WaveType.PULSE,
}
j = j + 1
else
self.music_tones[j] = tonex{
delay = delay,
start_frequency = lead_notes[self.notes % #lead_notes],
attack = 1, sustain = speed-2, release = 1,
amplitude = 0.08,
duty_cycle = 0.75,
type = WaveType.PULSE,
}
j = j + 1
end
end
self.notes = self.notes + 1
end
end
--------------------------------------------------------------------------------
-- Object
function Object:bbox(): Rect <forwarddecl> end
function Object:add_flag(flag: uint8) <inline>
self.flags = self.flags | flag
end
function Object:remove_flag(flag: uint8) <inline>
self.flags = self.flags & ~flag
end
function Object:has_flag(flag: uint8) <inline>
return self.flags & flag == flag
end
function Object:collides_with_blockable(offset: Vec2): *Object
local bbox = self:bbox():translate(offset)
for i,other in ipairs(game.objects) do
if other:has_flag(ObjectFlag.Blockable) and
self ~= other and not other:has_flag(ObjectFlag.Removed) and
bbox:overlaps(other:bbox()) then
if self.id == ObjectId.Player then
other:add_flag(ObjectFlag.Toggled)
end
return other
end
end
return nilptr
end
function Object:collides_with(offset: Vec2, id: int16): *Object
local bbox = self:bbox():translate(offset)
for i,other in ipairs(game.objects) do
if other.id == id and
self ~= other and not other:has_flag(ObjectFlag.Removed) and
bbox:overlaps(other:bbox()) then
return other
end
end
return nilptr
end
--------------------------------------------------------------------------------
-- Poof Effect
local PoofEffect = @record{
base: Object,
frames: uint8
}
function PoofEffect:_update()
self.frames = self.frames + 1
if self.frames > 60 then self.base:add_flag(ObjectFlag.Removed) end
end
function PoofEffect:_draw()
local t = (self.frames/60)^0.7*1.5
for ix=-1,1 do
for iy=-1,1 do
if ix == 0 and iy == 0 then continue end
local v = Vec2.normalize(Vec2{ix,iy})
local h = hash22(self.base.pos + v)
local p = self.base.pos + v*t*TILE_SIZE*(1+h)*0.5
local r = 3*(1-t)
circfill(p.x, p.y, r, 4)
end
end
end
--------------------------------------------------------------------------------
-- Dead Effect
local DeadEffect = @record{
base: Object,
frames: uint8
}
function DeadEffect:_update()
self.frames = math.min(self.frames + 1, 61)
if game.hearts == 0 then
if game.end_frame == 0 then
sfx(SoundEffectId.GameOver)
game.end_frame = game.frame
end
else
game.level_finished = self.frames > 60
end
end
function DeadEffect:_draw()
local t = (self.frames/60)^0.7*1.5
for ix=-1,1 do
for iy=-1,1 do
if ix == 0 and iy == 0 then continue end
local v = Vec2.normalize(Vec2{ix,iy})
local p = self.base.pos + v*t*TILE_SIZE*4
local r = TILE_SIZE*(1-t)
circfill(p.x, p.y, r, 4)
end
end
end
--------------------------------------------------------------------------------
-- Upgrade Effect
local UpgradeEffect = @record{
base: Object,
frames: uint8
}
function UpgradeEffect:_update()
if self.frames == 0 then
sfx(SoundEffectId.Upgrade)
end
self.frames = self.frames + 1
if self.frames > 60 then self.base:add_flag(ObjectFlag.Removed) end
end
function UpgradeEffect:_draw()
local y = math.floor(self.base.pos.y - self.frames*0.3)
local col = 3 + (self.frames // 4) % 2
ptext('upgrade', self.base.pos.x, y, col, 1)
end
--------------------------------------------------------------------------------
-- Pickup Effect
local PickupEffect = @record{
base: Object,
frames: uint8
}
function PickupEffect:_update()
if self.frames == 0 then
sfx(SoundEffectId.Fruit1)
sfx(SoundEffectId.Fruit2)
end
self.frames = self.frames + 1
if self.frames > 60 then self.base:add_flag(ObjectFlag.Removed) end
end
function PickupEffect:_draw()
local y = math.floor(self.base.pos.y - self.frames*0.3)
local col = 3 + (self.frames // 4) % 2
ptext('+1', self.base.pos.x, y, col, 1)
end
--------------------------------------------------------------------------------
-- Block
local Block = @record{base: Object}
function Block:__new()
self.base.flags = ObjectFlag.Blockable
end
function Block:_draw()
spr(ObjectId.Block, self.base.pos.x, self.base.pos.y, 1, 1, false, false)
end
--------------------------------------------------------------------------------
-- Spikes
local Spikes = @record{base: Object}
function Spikes:_draw()
spr(ObjectId.Spikes, self.base.pos.x, self.base.pos.y, 1, 1, false, false)
end
--------------------------------------------------------------------------------
-- Player
local Player = @record{
base: Object,
rem: Vec2,
vel: Vec2,
spridx: int16,
antidx: int16,
timer: int16,
freeze: int16,
helixframes: int16,
glideframes: int16,
floating: boolean,
gliding: boolean,
celebrating: boolean,
locked: boolean,
jump_pressed: boolean,
flipx: boolean,
}
function Player:_bbox(): Rect
return Rect{self.base.pos.x, self.base.pos.y + 2, 8, 14}
end
function Player:move_x(dx: float32)
local sx = math.sign(dx)
for ox=0,<math.abs(dx) do
local nx = self.base.pos.x + sx
if nx < 0 or nx >= 160-8 or self.base:collides_with_blockable({sx,0}) then
self.vel.x = 0
self.rem.x = 0
break
else
self.base.pos.x = nx
end
end
end
function Player:move_y(dy: float32)
local sy = math.sign(dy)
for oy=0,<math.abs(dy) do
local ny = self.base.pos.y + sy
if ny < 0 or ny >= 160-14 or self.base:collides_with_blockable({0,sy}) then
self.vel.y = 0
self.rem.y = 0
break
else
self.base.pos.y = ny
end
end
end
function Player:_update()
if self.freeze > 0 then
self.spridx = 0
self.antidx = 0
self.freeze = self.freeze - 1
return
end
if self.base:collides_with({0,0}, ObjectId.Spikes) then -- die
game:spawn_object(ObjectId.DeadEffect, self.base:bbox():center())
self.base:add_flag(ObjectFlag.Removed)
game.shake_stop_frame = game.frame + 15
game.deaths = game.deaths + 1
game.hearts = game.hearts - 1
sfx(SoundEffectId.Death)
return
end
local right_pressed = not self.locked and btn(BTN_RIGHT)
local left_pressed = not self.locked and btn(BTN_LEFT)
local down_pressed = not self.locked and btn(BTN_DOWN)
local up_pressed = not self.locked and btn(BTN_UP)
local jump_pressed = not self.locked and btn(BTN_O)
local accel_pressed = not self.locked and btn(BTN_X)
local ground = self.base:collides_with_blockable({0,1})
local flying = not ground
local winding = self.base:collides_with({0,0}, ObjectId.Wind)
local xdir = 0
do -- walk left/right
if right_pressed then
xdir = 1
elseif left_pressed then
xdir = -1
end
local maxspeed = 1
local accel = 0.6
local damping = 0.4
local target = maxspeed*xdir
if flying then
accel = 0.5
if math.abs(self.vel.x) > maxspeed then
damping = 0.95
xdir = 0
end
end
self.vel.x = self.vel.x * damping + accel*xdir
end
local jumped = false
do -- jump/fall
if not self.jump_pressed and jump_pressed then -- try jump
local walldir = 0
if not flying then -- ground jump
game:spawn_object(ObjectId.PoofEffect, self.base:bbox():bottom_center())
self.vel.y = -2.3
jumped = true
sfx(SoundEffectId.Jump)
elseif not winding and self.base:collides_with_blockable({2,0}) then -- right wall jump
game:spawn_object(ObjectId.PoofEffect, self.base:bbox():right_center())
self.vel = {-2, -2}
jumped = true
sfx(SoundEffectId.Jump)
elseif not winding and self.base:collides_with_blockable({-2,0}) then -- left wall jump
game:spawn_object(ObjectId.PoofEffect, self.base:bbox():left_center())
self.vel = { 2, -2}
jumped = true
sfx(SoundEffectId.Jump)
end
end
self.jump_pressed = jump_pressed
end
local gliding = false
local powergliding = false
local divingdown = false
local wallsliding = false
if not jumped then -- fall
local gravity = 0.1
if winding or math.abs(self.vel.y) <= 0.15 then -- slowdown on jump top
gravity=gravity*0.5
end
local maxspeed = 2
if winding then
maxspeed = 1
end
if winding and (jump_pressed or accel_pressed) then -- glide up
if self.vel.y >= -0.8 then
gravity = gravity-0.2
end
gliding = true
elseif accel_pressed and game.powerup and self.glideframes < 60 then -- power glide up
if self.vel.y >= -0.8 then
gravity = gravity-0.2
end
gliding = true
powergliding = true
self.glideframes = self.glideframes + 1
elseif flying and (jump_pressed or accel_pressed) and self.vel.y >= -0.2 then -- glide down
maxspeed = 0.5
gliding = true
elseif xdir ~= 0 and self.base:collides_with_blockable({xdir,0}) then -- wall sliding
maxspeed = 0.4
wallsliding = true
if self.timer % 8 == 7 then
sfx(SoundEffectId.WallSlide)
end
elseif flying and down_pressed then -- dive down
gravity = gravity * 2
maxspeed = maxspeed * 2.5
divingdown = true
end
self.vel.y = math.min(self.vel.y + gravity, maxspeed)
end
if self.floating then
if down_pressed or up_pressed or left_pressed or right_pressed or jump_pressed or accel_pressed or ground then
self.floating = false
self.glideframes = 0
self.vel.y = 0.2
else
gliding = true
self.vel.y = 0.4
end
end
do -- apply velocity
if self.vel.x ~= 0 then -- move x
self.rem.x = self.rem.x + self.vel.x
local dx = math.trunc(self.rem.x)
self.rem.x = self.rem.x - dx
self:move_x(dx)
end
if self.vel.y ~= 0 then -- move y
self.rem.y = self.rem.y + self.vel.y
local dy = math.trunc(self.rem.y)
self.rem.y = self.rem.y - dy
self:move_y(dy)
end
end
-- facing
if self.vel.x ~= 0 then
self.flipx = self.vel.x < 0
end
flying = not self.base:collides_with_blockable({0,1})
if not flying or winding then -- refuel
self.glideframes = 0
end
if (gliding and game.frame % 8 == 0) or (powergliding and game.frame % 4 == 0) then
sfx(SoundEffectId.Glide)
end
if divingdown and not flying then -- reached block
game:spawn_object(ObjectId.PoofEffect, self.base:bbox():bottom_center())
game.shake_stop_frame = game.frame + 8
sfx(SoundEffectId.Collide)
end
self.gliding = flying and gliding
if powergliding then
self.helixframes = math.min(self.helixframes + 2, 20)
else
self.helixframes = math.max(self.helixframes - 1, 0)
end
-- animation
if self.celebrating then
local frame = ((self.timer // 4) % 3)
self.spridx = 3 + ((game.frame // 20) % 3)
self.antidx = frame
self.flipx = (game.frame // 30) % 2 == 0
elseif flying then
if gliding then
local frame = ((self.timer // 4) % 3)
self.spridx = 3 + frame
self.antidx = frame
elseif wallsliding then
self.spridx = 8
self.antidx = 0
elseif divingdown then
self.spridx = 6
self.antidx = 3
else
self.spridx = 3
self.antidx = 0
end
elseif down_pressed then -- looking down
self.spridx = 6
self.antidx = 3
self.timer = 0
elseif up_pressed then -- looking up
self.spridx = 7
self.antidx = 4
self.timer = 0
elseif math.abs(self.vel.x) <= 0.1 and
not (right_pressed or left_pressed) then -- standing
self.spridx = 0
self.antidx = 0
self.timer = 0
else -- walking
self.spridx = ((1 + self.timer // 8) % 3)
if game.frame % 24 == 0 then
sfx(SoundEffectId.Step1)
elseif game.frame % 24 == 12 then
sfx(SoundEffectId.Step2)
end
self.antidx = 0
end
self.timer = self.timer + 1
end
function Player:_draw()
-- body
spr(self.base.id + self.spridx, self.base.pos.x, self.base.pos.y, 1, 2, self.flipx, false)
local palchange = self.gliding and (game.frame // 8) % 2 == 0
-- helix
if palchange then
pal(4, 3)
pal(3, 4)
end
if game.powerup then
spr(38 + self.antidx*3, self.base.pos.x - TILE_SIZE, self.base.pos.y, 3, 1, self.flipx, false)
else
spr(33 + self.antidx, self.base.pos.x, self.base.pos.y, 1, 1, self.flipx, false)
end
if palchange then
pal()
end
-- helix wind effect
if game.powerup and self.helixframes > 0 then
local fy = math.clamp(self.helixframes / 20, 0.0, 1.0)
for i=1,math.ceil(fy*20) do -- back wind
local h = hash22(Vec2{i*33.33, i*66.66})
local t = ((game.frame / 60) % 1000)*10
local oy = 1 + h.y*TILE_SIZE*0.5
if btn(BTN_DOWN) then
oy = oy + 1
end
local theta = t*2 + h.x*6.28
local col = math.qcos(theta) < 0 and 3 or 4
local ox = TILE_SIZE/2 + math.qsin(theta)*(TILE_SIZE + h.x*2)
rectfill(self.base.pos.x + ox, self.base.pos.y + oy,
self.base.pos.x + ox+1, self.base.pos.y + oy, col)
end
end
end
--------------------------------------------------------------------------------
-- Fruit
local Fruit = @record{
base: Object,
spawn_pos: Vec2
}
function Fruit:__new()
self.spawn_pos = self.base.pos
end
function Fruit:_update()
if self.base:collides_with({0,0}, ObjectId.Player) then -- captured
game:spawn_object(ObjectId.PickupEffect, self.base:bbox():center())
game.level_berries = game.level_berries + 1
self.base:add_flag(ObjectFlag.Removed)
else
local h = hash11(self.spawn_pos.x*20 + self.spawn_pos.y)
self.base.pos.y = self.spawn_pos.y + math.sin(h*6.14+(game.frame/60)*10)*1
end
end
function Fruit:_draw()
spr(ObjectId.Fruit, self.base.pos.x, self.base.pos.y, 1, 1, false, false)
end
--------------------------------------------------------------------------------
-- Wind
local Wind = @record{
base: Object,
fast_frames: uint8
}
function Wind:_update()
if self.base:has_flag(ObjectFlag.Toggled) and self.fast_frames == 0 then
self.fast_frames = 1
elseif self.fast_frames > 0 then
self.fast_frames = self.fast_frames + 1
if self.fast_frames > 121 then self.base:add_flag(ObjectFlag.Removed) end
end
end
function Wind:_draw()
local h = hash22(self.base.pos)
for i=1,3 do
local x, y
if self.fast_frames > 0 then -- fast wind
local t = ((self.fast_frames-1) / 60 % 1000)*10 + i*0.3
x = self.base.pos.x + TILE_SIZE/2 + math.qsin(t + h.x*6.28)*6
local o = TILE_SIZE-(self.base.pos.x + h.y*TILE_SIZE)
y = self.base.pos.y + TILE_SIZE-(self.base.pos.x + t*8 + h.y*TILE_SIZE) - o
else
local t = (game.frame / 60 % 1000)*10 + i*0.3
x = self.base.pos.x + TILE_SIZE/2 + math.qsin(t + h.x*6.28)*3
y = self.base.pos.y + TILE_SIZE-((self.base.pos.x + t*2 + h.y*TILE_SIZE) % TILE_SIZE)
end
circfill(x, y, 0.5, 4)
end
end
--------------------------------------------------------------------------------
-- Friend
local Friend = @record{
base: Object,
frames: uint16,
spawn_pos: Vec2,
}
function Friend:_bbox(): Rect
return Rect{self.base.pos.x, self.base.pos.y + 2, 8, 14}
end
function Friend:__new()
self.spawn_pos = self.base.pos
end
function Friend:_update()
if self.base:has_flag(ObjectFlag.Toggled) then -- celebrate
local t = math.max(self.frames/60 - hash11(self.spawn_pos.x)*0.3, 0)
self.base.pos.y = self.spawn_pos.y - math.floor(math.abs(math.sin(t*4))*TILE_SIZE*2)
self.frames = self.frames + 1
end
end
function Friend:_draw()
local sprid = ObjectId.Player
if self.base:has_flag(ObjectFlag.Toggled) then -- celebrate
sprid = ObjectId.Player + 1 + ((game.frame // 20) % 3)
end
local flipx = (game.frame // 30) % 2 == 0
spr(sprid, self.base.pos.x, self.base.pos.y, 1, 2, flipx, false)
end
--------------------------------------------------------------------------------
-- Box
local Box = @record{
base: Object,
touch_frame: uint32
}
local BOX_CYCLE_DURATION <comptime> = 240
local BOX_ANIM_DURATION <comptime> = 60
function Box:__new()
self.base.flags = ObjectFlag.Blockable
end
function Box:_update()
if self.base:has_flag(ObjectFlag.Toggled) and self.touch_frame == 0 then
self.touch_frame = game.frame
end
if self.touch_frame ~= 0 then
local elapsed_frames = game.frame - self.touch_frame
if elapsed_frames > BOX_ANIM_DURATION and elapsed_frames <= BOX_CYCLE_DURATION then -- disappeared
self.base:remove_flag(ObjectFlag.Blockable)
elseif elapsed_frames > BOX_CYCLE_DURATION then -- level_finished disappear
self.touch_frame = 0
self.base:add_flag(ObjectFlag.Blockable)
self.base:remove_flag(ObjectFlag.Toggled)
end
end
end
function Box:_draw()
local sprid = ObjectId.Box
if self.touch_frame ~= 0 then
local elapsed_frames = game.frame - self.touch_frame
if elapsed_frames < BOX_ANIM_DURATION then -- animate disappear
local idx = elapsed_frames // (BOX_ANIM_DURATION//3)
if idx > 0 then
sprid = 31 + idx - 1
end
elseif elapsed_frames <= BOX_CYCLE_DURATION then -- disappear
sprid = 0
end
end
spr(sprid, self.base.pos.x, self.base.pos.y, 1, 1, false, false)
end
--------------------------------------------------------------------------------
-- Platform
local Platform = @record{
base: Object,
spawn_x: float32,
spawn_speed: float32,
}
function Platform:_bbox(): Rect
return Rect{self.base.pos.x, self.base.pos.y, 16, 4}
end
function Platform:__new()
self.base.flags = ObjectFlag.Blockable
self.spawn_x = self.base.pos.x
self.spawn_speed = 2.0 + riv_rand_float()*2.0
end
function Platform:_update()
local oldx = self.base.pos.x
local xdir = (self.spawn_x // TILE_SIZE) % 2 == 0 and 1 or -1
self.base.pos.x = self.spawn_x + xdir*((math.sin((game.frame/60)*self.spawn_speed) * TILE_SIZE*2)//1)
local dx = self.base.pos.x - oldx
if dx ~= 0 then
local player = (@*Player)(self.base:collides_with({0,-1}, ObjectId.Player) or
self.base:collides_with({math.sign(dx),0}, ObjectId.Player))
if player then
player:move_x(dx)
end
end
end
function Platform:_draw()
spr(ObjectId.Platform, self.base.pos.x, self.base.pos.y, 1, 1, false, false)
spr(ObjectId.Platform, self.base.pos.x+TILE_SIZE, self.base.pos.y, 1, 1, false, false)
end
--------------------------------------------------------------------------------
-- Upgrade Fruit
local UpgradeFruit = @record{base: Object}
function UpgradeFruit:_bbox(): Rect
return Rect{self.base.pos.x+3, self.base.pos.y+2, 16-6, 16-4}
end
function UpgradeFruit:_update()
local player = (@*Player)(self.base:collides_with({0,0}, ObjectId.Player))
if player then
game:spawn_object(ObjectId.UpgradeEffect, self.base:bbox():center())
player.floating = true