-
Notifications
You must be signed in to change notification settings - Fork 1
/
GameGridMap.gd
1712 lines (1408 loc) · 55.1 KB
/
GameGridMap.gd
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
extends "res://_GridMap.gd"
var PLAYER_SPEED := 2.5 if _DEBUG_ else 1.4
var PLAYER_OFFSET := AREA_OFFSET + Vector3(0,0.5,0)
class Player extends Spatial:
enum ANIMS {
ABSORB, # Take/place the orb from/into its bag
ABSORB_NONE, # Look at the orb like it doesn't work
RELEASE, # Take/place the orb from/into its bag
RELEASE_NONE, # Look at the orb like it doesn't work
START, STOP, # Start/stop walking/running, for the first/last cell
WALK, RUN, # Run if > 3 blocks away (always looping)
TURN_LEFT, TURN_RIGHT, HALF_TURN, # Turn +90º, -90º or +180º
ASCEND, DESCEND, # Climb/descend stairs
ASCEND_LIFT, DESCEND_LIFT,
TAKE, # Lean to take orb
STAND_UP, # Player stand up for Intro
IDLE # Default pose
}
var ANIMATIONS := {
ANIMS.ABSORB: "Using orb", ANIMS.ABSORB_NONE: "Cannot using orb",
ANIMS.RELEASE: "Using orb", ANIMS.RELEASE_NONE: "Cannot using orb",
ANIMS.START: "Start running", ANIMS.STOP: "Stop running",
ANIMS.WALK: "Walking", ANIMS.RUN: "Running",
ANIMS.TURN_LEFT: "Turn running", ANIMS.TURN_RIGHT: "Turn running", ANIMS.HALF_TURN: "Half turn",
ANIMS.ASCEND: "Ascending stairs", ANIMS.DESCEND: "Descending stairs",
ANIMS.ASCEND_LIFT: "Ascending Lift", ANIMS.DESCEND_LIFT: "Descending Lift",
ANIMS.TAKE: "Getting orb",
ANIMS.STAND_UP: "Stand up",
ANIMS.IDLE: "Idle"
}
var WAIT_PREFIX := "Waiting "
var WAITING_ANIMATIONS := {
Season.SUMMER: [ WAIT_PREFIX+"Summer", WAIT_PREFIX+"Summer 2" ],
Season.AUTUMN: [ WAIT_PREFIX+"Autumn", WAIT_PREFIX+"Autumn 2" ],
Season.WINTER: [ WAIT_PREFIX+"Winter", WAIT_PREFIX+"Winter 2" ],
Season.SPRING: [ WAIT_PREFIX+"Spring", WAIT_PREFIX+"Spring 2" ]
}
const ANIM_DURATIONS := { } # Duration with corrected speed
func duration(anim: int) -> float:
assert(ANIM_DURATIONS.has(anim))
return ANIM_DURATIONS[anim] / float(speed_factor)
var ANIM_SPEED := {
ANIMS.ABSORB: 1.3, ANIMS.ABSORB_NONE: 0.6,
ANIMS.RELEASE: 1.3, ANIMS.RELEASE_NONE: 0.6,
ANIMS.START: 1.4, ANIMS.STOP: 1.4,
ANIMS.WALK: 1.2, ANIMS.RUN: 1.4,
ANIMS.TURN_LEFT: 1.4, ANIMS.TURN_RIGHT: 1.4,
ANIMS.ASCEND: 1.5, ANIMS.DESCEND: 1.5
}
func _speed(anim) -> float:
if anim == null: return 1.0
assert(anim is int)
return (ANIM_SPEED[anim] if ANIM_SPEED.has(anim) else 1.0) * speed_factor
func name(anim: int) -> String:
assert(ANIMATIONS.has(anim))
return ANIMATIONS[anim]
var animation_player: AnimationPlayer setget ,get_animation_player
func get_animation_player() -> AnimationPlayer:
return animation_player
var season: int = Season.SUMMER setget set_season
func set_season(value: int):
season = value
_update()
var blend_enabled := true setget set_blend_enabled
func set_blend_enabled(value: bool):
blend_enabled = value
if animation_player:
animation_player.playback_default_blend_time = 0.2 if value else 0
var speed_factor := 1.0 setget set_speed_factor
func set_speed_factor(value: float):
speed_factor = value
if animation_player:
animation_player.playback_speed = value
var default_tween := Tween.new() setget ,get_default_tween
func get_default_tween() -> Tween:
return default_tween
func _ready():
name = "Player"
var node := preload("res://models/player.dae").instance()
node.rotation.y = PI
add_child(node)
if node.has_node("Lift"):
node.remove_child(node.get_node("Lift")) # DEBUG
animation_player = node.get_node("AnimationPlayer")
animation_player.connect("animation_started", self, "_on_animation_started")
# Add animation blending
set_blend_enabled(blend_enabled)
var transitions := [
[ANIMS.START, ANIMS.RUN],
[ANIMS.RUN, ANIMS.RUN],
[ANIMS.RUN, ANIMS.STOP]
]
for anims in transitions:
animation_player.set_blend_time(name(anims[0]), name(anims[1]), 0.0)
# Adjust animations speed
set_speed_factor(speed_factor)
for anim in ANIMS.values():
var animation := animation_player.get_animation(name(anim)); assert(animation)
ANIM_DURATIONS[anim] = animation.length / _speed(anim)
default_tween.name = "Tween"
add_child(default_tween)
_update()
func _update():
if not is_inside_tree(): return
# Import new skeleton
var model := preload("res://models/Season player.dae").instance()
var skeleton = model.get_node("Skeleton"); assert(skeleton)
skeleton.get_parent().remove_child(skeleton)
# Remove all seasons except `season` from `skeleton`
for child in []+skeleton.get_children():
if child.name != Season.name(season):
child.queue_free(); skeleton.remove_child(child)
assert(skeleton.get_child_count() == 1)
var torso = skeleton.get_child(0)
torso.name = "Torso" # Rename to match animation paths
# Fix materials properties
for i in torso.mesh.get_surface_count()-1:
var mat = torso.get_surface_material(i); assert(mat is SpatialMaterial)
mat.roughness = 0.8
# Replace old skeleton (after removing "Torso" child node)
var root := get_child(0)
var old_skeleton = root.get_node("Skeleton")
var old_torso = old_skeleton.get_child(0); assert(old_torso.name == "Torso")
old_skeleton.remove_child(old_torso)
old_skeleton.replace_by(skeleton)
func reset_skeleton_translation():
var root = get_child(0)
var skeleton = root.get_node("Skeleton")
var index = skeleton.find_bone("Skeleton")
var t = skeleton.get_bone_pose(index)
skeleton.set_bone_pose(index, t.translated(-t.origin))
func _on_animation_started(anim_name: String):
assert(get_animation_player().current_animation_position < 1e-3)
var name: int; for key in ANIMATIONS:
if ANIMATIONS[key] == anim_name: name = key
get_animation_player().playback_speed = _speed(name)
func play_waiting():
var anims := WAITING_ANIMATIONS[season] as Array
var index := randi() % anims.size()
var anim = anims[index]
get_animation_player().play(anim)
func stop_waiting():
var player := get_animation_player()
if player.current_animation.begins_with(WAIT_PREFIX):
player.stop(false) # Stop only waiting animation
var Anim = Player.ANIMS
const Activable = preload("res://Activable.gd")
const OrbPanel = preload("res://OrbPanel.tscn")
const SoundPlayer = preload("res://SoundPlayer.gd")
const GameCamera = preload("res://GameCamera.gd")
signal player_hurt()
signal end_accessible(end_name) # The Player can walk to `end_name`
signal end_reached(end_name) # Player is going to `end_name` cell (emitted just before `finished`)
signal finished(end_name) # Player on `end_name` cell
signal failed() #
signal game_restarted() #
signal _spots_updated(spots)
signal _anim_finished(anim)
const ANIM_DONE_SIGNAL = "_anim_finished"
var END_NAMES = { # Use `get_end_names()` instead
"Level30": ["End-G", "End-R"],
"*": ["End"]
}
# Returns the cell where the player start the level (can be an end if from previous level).
func get_begin():
var begin = get_start()
var index = get_end_names().find(start_name)
if index != -1: begin = get_ends()[index]
return begin
var camera := GameCamera.new()
var audio_player := SoundPlayer.new()
var orb_panel := OrbPanel.instance()
var default_light := DirectionalLight.new()
var secondary_light := DirectionalLight.new()
var player := Player.new()
var start_name := START_NAME setget set_start
func set_start(name: String):
start_name = name
var ambience: int = Ambience.DEFAULT setget set_ambience
func set_ambience(value: int):
ambience = value
assert(camera)
camera.set_default_environment(ambience)
_update_path_color()
_update_music_player()
func _get_item_surface(item_name: String, surface_name: String) -> SpatialMaterial:
var item := self.mesh_library.find_item_by_name(item_name)
var mesh := self.mesh_library.get_item_mesh(item) as ArrayMesh
var surface := Helper.get_surface_named(mesh, surface_name); assert(surface != -1)
return mesh.surface_get_material(surface) as SpatialMaterial
func _update_path_color():
var nightmare := (ambience == Ambience.NIGHTMARE)
var color = Season.path_color(season)
if nightmare:
color = color.lightened(0.25); color.s *= 2.0
var mat := _get_item_surface(PATH_PREFIX, "Path")
mat.albedo_color = color
# Fix start and ends material path
for name in [START_NAME, END_NAME, END_2_NAME]:
var item := self.mesh_library.find_item_by_name(name)
var mesh := self.mesh_library.get_item_mesh(item) as ArrayMesh
mesh.surface_set_material(0, mat)
var inactive_particles = {} # { cell:Vector3 : InactiveParticles }
func _add_inactive_particles(season):
for lift in get_lifts():
var particles = load("res://particles/LiftParticles.tscn").instance()
inactive_particles[lift] = particles
for door in get_doors():
var particles = load("res://particles/DoorParticles.tscn").instance()
inactive_particles[door] = particles
for hole in get_holes():
var particles = load("res://particles/HoleParticles.tscn").instance()
inactive_particles[hole] = particles
for activable in inactive_particles:
var rot = get_cell_quat(activable)
var particles = inactive_particles[activable]
particles.transform *= Transform(rot)
particles.translation = activable + NODE_OFFSET + Vector3(0,0.5,0)
add_child(particles)
var orb = get_cell_orb(activable)
particles.set_orb(orb, season)
func _get_tree_items():
var prefix = "Tree"
var items = []; for cell in get_used_cells():
var item = get_cell_itemv(cell)
if self.mesh_library.get_item_name(item).begins_with(prefix):
items.append(item)
return items
func _get_trees():
var items = _get_tree_items()
var trees = []; for cell in get_used_cells():
if items.has(get_cell_itemv(cell)):
trees.append(cell)
return trees
func _replace_trees_season(season):
var trees = _get_trees()
if season == Season.SPRING or trees.empty(): return
var new_name = "Tree-%s" % Season.name(season)
var extras = load("res://models/ImportedExtras.tscn").instance()
var tree = extras.find_node("*_"+new_name, false); assert(tree)
Helper.remove_from_parent(tree)
for cell in trees:
var rot = get_cell_quat(cell)
var node = tree.duplicate()
node.transform *= Transform(rot)
node.translation = cell + NODE_OFFSET
add_child(node)
set_cell_itemv(cell, GridMap.INVALID_CELL_ITEM)
var snow # SnowParticles?
var season = Season.SUMMER setget set_season
func set_season(value):
season = value
if season == Season.WINTER and not snow:
var SnowParticles = load("res://particles/Snow.tscn")
snow = SnowParticles.instance()
add_child(snow); move_child(snow, orb_panel.get_index())
snow.emitting = true
_update_path_color()
var SCENERY_NAME = "Straight-2"
_get_item_surface(SCENERY_NAME, "Scenery-top").albedo_color = Season.scenery_color(value)
var PLANTS_SCENERY_NAME = "Straight-6"
_get_item_surface(PLANTS_SCENERY_NAME, "Plants").albedo_color = Season.plants_color(value)
#var TORCH_BLUE = "B-Torch"
#_set_item_surface_color(TORCH_BLUE, "Torch-2", Color(0,0,0))
var nightmare = (ambience == Ambience.NIGHTMARE)
# Update activable material color
var orb_names = {
Orb.BLUE: "Blue", Orb.GREEN: "Green", Orb.PURPLE: "Purple",
Orb.RED: "Red", Orb.YELLOW: "Yellow", Orb.GRAY: "Gray" }
for orb in orb_names:
var name = "%s-%s" % [Orb.prefix(orb), SUFFIXES[Type.TORCH]] # Torch item name
var orb_name = orb_names[orb] # Orb material name
var mat = _get_item_surface(name, orb_name)
var color = Orb.color(orb, season)
if nightmare: color = color.darkened(0.7)
mat.albedo_color = color
mat.flags_unshaded = nightmare
_add_inactive_particles(season)
_replace_trees_season(season)
_update_music_player()
func _update_music_player():
music_player.set_season(season, ambience == Ambience.NIGHTMARE)
func set_input_enabled(enabled):
orb_panel.enabled = enabled
if not enabled:
for spot in get_spots(): enable_hotspot(spot, false)
else:
reload_spots()
func _test_level_integrity():
Helper.test()
var start = get_start()
assert( start )
assert( [1,2].has(get_ends().size()) )
assert( camera is GameCamera )
#assert( self.mesh_library.resource_path == "res://models/ImportedMeshLib_8.meshlib" )
# Check that two activables are *not* neighboors
var activables = get_activables() + get_orbs()
for activable in activables:
for neighbor in _neighbors(activable):
assert(not Array(activables).has(neighbor))
# Check that items exist in library (and here are no path extension)
for cell in get_used_cells():
var item = get_cell_itemv(cell)
assert(Array(self.mesh_library.get_item_list()).has(item))
assert(self.mesh_library.get_item_mesh(item))
assert(item != PATH_EXT) # Notice: Remove all path extensions from grid
# Check the reachability of all cells
for cell in get_used_cells():
if is_walkable(cell) and cell != start:
assert( path_for(Actor._HOLLOW, cell, start).size() >= 2 )
# Check that holes have only path same-level neighbors
for hole in get_holes():
for neighboor in edges(hole):
var item = get_cell_itemv(neighboor)
assert( self.mesh_library.get_item_name(item).begins_with(PATH_PREFIX) )
assert( neighboor.y == hole.y )
func _ready():
add_child(camera)
get_viewport().connect("size_changed", self, "_on_frame_changed")
call_deferred("start")
func _on_frame_changed():
if snow: snow.position.x = get_viewport().size.x / 2
func get_walk_angle(from): # The angle (radians) to walkable cell next to `from`
if _DEBUG_:
var ns = 0; for n in _neighbors(from): ns += int(is_walkable(n)); assert(ns == 1) # DEBUG
var next; for cell in _neighbors(from):
if is_walkable(cell):
next = cell; break
return atan2(from.x-next.x, from.z-next.z)
var _level_angle
func get_level_angle(): # Get angle (radians) to player's back (i.e. first move to the top-right)
if _level_angle == null:
_level_angle = get_walk_angle(get_start())
return _level_angle
func start():
cell_scale = 1+1e-3 # Fixing cells aliasing
var is_nightmare = name.begins_with("Nightmare")
self.ambience = Ambience.NIGHTMARE if is_nightmare else Ambience.DEFAULT
self.season = season # Force color theme once loaded
connect("_spots_updated", self, "_on_spots_updated")
connect("end_accessible", self, "_on_end_accessible")
add_child(player)
player.translation = get_begin() + PLAYER_OFFSET
player.rotation.y = get_walk_angle(get_begin())
player.speed_factor = PLAYER_SPEED
player.season = season
add_child(audio_player)
if _DEBUG_: _test_level_integrity()
randomize()
init_path_extension()
init_ocean()
setup_camera()
setup_torchs()
setup_activables()
var basename = filename.get_file().get_basename()
if _DEBUG_: OS.set_window_title("[DEBUG] Noon – %s" % basename) # DEBUG
init_orb_levels(basename)
for orb in progression.available_orbs:
orbs[orb] = orbs.get(orb, 0)
if _DEBUG_:
var shown_orb = [Orb.BLUE, Orb.GREEN, Orb.PURPLE, Orb.RED]
for orb in shown_orb: add_orb_level(orb) # DEBUG
add_child(orb_panel)
orb_panel.ambience = ambience
orb_panel.orb_levels = orbs
orb_panel.connect("absorbed", self, "start_absorbing")
orb_panel.connect("released", self, "start_releasing")
orb_panel.connect("paused", self, "_on_paused")
orb_panel.set_visible(true, true)
play_animation(Player.ANIMS.IDLE)
set_input_enabled(true)
reload_spots()
func init_orb_levels(name: String) -> Dictionary:
if get_begin() != get_start():
name += "/%s" % start_name
var ORB_LEVELS := { # The number of orbs available at the level start
"Level2": { Orb.BLUE: 1, Orb.GREEN: 1 },
"Level10":{ Orb.BLUE: 1 },
"Level11":{ Orb.GREEN: 1 },
"Level12":{ Orb.BLUE: 1 },
"Level23":{ Orb.GREEN: 1 },
"Level32/End":{ Orb.PURPLE: 1 },
"Outro": { Orb.BLUE: 1, Orb.GREEN: 1, Orb.PURPLE: 1, Orb.RED: 1 }
}
return ORB_LEVELS.get(name, {})
func init_ocean():
var path = "res://maps/dist-%s.png" % name
var texture
if ResourceLoader.exists(path):
texture = load(path)
elif _DEBUG_:
var map = generate_distmap(true)
if map:
map.save_png(path)
texture = ImageTexture.new()
texture.create_from_image(map, Texture.FLAG_MIPMAPS)
var bounds = get_bounds_margin_all(15)
var center = bounds.position + (bounds.size / Vector3(2,2,2))
var node = MeshInstance.new(); node.name = "Ocean"
node.translation = Vector3(center.x-0.25, bounds.position.y-0.1, center.z-0.25)
node.rotation.x = -PI/2
add_child(node)
var mesh = QuadMesh.new()
mesh.size = Vector2(bounds.size.x, bounds.size.z)
node.mesh = mesh
var mat = preload("res://materials/OceanShader.tres")
assert(mat is ShaderMaterial)
mat.set_shader_param("dist_map", texture)
var shore_color = Season.shore_color(season)
var far_color = Season.far_color(season)
if ambience == Ambience.NIGHTMARE:
shore_color = shore_color.darkened(0.85); far_color = far_color.darkened(0.85)
mat.set_shader_param("shore_color", shore_color)
mat.set_shader_param("far_color", far_color)
node.set_surface_material(0, mat)
func init_path_extension(from_start:=true, from_ends:=true):
var directions = {} # { cell:Vector3 : direction:Vector3 }
var cells := PoolVector3Array()
if from_start: cells += PoolVector3Array([get_start()])
if from_ends: cells += get_ends()
for cell in cells:
for neighboor in _neighbors(cell):
if is_walkable(neighboor):
assert(not directions.has(cell))
directions[cell] = cell-neighboor
for cell in directions:
_add_extension_path(cell, directions[cell])
func _get_level_bottom():
var y = 0; for cell in get_used_cells():
if is_walkable(cell): y = min(y, cell.y)
return y
func _add_extension_path(from_cell, direction, length=13):
assert(abs(direction.x+direction.z) == 1 and direction.y == 0)
for i in range(1, length+1):
var cell = from_cell + direction * Vector3(i,i,i)
assert(cell != from_cell)
set_cell_itemv(cell, PATH_EXT)
# Add scenery and wave items on path right/left
var SCENERY_NAME = "Straight-2"
var item = self.mesh_library.find_item_by_name(SCENERY_NAME); assert(item != -1)
var WAVE_NAME = "Wave-Straight"
var wave = self.mesh_library.find_item_by_name(WAVE_NAME); assert(wave != -1)
var shows_waves = (ambience != Ambience.NIGHTMARE and cell.y == _get_level_bottom()) # Sea level
var angle = atan2(direction.x, direction.z)
var orientation = rad2orientation(angle)
var offset = direction.rotated(UP, PI/2)
set_cell_itemv(cell + offset, item, orientation)
if shows_waves:
set_cell_itemv(cell + offset * Vector3(2,2,2) - Vector3(0,1,0), wave, orientation)
orientation = rad2orientation(angle+PI)
offset = direction.rotated(UP, -PI/2)
set_cell_itemv(cell + offset, item, orientation)
if shows_waves:
set_cell_itemv(cell + offset * Vector3(2,2,2) - Vector3(0,1,0), wave, orientation)
var TORCH_PARTICLE_OFFSET = AREA_OFFSET + Vector3(0,0.6,0)
var torch_particles = {} # {Vector3 : TorchParticles}
func setup_torchs():
for torch in get_torches():
var TorchParticles = load("res://particles/TorchParticles.tscn")
var particles = TorchParticles.instance()
particles.translation = torch + TORCH_PARTICLE_OFFSET
particles.orb = get_cell_orb(torch)
add_child(particles)
particles.emitting = false
torch_particles[torch] = particles
var absorbed = not is_torch_light(torch)
set_cell_absorbed(torch, absorbed, false)
func setup_camera():
camera.set_default_environment(ambience)
camera.angle = get_level_angle()
camera.make_current()
var NODE_OFFSET := AREA_OFFSET # Activable node container offset with underlying cell
var activables := {} # { cell:Vector3 : Activable }
func setup_activables():
for cell in get_activables() + get_orbs():
var orb := get_cell_orb(cell)
# Add only the activable movable part to grid
var name: String
if is_door(cell): name = "Door"
elif is_hole(cell): name = "Hole"
elif is_lift(cell): name = "Lift"
elif is_orb(cell): name = "Orb"
else: continue
var color := Orb.color(orb, season)
var activable := Activable.new(name, color, ambience == Ambience.NIGHTMARE)
add_child(activable); activables[cell] = activable
# Fix activable position/orientation
var rot := get_cell_quat(cell)
activable.transform *= Transform(rot)
activable.translation = cell + NODE_OFFSET
# Set cell with inactive mesh
var absorbed := not can_absorb(cell, orb)
if is_orb(cell): absorbed = is_orb_picked(cell)
var inactive = {
Type_.DOOR_CLOSED: Type.DOOR,
Type.HOLE: Type_.HOLE_EMPTY,
Type_.LIFT_UP: Type.LIFT }
var item := get_cell_itemv(cell)
var type := _item_type(item)
if inactive.has(type):
item = _item_for(inactive[type], orb)
var orientation := get_cell_item_orientationv(cell)
set_cell_itemv(cell, item, orientation)
# Remove lift top and orb from cell mesh
if is_lift(cell) or is_orb(cell):
var mesh := self.mesh_library.get_item_mesh(item)
var surface_name = {
Orb.BLUE: "Blue", Orb.GREEN: "Green", Orb.PURPLE: "Purple",
Orb.RED: "Red", Orb.YELLOW: "Yellow", Orb.GRAY: "Gray" }[orb]
var surface := Helper.get_surface_named(mesh, surface_name); assert(surface != null)
var mat := mesh.surface_get_material(surface).duplicate()
mat.flags_transparent = true
mat.albedo_color.a = 0
mesh.surface_set_material(surface, mat)
set_cell_absorbed(cell, absorbed, false)
# Callbacks
var accessible_ends = []
func _on_spots_updated(spots):
var ends := []
for spot in spots:
if spot != get_begin() and (Array(get_ends()).has(spot) or spot == get_start()):
var end_name = end_name_for(spot); assert(end_name)
if not accessible_ends.has(spot):
emit_signal("end_accessible", end_name)
ends.append(spot)
accessible_ends = ends
func _get_end_angle(end_index:=0):
var end = get_ends()[end_index]
var ext_cell; for neighbor in _neighbors(end):
if get_cell_itemv(neighbor) == PATH_EXT: ext_cell = neighbor
assert(ext_cell)
return atan2(ext_cell.x-end.x, ext_cell.z-end.z) - PI/2
func _on_end_accessible(end_name: String):
if ambience == Ambience.NIGHTMARE:
camera.set_ambiant_energy(0.15, 2.0)
# Move player to end
var end = get_ends()[get_end_names().find(end_name)]
move_player(end)
# Move wolf to end
var wolf = load("res://models/wolf.tscn").instance()
add_child(wolf); glide(wolf, get_begin(), end)
# Add moon light reflect near to end
var moon = load("res://models/MoonLight.tscn").instance()
var offset = Vector3(-1.5, _get_level_bottom(), 3.5)
var angle = _get_end_angle()
moon.translation = get_start() + offset.rotated(UP, angle) * 0.8
moon.opacity = 0; add_child(moon)
var tween = moon.get_node("Tween")
tween.interpolate_property(moon, "opacity", 0, 1,
0.5, Tween.TRANS_LINEAR, Tween.EASE_IN); tween.start()
# Pause Menu
func _on_paused():
toggle_pause()
var pause_menu
func toggle_pause():
if pause_menu:
Helper.remove_from_parent(pause_menu)
pause_menu = null
get_tree().paused = false
else:
pause_menu = load("res://PauseMenu.tscn").instance()
pause_menu.connect("resumed", self, "toggle_pause")
pause_menu.connect("restarted", self, "_on_restarted")
pause_menu.set_sounds_mute(audio_player.muted)
pause_menu.connect("sounds_toggled", self, "_on_sounds_toggled")
pause_menu.set_music_mute(music_player.muted)
pause_menu.connect("music_toggled", self, "_on_music_toggled")
add_child(pause_menu)
get_tree().paused = true
func _on_sounds_toggled(enabled):
audio_player.muted = not enabled
func _on_music_toggled(enabled):
music_player.muted = not enabled
func _on_restarted():
emit_signal("game_restarted")
func get_player_cell():
return round3(player.translation - PLAYER_OFFSET)
func face_player(to):
var rotation = player.rotation
player.look_at(to + PLAYER_OFFSET, UP)
var angle = player.rotation.y - rotation.y
player.rotation = rotation
var anim = rotation_anim(angle)
if anim != null:
queue_animation(anim)
else:
call_deferred("emit_signal", ANIM_DONE_SIGNAL, anim)
func rotation_anim(radians):
var angle = rad2deg(shortest(radians))
angle = round(angle / 90) * 90
var anim
if angle == 90.0:
anim = Anim.TURN_LEFT
elif angle == -90.0:
anim = Anim.TURN_RIGHT
elif abs(angle) == 180.0:
anim = Anim.HALF_TURN
else:
assert(abs(angle) < 1e-3)
return anim
func move_player(to: Vector3):
var from = get_player_pos()
if to == from: return # DEBUG (`move_player` should not be called if same position)
var id = closest_reachable_path_for(Actor.PLAYER, from, to, null, true)
var path := yield(self, id)[0] as PoolVector3Array
walk(player, path)
_on_reaching_cell(to)
func walk(node: Spatial, path: Array):
set_input_enabled(false)
face_player(path[1])
yield(self, ANIM_DONE_SIGNAL)
var should_run = (path.size() > 3)
audio_player.play(Sound.RUN if should_run else Sound.WALK)
var tween = player.get_default_tween()
tween.start()
var delay := 0.0
var tweens := 0
var from = node.translation
path.pop_front()
var index := 0; while true:
if index >= path.size(): break # Create custom loop to inc `index` into loop
var anim: int = Anim.RUN if should_run else Anim.WALK
var cell := path[index] as Vector3
var to := get_lift_bottom(cell) + PLAYER_OFFSET
to.y += int(is_lift_up(cell))
if index == path.size()-1: # Last cell (cannot be turn or stairs)
pass
elif is_stairs(cell):
var ascending = (abs(from.y-to.y) < 1e-3)
assert(ascending or abs(from.y-(to.y+1)) < 1e-3) # or descending
anim = Anim.ASCEND if ascending else Anim.DESCEND
to = path[index+1]+PLAYER_OFFSET
index += 1
else: # Straight path or turn
var next = path[index+1]+PLAYER_OFFSET
var before = to-from; var after = next-to
#assert(abs(before.x)+abs(before.z) == 1 and abs(after.x)+abs(after.z) == 1) # DEBUG
var angle = shortest(atan2(after.x,after.z)-atan2(before.x,before.z))
var turn_anim = rotation_anim(angle)
if turn_anim != null: # Turn
anim = turn_anim
# Set `to` into turn interior for more natural path
to = from.linear_interpolate(next, 0.5).linear_interpolate(to, 0.5)
else: # Straight path
pass
if anim == Anim.RUN and index == 0:
anim = Anim.START
if anim == Anim.RUN and index == path.size()-1:
anim = Anim.STOP
var duration := player.duration(anim)
tween.interpolate_property(node, "translation", from, to,
duration, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT, delay); delay += duration
tweens += 1
queue_animation(anim)
from = to; index += 1
queue_animation(Anim.IDLE)
for _i in tweens:
yield(tween, "tween_completed")
var to = from - PLAYER_OFFSET
audio_player.stop_sound(Sound.RUN if should_run else Sound.WALK)
_on_walked(node, to)
func _on_walked(node: Spatial, to: Vector3):
if to == get_start():
prints("(!) Player on start:", to)
_on_level_finished(to)
else:
var index := Array(get_ends()).find(to)
if index != -1:
prints("(!) Player on end:", ["End #1", "End #2"][index])
_on_level_finished(to)
reload_spots()
check_orb_nearby()
set_input_enabled(true)
func glide(node, from, to, speed=PLAYER_SPEED*2): # speed in cells per seconds
var tween = Tween.new()
add_child(tween); tween.start()
var id = closest_reachable_path_for(Actor._HOLLOW, from, to, null, true)
var path := yield(self, id)[0] as PoolVector3Array
for i in path.size()-2:
var current = path[i]; if is_lift(current): current.y = get_lift_bottom(current).y + 1
var next = path[i+1]; if is_lift(next): next.y = get_lift_bottom(next).y + 1
tween.interpolate_property(node, "translation",
current + PLAYER_OFFSET, next + PLAYER_OFFSET, 1.0/speed,
Tween.TRANS_LINEAR, Tween.EASE_IN)
var angle = atan2(next.x-current.x, next.z-current.z) - PI/2
var to_angle = node.rotation; to_angle.y += shortest(angle - node.rotation.y)
tween.interpolate_property(node, "rotation",
node.rotation, to_angle, 1.0/speed,
Tween.TRANS_LINEAR, Tween.EASE_IN)
yield(tween, "tween_completed")
yield(tween, "tween_completed")
Helper.remove_from_parent(tween)
func get_end_names() -> Array:
for level in END_NAMES.keys(): # Use `keys()` to keep sorted
if filename.get_file().find(level) != -1:
return END_NAMES[level]
return END_NAMES["*"]
func end_name_for(cell: Vector3):
if cell == get_begin():
pass
elif cell == get_start():
return START_NAME
elif Array(get_ends()).has(cell):
var index = Array(get_ends()).find(cell)
return get_end_names()[index]
return null
func _on_reaching_cell(cell):
var end_name = end_name_for(cell)
if end_name:
emit_signal("end_reached", end_name)
func _on_level_finished(cell):
if cell == get_begin():
return
var end_name = end_name_for(cell); assert(end_name)
audio_player.play_sound(Sound.LEVEL_END)
audio_player.stop_sound(Sound.OCEAN)
emit_signal("finished", end_name)
var event_pos: Vector2
var ray_origin # Vector3?
var ray_target # Vector3?
func _unhandled_input(event):
if event is InputEventMouseButton and event.pressed and event.button_index == BUTTON_LEFT:
self.event_pos = event.position
self.ray_origin = camera.project_ray_origin(event.position)
self.ray_target = ray_origin + camera.project_ray_normal(event.position) * 1_000
func _physics_process(delta):
if ray_origin != null and ray_target != null:
var space_state = PhysicsServer.space_get_direct_state(get_world().get_space())
var hit = space_state.intersect_ray(ray_origin, ray_target)
# Get selected spot
if not hit.empty() and hit.collider.is_ray_pickable():
var to = hit.collider.global_transform.origin - AREA_OFFSET
to.y -= int(is_lift_up(to))
move_player(to)
else: # If no spot selected
# Find the closest to click
var closest_spot; var min_distance = INF
for spot in get_spots():
var screen_pos := camera.unproject_position(spot)
var distance = self.event_pos.distance_squared_to(screen_pos)
if distance < min_distance:
closest_spot = spot
min_distance = distance
# Move the boy on closer spot to him on path to `closest_spot`
assert(closest_spot != null)
var path = path_for(Actor.PLAYER, get_player_cell(), closest_spot)
for cell in path:
if Array(get_spots()).has(cell):
move_player(cell); break
ray_origin = null; ray_target = null
func _unhandled_key_input(event):
if not event.pressed: return
match event.scancode:
KEY_ESCAPE:
toggle_pause()
KEY_SPACE: # DEBUG
if _DEBUG_: reload_spots()
func check_orb_nearby():
var pos = get_player_pos()
for cell in _neighbors(pos):
if is_orb(cell) and not is_orb_picked(cell):
var orb = get_cell_orb(cell)
progression.available_orbs.append(orb)
add_orb_level(orb, 1)
pick_orb(cell)
audio_player.play(Sound.TAKE_ORB)
func is_player_on_lift() -> bool:
return is_lift(get_player_pos())
### Get spots to move player
var _spots # PoolVector3Array?, Cached spots
func get_spots() -> PoolVector3Array:
if _spots == null:
reload_spots()
return _spots
func _get_orb_positions() -> PoolVector3Array: # Returns closest orb positions to player, where it can walk on to pick them
var cells := PoolVector3Array()
for cell in get_cells_for(Type.ORB):
if not is_orb_picked(cell):
var spot; var distance = INF
for neighbor in _neighbors(cell):
var d = get_player_pos().distance_to(neighbor)
if is_walkable(neighbor) and d < distance:
distance = d; spot = neighbor
if spot != null:
cells.append(spot)
return cells
func _update_spots():
var concerns = [get_start()] + Array(get_ends()) + Array(_get_orb_positions())
concerns.erase(get_begin())
var spots := []
for cell in concerns:
var id := closest_reachable_path_for(Actor.PLAYER, get_player_pos(), cell, null, true)
var path := Array(yield(self, id)[0] as PoolVector3Array)
for cell in path:
if is_lift(cell): spots.append(get_lift_bottom(cell))
spots.append(path.back())
Helper.distinct(spots)
spots.erase(get_player_pos())
for spot in spots: assert(is_valid(spot) and is_walkable(spot)) # DEBUG
emit_signal("_spots_updated", PoolVector3Array(spots))
var _reloading := false
func reload_spots():
if _reloading: return
call_deferred("_reload_spots"); _reloading = true
func _reload_spots():
#if not force and not level_animation_player.is_idle(): return # Wait for all animations to finish
prints("=== Reload Spots ===", "[%s]" % get_player_pos())
var ts = OS.get_ticks_msec()
_update_spots()
_spots = yield(self, "_spots_updated")
prints("Update spots:", _spots, "(%sms)" % (OS.get_ticks_msec()-ts)) # DEBUG
disable_all_hotspots()
for spot in get_spots():
enable_hotspot(spot, true)
# Remove start if the player cannot go to previous level
#for position in []+spots:
# if position == start_node.global_transform.origin:
# if LevelsMap.new().level_before(get_level_name(), start_name) == null:
# spots.erase(position)
_reloading = false
update_camera()
var hotspots = []
func enable_hotspot(cell: Vector3, enabled:=true):
.enable_hotspot(cell, enabled)
hotspots.append(cell) if enabled else hotspots.erase(cell)
var position = cell + Vector3(0,1,0)
add_spot_node(position) if enabled else remove_spot_node(position)
func disable_all_hotspots():
for spot in []+hotspots:
enable_hotspot(spot, false)
assert(hotspots.empty() and spot_nodes.empty())
var _activables := PoolVector3Array()
func get_activables() -> PoolVector3Array:
if _activables.size() == 0:
_activables = get_doors() + get_holes() + get_lifts() + get_torches()
return _activables
func get_absorbables(orb: int) -> PoolVector3Array:
var cells := PoolVector3Array()
for cell in get_activables():
if can_absorb(cell, orb):
cells.append(cell)
return cells