waited far too long for an initial commit but here we are

This commit is contained in:
2023-11-08 14:28:55 +11:00
commit 0427a58635
299 changed files with 10191 additions and 0 deletions

97
Scripts/AStarGraph3D.gd Normal file
View File

@ -0,0 +1,97 @@
extends Node3D
class_name AStarGraph3D
@export var grid_size := Vector2(21, 13)
@export var point_gap := 1.2
var non_build_locations = []
var astar := AStar3D.new()
#TODO generalize this better
@export var start : Node3D
@export var end : Node3D
@export var spawner : EnemySpawner
@export var visualized_path : VisualizedPath
var tower_base_scene = load("res://Scenes/tower_base.tscn")
var tower_frame_scene = load("res://Scenes/tower_frame.tscn")
var tower_bases = []
func toggle_point(point_id):
networked_toggle_point.rpc(point_id)
func point_is_build_location(point_id):
return !non_build_locations.has(point_id)
func test_path_if_point_toggled(point_id):
if astar.is_point_disabled(point_id):
astar.set_point_disabled(point_id, false)
else:
astar.set_point_disabled(point_id, true)
var result = find_path()
if astar.is_point_disabled(point_id):
astar.set_point_disabled(point_id, false)
else:
astar.set_point_disabled(point_id, true)
return result
@rpc("reliable", "any_peer", "call_local")
func networked_toggle_point(point_id):
if astar.is_point_disabled(point_id):
astar.set_point_disabled(point_id, false)
else:
astar.set_point_disabled(point_id, true)
var base = tower_base_scene.instantiate()
base.position = astar.get_point_position(point_id)
tower_bases.append(base)
add_child(base)
find_path()
func find_path() -> bool:
var path = astar.get_point_path(astar.get_point_count() - 2, astar.get_point_count() - 1)
if !path.is_empty():
var curve = Curve3D.new()
for point in path:
curve.add_point(point)
spawner.path.curve = curve
spawner.path.spawn_visualizer_points()
return true
return false
func make_grid():
for x in grid_size.x:
for y in grid_size.y:
var point_position = Vector3((x - floori(grid_size.x / 2)) * point_gap, 0.5, (y - floori(grid_size.y / 2)) * point_gap)
astar.add_point(int(x * grid_size.y + y), point_position)
var frame = tower_frame_scene.instantiate()
frame.position = point_position
add_child(frame)
for x in grid_size.x:
for y in grid_size.y:
var point_id = grid_size.y * x + y
if x > 0:
var north_point_id = grid_size.y * (x - 1) + y
astar.connect_points(point_id, north_point_id, false)
if x < grid_size.x - 1:
var south_point_id = grid_size.y * (x + 1) + y
astar.connect_points(point_id, south_point_id, false)
if y > 0:
var east_point_id = grid_size.y * x + (y - 1)
astar.connect_points(point_id, east_point_id, false)
if y < grid_size.y - 1:
var west_point_id = grid_size.y * x + (y + 1)
astar.connect_points(point_id, west_point_id, false)
non_build_locations.append(astar.get_point_count())
astar.add_point(astar.get_point_count(), start.global_position)
for x in grid_size.y:
astar.connect_points(int(astar.get_point_count() - 1), x)
non_build_locations.append(astar.get_point_count())
astar.add_point(astar.get_point_count(), end.global_position)
for x in grid_size.y:
astar.connect_points(astar.get_point_count() - 1, int(grid_size.y * (grid_size.x - 1) + x))

42
Scripts/DebugMesh.gd Normal file
View File

@ -0,0 +1,42 @@
@tool
extends MeshInstance3D
class_name DebugMesh
func _ready():
var mat = StandardMaterial3D.new()
mesh = ImmediateMesh.new()
mat.no_depth_test = true
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.vertex_color_use_as_albedo = true
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
set_material_override(mat)
func clear():
mesh.clear_surfaces()
func draw_line(begin_pos: Vector3, end_pos: Vector3, color: Color = Color.RED) -> void:
mesh.surface_begin(Mesh.PRIMITIVE_LINES)
mesh.surface_set_color(color)
mesh.surface_add_vertex(begin_pos)
mesh.surface_add_vertex(end_pos)
mesh.surface_end()
func draw_sphere(center: Vector3, radius: float = 1.0, color: Color = Color.RED) -> void:
var step: int = 30
var sppi: float = 2 * PI / step
var axes = [
[Vector3.UP, Vector3.RIGHT],
[Vector3.RIGHT, Vector3.FORWARD],
[Vector3.FORWARD, Vector3.UP]
]
for axis in axes:
mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP)
mesh.surface_set_color(color)
for i in range(step + 1):
mesh.surface_add_vertex(center + (axis[0] * radius)
.rotated(axis[1], sppi * (i % step)))
mesh.surface_end()

78
Scripts/HUD.gd Normal file
View File

@ -0,0 +1,78 @@
extends CanvasLayer
class_name HUD
var last_lives_count = 120
@export var wave_count : Label
@export var lives_count : Label
@export var enemy_count : Label
@export var currency_count : Label
@export var crosshair : TextureRect
@export var minimap : TextureRect
@export var minimap_cam : MinimapCamera3D
@export var minimap_viewport : SubViewport
@export var fps_label : Label
var minimap_anchor : Node3D
@export var enemy_sprites : Array[TextureRect]
@export var enemy_counts : Array[Label]
func _process(_delta: float) -> void:
fps_label.text = "FPS: " + str(Engine.get_frames_per_second())
func set_wave_count(value):
wave_count.text = str(value)
func set_lives_count(value):
lives_count.text = str(value)
for x in last_lives_count - value:
$LivesBar.take_life()
last_lives_count = value
func set_enemy_count(value):
enemy_count.text = "Enemies Remaining: " + str(value)
func set_upcoming_wave(value):
var frame_count = 0
for x in enemy_sprites.size():
enemy_sprites[x].set_visible(false)
enemy_counts[x].set_visible(false)
for enemy in value:
enemy_sprites[frame_count].texture = enemy.icon
enemy_counts[frame_count].text = str(value[enemy])
enemy_sprites[frame_count].set_visible(true)
enemy_counts[frame_count].set_visible(true)
frame_count += 1
func set_currency_count(value):
currency_count.text = str(value)
func set_crosshair_visible(value : bool):
crosshair.set_visible(value)
func maximise_minimap(anchor):
minimap_cam.anchor = anchor
minimap.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
minimap.offset_bottom = -40
minimap.offset_top = 40
minimap.offset_left = 40
minimap.offset_right = -40
minimap_viewport.size = Vector2(1840, 1000)
minimap_cam.size = 30
func minimize_minimap(anchor):
minimap_cam.anchor = anchor
minimap.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT)
minimap.offset_right = -40
minimap.offset_top = 40
minimap.offset_left = -256
minimap.offset_bottom = 256
minimap_viewport.size = Vector2(256, 256)
minimap_cam.size = 15

View File

@ -0,0 +1,7 @@
extends Resource
class_name TowerStats
@export var can_target : Data.TargetType
@export var damage := 10.0
@export var fire_range := 20.0
@export var fire_rate := 1.0

View File

@ -0,0 +1,6 @@
extends Resource
class_name WeaponStats
@export var damage : int
@export var fire_rate : float
@export var fire_range : float

13
Scripts/Resources/card.gd Normal file
View File

@ -0,0 +1,13 @@
extends Resource
class_name Card
enum Faction {GENERIC}
@export var title : String
@export var rarity : Data.Rarity
@export var faction : Faction
@export var sprite : AtlasTexture
@export var turret : PackedScene
@export var weapon : PackedScene
@export var weapon_stats : WeaponStats
@export var tower_stats : TowerStats

View File

@ -0,0 +1,12 @@
extends Resource
class_name Enemy
@export var title = "dog"
@export var target_type : Data.EnemyType
@export var icon : Texture
@export var sprite : AtlasTexture
@export var spawn_power := 10
@export var health = 100
@export var penalty = 10
@export var movement_speed = 0.5
@export var spawn_cooldown = 1.0

View File

@ -0,0 +1,6 @@
extends Resource
class_name HeroClass
@export var hero_name : String = "Default"
@export var texture : AtlasTexture
@export var deck : Array[Card]

View File

@ -0,0 +1,63 @@
extends Resource
class_name PlayerKeymap
const SAVE_PATH := "user://keymap.tres"
@export var title : String
@export var move_forward : InputEventKey
@export var move_backward : InputEventKey
@export var move_left : InputEventKey
@export var move_right : InputEventKey
@export var jump : InputEventKey
@export var sprint : InputEventKey
@export var interact : InputEventKey
@export var open_text_chat : InputEventKey
@export var ready : InputEventKey
@export var pause : InputEventKey
@export var equip_card_in_gauntlet : InputEventKey
@export var view_map : InputEventKey
func apply():
replace_action_event("Move Forward", move_forward)
replace_action_event("Move Backward", move_backward)
replace_action_event("Move Left", move_left)
replace_action_event("Move Right", move_right)
replace_action_event("Jump", jump)
replace_action_event("Sprint", sprint)
replace_action_event("Interact", interact)
replace_action_event("Open Text Chat", open_text_chat)
replace_action_event("Ready", ready)
replace_action_event("Pause", pause)
replace_action_event("Equip In Gauntlet", equip_card_in_gauntlet)
replace_action_event("View Map", view_map)
func replace_action_event(action_string, event):
InputMap.action_erase_events(action_string)
InputMap.action_add_event(action_string, event)
func get_current_input_map():
move_forward = InputMap.action_get_events("Move Forward")[0]
move_backward = InputMap.action_get_events("Move Backward")[0]
move_left = InputMap.action_get_events("Move Left")[0]
move_right = InputMap.action_get_events("Move Right")[0]
jump = InputMap.action_get_events("Jump")[0]
sprint = InputMap.action_get_events("Sprint")[0]
interact = InputMap.action_get_events("Interact")[0]
open_text_chat = InputMap.action_get_events("Open Text Chat")[0]
ready = InputMap.action_get_events("Ready")[0]
pause = InputMap.action_get_events("Pause")[0]
equip_card_in_gauntlet = InputMap.action_get_events("Equip In Gauntlet")[0]
view_map = InputMap.action_get_events("View Map")[0]
func save_profile_to_disk():
get_current_input_map()
ResourceSaver.save(self, SAVE_PATH)
static func load_profile_from_disk() -> PlayerKeymap:
if ResourceLoader.exists(SAVE_PATH):
return ResourceLoader.load(SAVE_PATH)
return Data.keymaps[0]

View File

@ -0,0 +1,42 @@
extends Resource
class_name PlayerPreferences
const SAVE_PATH := "user://preferences.tres"
@export var mouse_sens := 28.0
@export var invert_lookY := false
@export var invert_lookX := false
@export var toggle_sprint := false
@export var vsync_mode := 1
@export var aa_mode := 0
@export var windowed_mode := 0
@export var hfov := 100.0
func apply_graphical_settings(viewport):
DisplayServer.window_set_vsync_mode(vsync_mode)
match aa_mode:
0:
viewport.use_taa = false
viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
1:
viewport.use_taa = false
viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_FXAA
2:
viewport.use_taa = true
viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
match windowed_mode:
0:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
1:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
2:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN)
func save_profile_to_disk():
ResourceSaver.save(self, SAVE_PATH)
static func load_profile_from_disk() -> PlayerPreferences:
if ResourceLoader.exists(SAVE_PATH):
return ResourceLoader.load(SAVE_PATH)
return PlayerPreferences.new()

View File

@ -0,0 +1,47 @@
extends Resource
class_name PlayerProfile
signal display_name_changed(old_name, new_name)
signal preferred_class_changed(old_class, new_class)
const SAVE_PATH := "user://profile.tres"
@export var display_name := "Charlie"
@export var preferred_class := 0
func to_dict() -> Dictionary:
var dict = {}
dict["display_name"] = display_name
dict["preferred_class"] = preferred_class
return dict
static func from_dict(dict) -> PlayerProfile:
var output = PlayerProfile.new()
output.display_name = dict["display_name"]
output.preferred_class = dict["preferred_class"]
return output
func set_display_name(new_display_name):
if new_display_name == display_name:
return
var old_name = display_name
display_name = new_display_name
save_profile_to_disk()
display_name_changed.emit(old_name, display_name)
func get_display_name() -> String:
return display_name
func set_preferred_class(new_preferred_class):
if new_preferred_class == preferred_class:
return
var old_class = preferred_class
preferred_class = new_preferred_class
preferred_class_changed.emit(old_class, preferred_class)
func get_preferred_class() -> int:
return preferred_class
func save_profile_to_disk():
ResourceSaver.save(self, SAVE_PATH)
static func load_profile_from_disk() -> PlayerProfile:
if ResourceLoader.exists(SAVE_PATH):
return ResourceLoader.load(SAVE_PATH)
return PlayerProfile.new()

View File

@ -0,0 +1,8 @@
extends Resource
class_name StatusStats
@export var unique := false
@export var proc_frequency := 0.0
@export var duration := 1.0
@export var potency := 1.0
@export var icon : Texture

36
Scripts/StatusEffector.gd Normal file
View File

@ -0,0 +1,36 @@
extends Node3D
class_name StatusEffector
@export var hbox : HBoxContainer
var icon_scene = preload("res://Scenes/status_icon.tscn")
var effects : Array[StatusEffect]
var icons : Array[TextureRect]
func add_effect(new_effect : StatusEffect):
var icon_present = false
for effect in effects:
if effect.stats == new_effect.stats:
icon_present = true
new_effect.expired.connect(remove_effect)
effects.append(new_effect)
if !icon_present:
var icon = icon_scene.instantiate()
icon.texture = new_effect.stats.icon
icons.append(icon)
hbox.add_child(icon)
func remove_effect(expiring_effect : StatusEffect):
effects.erase(expiring_effect)
var has_remaining_stack = false
for effect in effects:
if effect.stats == expiring_effect.stats:
has_remaining_stack = true
if !has_remaining_stack:
for icon in icons:
if icon.texture == expiring_effect.stats.icon:
icons.erase(icon)
icon.queue_free()
break

View File

@ -0,0 +1,6 @@
extends StatusEffect
class_name StatusOnFire
func proc():
affected.damage(stats.potency)

View File

@ -0,0 +1,44 @@
extends Node
class_name StatusEffect
signal expired(effect : StatusEffect)
var stats : StatusStats
var affected :
set(value):
affected = value
on_attached()
var cooldown := 0.0
var other_cooldown := 0.0
var time_existed := 0.0
func on_attached():
pass
func on_removed():
expired.emit(self)
func proc():
pass
func _ready():
other_cooldown = 1.0 / stats.proc_frequency
func _process(delta: float) -> void:
time_existed += delta
if time_existed >= stats.duration:
on_removed()
queue_free()
return
if stats.proc_frequency > 0.0:
cooldown += delta
if cooldown >= other_cooldown:
cooldown -= other_cooldown
proc()

View File

@ -0,0 +1,18 @@
extends StatusEffect
class_name StatusSticky
func on_attached():
super.on_attached()
affected.movement_speed = affected.stats.movement_speed * (1.0 - stats.potency)
func on_removed():
super.on_removed()
var siblings = get_parent().get_children()
var stickies = 0
for node in siblings:
if node is StatusSticky:
stickies += 1
if stickies == 1:
affected.movement_speed = affected.stats.movement_speed

13
Scripts/alert_popup.gd Normal file
View File

@ -0,0 +1,13 @@
extends PanelContainer
class_name AlertPopup
signal completed
func set_popup(prompt_text, dismiss_text):
$VBoxContainer/Label.text = prompt_text
$VBoxContainer/Button.text = dismiss_text
func _on_button_pressed() -> void:
completed.emit()
queue_free()

30
Scripts/card_hand.gd Normal file
View File

@ -0,0 +1,30 @@
extends Node2D
class_name CardInHand
var stats : Card
@export var rarity_sprite : Sprite2D
@export var title_text: Label
@export var damage_text_text: Label
@export var damage_text: Label
@export var fire_rate_text_text: Label
@export var fire_rate_text: Label
@export var range_text_text: Label
@export var range_text: Label
func set_card(value):
stats = value
title_text.text = stats.title
rarity_sprite.region_rect = Rect2(64 * stats.rarity, 0, 64, 64)
view_weapon()
func view_weapon():
damage_text.text = str(stats.weapon_stats.damage)
fire_rate_text.text = str(stats.weapon_stats.fire_rate)
range_text.text = str(stats.weapon_stats.fire_rate)
func view_tower():
damage_text.text = str(stats.tower_stats.damage)
fire_rate_text.text = str(stats.tower_stats.fire_rate)
range_text.text = str(stats.tower_stats.fire_rate)

59
Scripts/card_printer.gd Normal file
View File

@ -0,0 +1,59 @@
extends StaticBody3D
class_name CardPrinter
@export var cards : Array[CardInHand]
@export var item_card_scene : PackedScene
var card_available = false
@export var button_collider : CollisionShape3D
@export var button_box : Node3D
@export var choice_colliders : Array[CollisionShape3D]
func randomize_cards():
var weight_total = 0
for rarity in Data.Rarity:
weight_total += Data.rarity_weights[rarity]
var generated_rarity = randi_range(0, weight_total)
var decided_rarity := 0
for rarity in Data.Rarity:
weight_total -= Data.rarity_weights[rarity]
if generated_rarity >= weight_total:
decided_rarity = Data.Rarity[rarity]
break
var card_array = []
for x in Data.cards:
if x.rarity == decided_rarity:
card_array.append(x)
var card
for x in cards:
if card_array.size() > 0:
card = card_array.pick_random()
card_array.erase(card)
x.set_card(card)
$Node3D.set_visible(true)
for x in choice_colliders:
x.disabled = false
card_available = true
func retrieve_card(i):
$Node3D.set_visible(false)
for x in choice_colliders:
x.disabled = true
if card_available:
var card = cards[i].stats
var item = item_card_scene.instantiate() as ItemCard
item.card = card
add_child(item)
item.position += -transform.basis.z * 2
button_collider.disabled = false
button_box.position = Vector3(0,0,0)
func _on_static_body_3d_button_interacted(_value) -> void:
button_collider.disabled = true
button_box.position = Vector3(0,0,-0.2)
randomize_cards()

36
Scripts/chatbox.gd Normal file
View File

@ -0,0 +1,36 @@
extends Control
class_name Chatbox
signal opened
signal closed
var text_selected := false
var username := "default"
func _input(event: InputEvent) -> void:
if event.is_action_pressed("Open Text Chat"):
if text_selected:
closed.emit()
$VBoxContainer/LineEdit.deselect()
$VBoxContainer/LineEdit.visible = false
text_selected = false
if $VBoxContainer/LineEdit.text.length() != 0:
if $VBoxContainer/LineEdit.text.begins_with("/"):
Game.parse_command($VBoxContainer/LineEdit.text, multiplayer.get_unique_id())
else:
rpc("append_message", username, $VBoxContainer/LineEdit.text)
$VBoxContainer/LineEdit.clear()
else:
opened.emit()
$VBoxContainer/LineEdit.visible = true
$VBoxContainer/LineEdit.grab_focus()
text_selected = true
func change_username(old_name, new_name):
append_message("server", old_name + " has changed their display name to " + new_name)
@rpc("reliable","call_local","any_peer")
func append_message(user, content):
$VBoxContainer/RichTextLabel.append_text("[" + user + "] " + content + "\n")

View File

@ -0,0 +1,19 @@
extends PanelContainer
class_name ConfirmationPopup
signal completed(outcome)
func set_popup(prompt_text, confirm_text, cancel_text):
$VBoxContainer/Label.text = prompt_text
$VBoxContainer/HBoxContainer/Confirm.text = confirm_text
$VBoxContainer/HBoxContainer/Cancel.text = cancel_text
func _on_confirm_pressed() -> void:
completed.emit(true)
queue_free()
func _on_cancel_pressed() -> void:
completed.emit(false)
queue_free()

View File

@ -0,0 +1,23 @@
extends Sprite3D
@onready var label: Label = $SubViewport/Label
var time_alive := 0.0
var movement_speed := 1.0
var movement_vector : Vector3
func _ready():
var theta = deg_to_rad(40)
var z = randf_range(cos(theta), 1)
var phi = randf_range(0, 2 * PI)
var vector = Vector3(sqrt(1 - pow(z, 2)) * cos(phi), z, sqrt(1 - pow(z, 2)) * sin(phi))
movement_vector = vector.normalized()
func set_number(num):
label.text = str(num)
func _process(delta: float) -> void:
time_alive += delta
position += movement_vector * movement_speed * delta
if time_alive >= 1.0:
queue_free()

53
Scripts/data.gd Normal file
View File

@ -0,0 +1,53 @@
extends Node
var characters : Array[HeroClass]
var cards : Array[Card]
var enemies : Array[Enemy]
var keymaps : Array[PlayerKeymap]
var preferences : PlayerPreferences
var player_profile : PlayerProfile
var player_keymap : PlayerKeymap
var wall_cost := 4
var printer_cost := 10
enum TargetType {LAND = 1, AIR = 2, BOTH = 3}
enum EnemyType {LAND = 1, AIR = 2}
enum Rarity {COMMON, UNCOMMON, RARE, EPIC, LEGENDARY}
var rarity_weights = {
"COMMON" = 100,
"UNCOMMON" = 60,
"RARE" = 20,
"EPIC" = 8,
"LEGENDARY" = 1
}
func _ready() -> void:
player_profile = PlayerProfile.load_profile_from_disk()
preferences = PlayerPreferences.load_profile_from_disk()
player_keymap = PlayerKeymap.load_profile_from_disk()
preferences.apply_graphical_settings(get_viewport())
player_keymap.apply()
characters.append(preload("res://PCs/Red/red.tres"))
characters.append(preload("res://PCs/Green/green.tres"))
characters.append(preload("res://PCs/Blue/blue.tres"))
cards.append(preload("res://PCs/Universal/ClassCards/Assault/card_assault.tres"))
cards.append(preload("res://PCs/Universal/ClassCards/BombLauncher/card_grenade_launcher.tres"))
cards.append(preload("res://PCs/Universal/ClassCards/Flamethrower/card_flamethrower.tres"))
cards.append(preload("res://PCs/Universal/ClassCards/Gatling/card_gatling.tres"))
cards.append(preload("res://PCs/Universal/ClassCards/GlueLauncher/card_glue_launcher.tres"))
cards.append(preload("res://PCs/Universal/ClassCards/RocketLauncher/card_rocket_launcher.tres"))
enemies.append(preload("res://Worlds/GreenPlanet/Enemies/dog.tres"))
enemies.append(preload("res://Worlds/GreenPlanet/Enemies/dog_fast.tres"))
enemies.append(preload("res://Worlds/GreenPlanet/Enemies/dog_heavy.tres"))
enemies.append(preload("res://Worlds/GreenPlanet/Enemies/dog_boss.tres"))
enemies.append(preload("res://Worlds/GreenPlanet/Enemies/airenemy.tres"))
enemies.append(preload("res://Worlds/GreenPlanet/Enemies/airenemy2.tres"))
keymaps.append(preload("res://Resources/Keymaps/qwerty.tres"))
keymaps.append(preload("res://Resources/Keymaps/azerty.tres"))
keymaps.append(preload("res://Resources/Keymaps/dvorak.tres"))
keymaps.append(preload("res://Resources/Keymaps/colemak.tres"))
keymaps.append(preload("res://Resources/Keymaps/workman.tres"))

103
Scripts/edit_tool.gd Normal file
View File

@ -0,0 +1,103 @@
extends Node3D
class_name EditTool
@export var hero : Hero
@export var inventory : Inventory
@export var ray : RayCast3D
@export var wall_preview : TowerBase
@export var build_preview_material : StandardMaterial3D
@export var progress_bar : TextureProgressBar
var enabled := true
var point_id := -1
var obstacle_last_point : int
var valid_point := false
var is_looking_at_tower_base := false
var ray_collider
var ray_point
var interact_key_held := false
var interacted_once := false
var interact_held_time := 0.0
var interact_hold_time := 0.5
func _ready() -> void:
wall_preview.set_material(build_preview_material)
wall_preview.toggle_collision()
func _process(delta: float) -> void:
if !enabled:
ray_collider = null
ray_point = null
wall_preview.set_visible(false)
return
if interact_key_held and !interacted_once and valid_point and hero.currency >= Data.wall_cost and ray.is_colliding() and Game.level.a_star_graph_3d.point_is_build_location(point_id):
interact_held_time += delta
set_progress_percent(interact_held_time / interact_hold_time)
if interact_held_time >= interact_hold_time:
set_progress_percent(0)
interacted_once = true
build_wall()
if !interact_key_held:
interact_held_time = 0.0
interacted_once = false
set_progress_percent(0)
point_id = -1
if ray.is_colliding():
if !interact_key_held:
wall_preview.set_visible(true)
ray_collider = ray.get_collider()
ray_point = ray.get_collision_point()
is_looking_at_tower_base = ray_collider is TowerBase
if Game.level:
point_id = Game.level.a_star_graph_3d.astar.get_closest_point(ray_point)
if !Game.level.a_star_graph_3d.point_is_build_location(point_id) or hero.currency < Data.wall_cost:
wall_preview.set_visible(false)
else:
var point_position = Game.level.a_star_graph_3d.astar.get_point_position(point_id)
wall_preview.global_position = point_position
wall_preview.global_rotation = Vector3.ZERO
if obstacle_last_point != point_id:
obstacle_last_point = point_id
if Game.level.a_star_graph_3d.test_path_if_point_toggled(point_id):
build_preview_material.albedo_color = Color.GREEN
build_preview_material.albedo_color.a = 0.8
valid_point = true
else:
build_preview_material.albedo_color = Color.RED
build_preview_material.albedo_color.a = 0.8
valid_point = false
else:
ray_collider = null
ray_point = null
is_looking_at_tower_base = false
wall_preview.set_visible(false)
func interact():
if ray_collider is TowerBase:
var tower_base = ray_collider as TowerBase
put_card_in_tower_base(tower_base)
func build_wall():
if point_id >= 0 and valid_point and hero.currency >= Data.wall_cost:
hero.currency -= Data.wall_cost
Game.level.a_star_graph_3d.toggle_point(point_id)
wall_preview.set_visible(false)
func put_card_in_tower_base(tower_base: TowerBase):
if tower_base.has_card:
inventory.add(tower_base.remove_card())
else:
tower_base.add_card(inventory.remove())
func set_progress_percent(value: float):
progress_bar.value = progress_bar.max_value * value

View File

@ -0,0 +1,36 @@
extends Sprite3D
class_name EightDirectionSprite3D
func _process(_delta: float) -> void:
var cam = get_viewport().get_camera_3d()
if !cam:
return
var tile_size = texture.region.size.x
#stupid algorithm for dummy game developers
var camera_look_dir_3D = cam.global_position.direction_to(global_position).normalized()
var a = Vector2(global_transform.basis.z.x, global_transform.basis.z.z).normalized()
var b = Vector2(camera_look_dir_3D.x, camera_look_dir_3D.z).normalized()
var dot = a.x * b.x + a.y * b.y
var det = a.x * b.y - a.y * b.x
var final = rad_to_deg(atan2(det, dot)) + 180
var t = texture.region
if final > 337.5 or final < 22.5:
t = Rect2(tile_size * 4, t.position.y, tile_size, tile_size)
elif final > 22.5 and final < 67.5:
t = Rect2(tile_size * 5, t.position.y, tile_size, tile_size)
elif final > 67.5 and final < 112.5:
t = Rect2(tile_size * 6, t.position.y, tile_size, tile_size)
elif final > 112.5 and final < 157.5:
t = Rect2(tile_size * 7, t.position.y, tile_size, tile_size)
elif final > 157.5 and final < 202.5:
t = Rect2(0, t.position.y, tile_size, tile_size)
elif final > 202.5 and final < 247.5:
t = Rect2(tile_size * 1, t.position.y, tile_size, tile_size)
elif final > 247.5 and final < 292.5:
t = Rect2(tile_size * 2, t.position.y, tile_size, tile_size)
elif final > 292.5 and final < 337.5:
t = Rect2(tile_size * 3, t.position.y, tile_size, tile_size)
texture.region = t

66
Scripts/enemy_spawner.gd Normal file
View File

@ -0,0 +1,66 @@
extends Node3D
class_name EnemySpawner
@export var path : VisualizedPath
@export var type : Data.EnemyType
@export var dest : Node3D
var signal_for_after_enemy_died
var signal_for_after_enemy_reached_goal
signal signal_for_when_enemy_spawns
var current_wave
var enemy_spawn_timers = {}
var enemies_spawned = {}
var enemies_to_spawn := 0
var done_spawning = true
@export var land_enemy_scene : PackedScene
@export var air_enemy_scene : PackedScene
func _process(delta: float) -> void:
if enemies_to_spawn == 0:
done_spawning = true
return
for x in enemy_spawn_timers:
if enemies_spawned[x] == current_wave[x]:
continue
var enemy_stats = x
enemy_spawn_timers[x] += delta
if enemy_spawn_timers[x] >= enemy_stats.spawn_cooldown:
if type == Data.EnemyType.LAND:
var enemy = land_enemy_scene.instantiate() as EnemyController
enemy.stats = enemy_stats
enemy.died.connect(signal_for_after_enemy_died)
enemy.reached_goal.connect(signal_for_after_enemy_reached_goal)
path.add_child(enemy)
enemy_spawn_timers[x] -= enemy_stats.spawn_cooldown
signal_for_when_enemy_spawns.emit()
if type == Data.EnemyType.AIR:
var enemy = air_enemy_scene.instantiate() as AirEnemyController
enemy.stats = enemy_stats
enemy.destination = dest
enemy.died.connect(signal_for_after_enemy_died)
enemy.reached_goal.connect(signal_for_after_enemy_reached_goal)
add_child(enemy)
enemy_spawn_timers[x] -= enemy_stats.spawn_cooldown
signal_for_when_enemy_spawns.emit()
enemies_spawned[x] += 1
enemies_to_spawn -= 1
func spawn_wave(value):
var relevant_enemies = {}
for x in value:
if x.target_type == type:
relevant_enemies[x] = value[x]
current_wave = relevant_enemies
enemies_to_spawn = 0
enemy_spawn_timers = {}
for x in current_wave:
enemies_to_spawn += current_wave[x]
enemy_spawn_timers[x] = 0.0
enemies_spawned[x] = 0
done_spawning = false

35
Scripts/float_and_spin.gd Normal file
View File

@ -0,0 +1,35 @@
extends RayCast3D
@export_range(0.0, 3.0) var float_height := 1.5
@export_range(0.0, 2.0) var bounce_dist := 0.5
@export_range(0.0, 2.0) var bounce_speed := 0.4
@export_range(0.0, 4.0) var spin_speed := 0.5
@export var curve: Curve
var start_height = 0.0
var dest_height = 0.0
var t = 0.0
func _ready():
start_height = position.y
#raycast downwards and position the item at a set height above the ground that the raycast
#presumably hits
#Now I know what you're thinking: "if the item is placed on the ground in the editor anyway, we can put
#the item at the correct height by simply adding the height to the existing y value that the item will have
#anyway when the game starts" but what you're not considering is that for ease of placement the model will probably
#always be given an offset from the root nodes position so as youre placing it in the editor its not clipping through
#the ground, and some items might be given more or less of this vertical offset, if we do it at runtime by actually
#checking where the ground is, you dont need to fuck around with this offset at the scene level, you just adjust
#the script variable and the item can figure itself out, yeah its fancy but its also a nice creature comfort.
if is_colliding():
start_height = get_collision_point().y + (1 * float_height) - (bounce_dist / 2.0)
dest_height = start_height + (bounce_dist / 2.0)
func _process(delta):
t += bounce_speed * delta
position.y = start_height + (dest_height - start_height) * curve.sample(t)
if t >= 1.0:
t = 0.0
rotation.y += spin_speed * delta

211
Scripts/game.gd Normal file
View File

@ -0,0 +1,211 @@
extends Node
signal wave_started(wave_number)
signal wave_finished(wave_number)
signal base_took_damage(remaining_health)
signal game_started
signal game_restarted
signal lost_game
signal won_game
signal enemy_number_changed(number_of_enemies)
var level_scene = load("res://Worlds/GreenPlanet/Levels/first_level.tscn")
var player_scene = load("res://PCs/hero.tscn")
var main_menu_scene_path = "res://Scenes/Menus/main_menu.tscn"
var multiplayer_lobby_scene_path = "res://Scenes/Menus/multiplayer_lobby.tscn"
var singleplayer_lobby_scene_path = "res://Scenes/Menus/singleplayer_lobby.tscn"
var won_game_scene = load("res://Scenes/Menus/won_game_screen.tscn")
var lose_game_scene = load("res://Scenes/Menus/lost_game_screen.tscn")
var connected_players_nodes = {}
var game_active := false
var level : Level
var enemies := 0
var objective_health := 120
var wave := 0
var upcoming_wave
var pot : int
func parse_command(text : String, peer_id : int):
if text.substr(1, 4) == "give":
var gift_name = text.substr(6) as String
var gift = Data.cards[0]
for x in Data.cards:
if x.title == gift_name:
gift = x
connected_players_nodes[peer_id].inventory.add(gift)
func spawn_level():
level = level_scene.instantiate() as Level
for x in level.enemy_spawns:
#x.path = level.a_star_graph_3d.visualized_path
x.signal_for_after_enemy_died = enemy_died
x.signal_for_after_enemy_reached_goal = damage_goal
x.signal_for_when_enemy_spawns.connect(increase_enemy_count)
add_child(level)
func spawn_players(player_array, player_profiles, chatbox_open_signal, chatbox_closed_signal):
var p_i = 0
player_array.sort()
for peer_id in player_array:
var player = player_scene.instantiate() as Hero
player.name = str(peer_id)
player.position = level.player_spawns[p_i].global_position
player.profile = player_profiles[peer_id]
player.hero_class = Data.characters[player_profiles[peer_id].preferred_class]
player.ready_state_changed.connect(ready_player)
if peer_id == multiplayer.get_unique_id():
chatbox_open_signal.connect(player.pause)
chatbox_closed_signal.connect(player.unpause)
player.set_multiplayer_authority(peer_id)
connected_players_nodes[peer_id] = player
wave_started.connect(player.exit_editing_mode)
wave_finished.connect(player.enter_editing_mode)
base_took_damage.connect(player.hud.set_lives_count)
enemy_number_changed.connect(player.hud.set_enemy_count)
add_child(player)
p_i += 1
start_game()
func ready_player(_value):
for key in connected_players_nodes:
if connected_players_nodes[key].ready_state == false:
return
for key in connected_players_nodes:
connected_players_nodes[key].ready_state = false
spawn_enemy_wave()
func spawn_enemy_wave():
wave += 1
level.a_star_graph_3d.find_path()
level.a_star_graph_3d.visualized_path.disable_visualization()
for spawn in level.enemy_spawns:
spawn.spawn_wave(upcoming_wave)
wave_started.emit(wave)
func set_upcoming_wave():
var spawn_power = WaveManager.calculate_spawn_power(wave + 1, connected_players_nodes.size())
upcoming_wave = WaveManager.generate_wave(spawn_power, level.enemy_pool)
pot = 6 + (spawn_power / 100)
func increase_enemy_count():
enemies += 1
enemy_number_changed.emit(enemies)
func enemy_died():
enemies -= 1
enemy_number_changed.emit(enemies)
for x in level.enemy_spawns:
if !x.done_spawning:
return
if enemies == 0:
end_wave()
if wave >= 20:
win_game()
func damage_goal(penalty):
enemies -= 1
enemy_number_changed.emit(enemies)
objective_health -= penalty
base_took_damage.emit(objective_health)
if objective_health <= 0:
lose_game()
elif enemies == 0:
end_wave()
if wave >= 20:
win_game()
func end_wave():
for peer_id in connected_players_nodes:
connected_players_nodes[peer_id].currency += pot / connected_players_nodes.size()
level.a_star_graph_3d.visualized_path.enable_visualization()
wave_finished.emit(wave)
set_upcoming_wave()
if wave < 20:
for key in connected_players_nodes:
connected_players_nodes[key].hud.set_upcoming_wave(upcoming_wave)
func remove_player(peer_id):
connected_players_nodes[peer_id].queue_free()
connected_players_nodes.erase(peer_id)
func start_game():
game_active = true
level.a_star_graph_3d.make_grid()
level.a_star_graph_3d.find_path()
set_upcoming_wave()
for peer_id in connected_players_nodes:
connected_players_nodes[peer_id].currency = 20
connected_players_nodes[peer_id].hud.set_upcoming_wave(upcoming_wave)
game_started.emit()
func restart_game():
#implement game reloading system
for peer_id in connected_players_nodes:
connected_players_nodes[peer_id].queue_free()
connected_players_nodes.clear()
level.queue_free()
enemies = 0
objective_health = 100
wave = 0
spawn_level()
game_restarted.emit()
pass
func lose_game():
if game_active == false:
return
game_active = false
var menu = lose_game_scene.instantiate()
add_child(menu)
lost_game.emit()
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
for peer_id in connected_players_nodes:
connected_players_nodes[peer_id].pause()
func win_game():
if game_active == false:
return
game_active = false
var menu = won_game_scene.instantiate()
add_child(menu)
won_game.emit()
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
for peer_id in connected_players_nodes:
connected_players_nodes[peer_id].pause()
func quit_to_desktop():
multiplayer.multiplayer_peer.close()
multiplayer.multiplayer_peer = null
get_tree().quit()
func scene_switch_main_menu():
multiplayer.multiplayer_peer.close()
multiplayer.multiplayer_peer = null
for node in get_children():
node.queue_free()
get_tree().change_scene_to_file(main_menu_scene_path)
func scene_switch_to_multiplayer_lobby():
get_tree().change_scene_to_file(multiplayer_lobby_scene_path)
func scene_switch_to_singleplayer_lobby():
get_tree().change_scene_to_file(singleplayer_lobby_scene_path)

View File

@ -0,0 +1,2 @@
extends Node3D
class_name GroundEnemyController

27
Scripts/health.gd Normal file
View File

@ -0,0 +1,27 @@
extends Node
class_name Health
signal health_depleted
signal health_changed(health)
@export var damage_particle_scene : PackedScene
@export var max_health := 10
var current_health
func take_damage(damage):
var marker = damage_particle_scene.instantiate()
get_tree().root.add_child(marker)
marker.set_number(damage)
marker.position = get_parent().global_position + Vector3.UP
current_health -= damage
health_changed.emit(current_health)
if current_health <= 0:
health_depleted.emit()
func heal_damage(healing):
current_health += healing
if current_health > max_health:
current_health = max_health

13
Scripts/hitbox.gd Normal file
View File

@ -0,0 +1,13 @@
extends CollisionShape3D
class_name Hitbox
signal took_damage(amount)
func damage(amount):
networked_damage.rpc(amount)
@rpc("any_peer","call_local")
func networked_damage(amount):
took_damage.emit(amount)

View File

@ -0,0 +1,10 @@
extends StaticBody3D
class_name InteractButton
signal button_interacted(value)
@export var button_press_value := 0
@export var press_cost := 0
func press():
button_interacted.emit(button_press_value)

66
Scripts/inventory.gd Normal file
View File

@ -0,0 +1,66 @@
extends Node
class_name Inventory
signal item_added(item)
signal item_removed(item)
@export var max_size := 0
var contents : Array[Card] = []
var selected_index := 0
var selected_item : Card :
get:
return contents[selected_index]
set(_value):
return
func add(card : Card) -> bool:
if card != null and contents.size() < max_size or max_size == 0:
contents.append(card)
item_added.emit(card)
networked_add.rpc(Data.cards.find(card))
return true
return false
func remove_at(index : int) -> Card:
if contents.size() <= 0:
return null
var card = contents[index]
contents.remove_at(index)
if selected_index >= contents.size() and selected_index > 0:
selected_index -= 1
item_removed.emit(card)
networked_remove_at.rpc(index)
return card
func remove() -> Card:
return remove_at(selected_index)
func increment_selected():
if contents.size() > 0:
selected_index += 1
if selected_index >= contents.size():
selected_index = 0
func decrement_selected():
if contents.size() > 0:
selected_index -= 1
if selected_index < 0:
selected_index = contents.size() - 1
@rpc("reliable")
func networked_add(value):
contents.append(Data.cards[value])
item_added.emit(Data.cards[value])
@rpc("reliable")
func networked_remove_at(value):
var item = contents[value]
contents.remove_at(value)
item_removed.emit(item)

14
Scripts/item_card.gd Normal file
View File

@ -0,0 +1,14 @@
extends StaticBody3D
class_name ItemCard
@export var card : Card
func pick_up() -> Card:
queue_free()
networked_pick_up.rpc()
return card
@rpc func networked_pick_up():
queue_free()

8
Scripts/level.gd Normal file
View File

@ -0,0 +1,8 @@
extends GridMap
class_name Level
@export var enemy_pool : Array[Enemy]
@export var player_spawns : Array[Node3D] = []
@export var enemy_spawns : Array[Node3D] = []
@export var enemy_goals : Array[Node3D] = []
@export var a_star_graph_3d : AStarGraph3D

View File

@ -0,0 +1,20 @@
extends Control
class_name LivesBarSegment
var lives_left := 6
func take_life(value : int):
for x in value:
lives_left -= 1
if lives_left == 5:
$AnimationPlayer.play("lose1")
if lives_left == 4:
$AnimationPlayer2.play("lose2")
if lives_left == 3:
$AnimationPlayer3.play("lose3")
if lives_left == 2:
$AnimationPlayer4.play("lose4")
if lives_left == 1:
$AnimationPlayer5.play("lose5")
if lives_left == 0:
$AnimationPlayer6.play("lose6")

10
Scripts/lives_bar.gd Normal file
View File

@ -0,0 +1,10 @@
extends TextureRect
@export var segments : Array[LivesBarSegment]
var lives := 120.0
func take_life():
var segment_to_animate = ceil(lives / 6.0) - 1
lives -= 1
segments[segment_to_animate].take_life(1)

15
Scripts/loadout_editor.gd Normal file
View File

@ -0,0 +1,15 @@
extends Panel
class_name LoadoutEditor
signal character_selected(character)
func _ready() -> void:
for i in Data.characters.size():
var button = Button.new()
button.text = Data.characters[i].hero_name
button.pressed.connect(set_character.bind(i))
$HBoxContainer.add_child(button)
func set_character(i: int):
character_selected.emit(i)

View File

@ -0,0 +1,10 @@
extends Control
func _on_quit_button_pressed() -> void:
Game.quit_to_desktop()
func _on_restart_button_pressed() -> void:
Game.restart_game()
queue_free()

46
Scripts/main_menu.gd Normal file
View File

@ -0,0 +1,46 @@
extends Control
var confirmation_popup_scene = preload("res://Scenes/Menus/confirmation_popup.tscn")
var text_input_popup_scene = preload("res://Scenes/Menus/text_input_popup.tscn")
var multiplayer_lobby_scene_path = "res://Scenes/multiplayer_lobby.tscn"
var options_menu_scene = preload("res://Scenes/Menus/options_menu.tscn")
func _ready() -> void:
$ProfileEditor/VBoxContainer/HBoxContainer/DisplayName.text = Data.player_profile.display_name
func _on_display_name_edit_pressed() -> void:
var popup = text_input_popup_scene.instantiate() as TextInputPopup
popup.set_popup(Data.player_profile.display_name, "Display Name", "Confirm")
popup.completed.connect(change_profile_display_name)
add_child(popup)
func change_profile_display_name(display_name):
$ProfileEditor/VBoxContainer/HBoxContainer/DisplayName.text = display_name
Data.player_profile.set_display_name(display_name)
func _on_quit_button_pressed() -> void:
var popup = confirmation_popup_scene.instantiate() as ConfirmationPopup
popup.set_popup("Are you sure you want to quit?", "Yes", "No")
popup.completed.connect(quit_game)
add_child(popup)
func quit_game(confirmation):
if confirmation:
get_tree().quit()
func _on_play_button_pressed() -> void:
Game.scene_switch_to_singleplayer_lobby()
func _on_options_button_pressed() -> void:
var menu = options_menu_scene.instantiate()
add_child(menu)
func _on_multiplayer_button_pressed() -> void:
Game.scene_switch_to_multiplayer_lobby()

10
Scripts/minimap_cam.gd Normal file
View File

@ -0,0 +1,10 @@
extends Camera3D
class_name MinimapCamera3D
@export var anchor : Node3D
@export var face_north : bool
func _process(_delta: float) -> void:
global_position = anchor.global_position + (Vector3.UP * 100)
if face_north:
rotation.y = anchor.rotation.y

View File

@ -0,0 +1,124 @@
extends Control
class_name MultiplayerLobby
signal player_connected(peer_id, player_profile)
signal player_disconnected(peer_id)
signal disconnected_from_server
const SERVER_PORT := 58008
const MAX_PLAYERS := 4
var enet_peer = ENetMultiplayerPeer.new()
@export var server_form : ServerForm
@export var scoreboard : Scoreboard
@export var loadout_editor : LoadoutEditor
@export var chatbox : Chatbox
var alert_popup_scene = preload("res://Scenes/Menus/alert_popup.tscn")
var connected_players_profiles = {}
func _ready():
multiplayer.peer_connected.connect(_on_player_connected)
multiplayer.peer_disconnected.connect(_on_player_disconnected)
multiplayer.connected_to_server.connect(_on_connection_succeeded)
multiplayer.connection_failed.connect(_on_connection_failed)
multiplayer.server_disconnected.connect(_on_server_disconnected)
func _on_player_connected(peer_id):
add_player.rpc_id(peer_id, Data.player_profile.to_dict())
if multiplayer.get_unique_id() == 1:
print("Player connected with id: " + str(peer_id))
func _on_player_disconnected(peer_id):
connected_players_profiles.erase(peer_id)
player_disconnected.emit(peer_id)
func _on_connection_succeeded():
setup_game(multiplayer.get_unique_id())
func _on_connection_failed():
multiplayer.multiplayer_peer = null
var popup = alert_popup_scene.instantiate() as AlertPopup
popup.set_popup("Unable to connect to server", "OK")
add_child(popup)
func _on_server_disconnected():
multiplayer.multiplayer_peer = null
disconnected_from_server.emit()
func create_server() -> void:
enet_peer.create_server(SERVER_PORT, MAX_PLAYERS)
multiplayer.multiplayer_peer = enet_peer
setup_game(1)
func setup_game(peer_id):
player_disconnected.connect(Game.remove_player)
Game.spawn_level()
scoreboard.all_players_ready.connect(start_game)
Game.game_restarted.connect(setup_the_ui)
setup_the_ui()
chatbox.username = Data.player_profile.display_name
Data.player_profile.display_name_changed.connect(chatbox.change_username)
loadout_editor.character_selected.connect(Data.player_profile.set_preferred_class)
loadout_editor.character_selected.connect(edit_player_profile)
connected_players_profiles[peer_id] = Data.player_profile
player_connected.emit(peer_id, Data.player_profile)
func setup_the_ui():
scoreboard.unready_all_players()
scoreboard.set_visible(true)
loadout_editor.set_visible(true)
$ReadyButton.set_visible(true)
chatbox.set_visible(true)
func connect_to_server() -> void:
var ip = server_form.get_server_ip() if server_form.get_server_ip() else "localhost"
var port = server_form.get_server_port() if server_form.get_server_port() else str(SERVER_PORT)
enet_peer.create_client(ip, int(port))
multiplayer.multiplayer_peer = enet_peer
func ready_player():
var peer_id = multiplayer.get_unique_id()
networked_ready_player.rpc(peer_id)
func start_game():
enet_peer.refuse_new_connections = true
Game.spawn_players(connected_players_profiles.keys(), connected_players_profiles, chatbox.opened, chatbox.closed)
scoreboard.set_visible(false)
loadout_editor.set_visible(false)
func edit_player_profile(_argument):
var profile_dict = Data.player_profile.to_dict()
networked_edit_player_profile.rpc(multiplayer.get_unique_id(), profile_dict)
@rpc("any_peer", "reliable", "call_local")
func networked_edit_player_profile(peer_id, new_profile_dict):
connected_players_profiles[peer_id].set_display_name(new_profile_dict["display_name"])
connected_players_profiles[peer_id].set_preferred_class(new_profile_dict["preferred_class"])
@rpc("any_peer","reliable")
func add_player(new_player_profile_dict):
var new_player_peer_id = multiplayer.get_remote_sender_id()
var new_player_profile = PlayerProfile.from_dict(new_player_profile_dict)
connected_players_profiles[new_player_peer_id] = new_player_profile
player_connected.emit(new_player_peer_id, new_player_profile)
@rpc("any_peer", "reliable", "call_local")
func networked_ready_player(peer_id):
scoreboard.set_player_ready_state(peer_id, true)

7
Scripts/on_top_camera.gd Normal file
View File

@ -0,0 +1,7 @@
extends Camera3D
@export var clone_camera : Node3D
func _process(_delta: float) -> void:
global_position = clone_camera.global_position
global_rotation = clone_camera.global_rotation

123
Scripts/options_menu.gd Normal file
View File

@ -0,0 +1,123 @@
extends Control
class_name OptionsMenu
@export var look_sens_slider : HSlider
@export var look_sens_input : SpinBox
@export var toggle_sprint_checkbox : CheckButton
@export var vsync_dropdown : OptionButton
@export var aa_dropdown : OptionButton
@export var window_dropdown : OptionButton
@export var invert_lookY : CheckButton
@export var invert_lookX : CheckButton
@export var fov_input : SpinBox
@export var fov_slider : HSlider
var keybind_boxes = []
var keybind_buttons = {}
var key_event
var selected_button
var selected_button_button
var listening_for_key := false
func _ready():
look_sens_slider.value = Data.preferences.mouse_sens
look_sens_input.value = Data.preferences.mouse_sens
toggle_sprint_checkbox.button_pressed = Data.preferences.toggle_sprint
vsync_dropdown.selected = Data.preferences.vsync_mode
aa_dropdown.selected = Data.preferences.aa_mode
invert_lookY.button_pressed = Data.preferences.invert_lookY
invert_lookX.button_pressed = Data.preferences.invert_lookX
fov_input.value = Data.preferences.hfov
fov_slider.value = Data.preferences.hfov
for index in Data.keymaps.size():
var map = Data.keymaps[index]
var button = Button.new()
button.text = map.title
button.pressed.connect(set_keymap.bind(index))
$VBoxContainer/TabContainer/Keybinds/HBoxContainer.add_child(button)
load_keybind_labels()
func set_keymap(keymap_index):
Data.player_keymap = Data.keymaps[keymap_index]
Data.player_keymap.apply()
load_keybind_labels()
func load_keybind_labels():
for box in keybind_boxes:
box.queue_free()
keybind_boxes.clear()
for action in InputMap.get_actions():
if !action.begins_with("ui_"):
var box = HBoxContainer.new()
var alabel = Label.new()
var elabel = Button.new()
alabel.text = action
if InputMap.action_get_events(action).size() > 0:
elabel.text = InputMap.action_get_events(action)[0].as_text()
elabel.size_flags_horizontal += Control.SIZE_EXPAND
alabel.size_flags_horizontal += Control.SIZE_EXPAND
alabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
alabel.size_flags_stretch_ratio = 2.0
#elabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
box.add_child(alabel)
box.add_child(elabel)
elabel.pressed.connect(_on_keybind_button_pressed.bind(elabel))
keybind_buttons[elabel] = action
$VBoxContainer/TabContainer/Keybinds/ScrollContainer/VBoxContainer.add_child(box)
keybind_boxes.append(box)
func _on_cancel_pressed() -> void:
queue_free()
func _on_confirm_pressed() -> void:
Data.preferences.mouse_sens = look_sens_slider.value
Data.preferences.toggle_sprint = toggle_sprint_checkbox.button_pressed
Data.preferences.vsync_mode = vsync_dropdown.selected
Data.preferences.aa_mode = aa_dropdown.selected
Data.preferences.windowed_mode = window_dropdown.selected
Data.preferences.invert_lookY = invert_lookY.button_pressed
Data.preferences.invert_lookX = invert_lookX.button_pressed
Data.preferences.apply_graphical_settings(get_viewport())
Data.preferences.save_profile_to_disk()
Data.player_keymap.save_profile_to_disk()
queue_free()
func _on_mouse_sens_spin_box_value_changed(value: float) -> void:
look_sens_slider.value = value
func _on_mouse_sens_h_slider_value_changed(value: float) -> void:
look_sens_input.value = value
func _on_fov_spin_box_value_changed(value: float) -> void:
if value < 40.0:
value = 40.0
if value > 160.0:
value = 160.0
fov_slider.value = value
Data.preferences.hfov = value
func _on_fov_h_slider_value_changed(value: float) -> void:
fov_input.value = value
Data.preferences.hfov = value
func _on_keybind_button_pressed(value: Button) -> void:
selected_button = keybind_buttons[value]
selected_button_button = value
listening_for_key = true
func _input(event: InputEvent) -> void:
if listening_for_key and (event is InputEventKey or event is InputEventMouseButton or event is InputEventJoypadButton):
key_event = event
listening_for_key = false
Data.player_keymap.replace_action_event(selected_button, key_event)
selected_button_button.text = key_event.as_text()

View File

@ -0,0 +1,16 @@
extends PathFollow3D
@export var speed = 0.5
@export var world_model : Node3D
@export var minimap_model : Node3D
func _process(delta: float) -> void:
progress += speed * delta
func set_world_visible(value: bool):
world_model.set_visible(value)
func set_minimap_visible(value: bool):
minimap_model.set_visible(value)

46
Scripts/pause_menu.gd Normal file
View File

@ -0,0 +1,46 @@
extends Control
class_name PauseMenu
signal closed
var options_menu_scene = preload("res://Scenes/Menus/options_menu.tscn")
var confirmation_popup_scene = preload("res://Scenes/Menus/confirmation_popup.tscn")
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("Pause"):
accept_event()
_on_resume_pressed()
func _on_resume_pressed() -> void:
closed.emit()
queue_free()
func _on_options_pressed() -> void:
var menu = options_menu_scene.instantiate()
add_child(menu)
func _on_quit_to_main_menu_pressed() -> void:
var popup = confirmation_popup_scene.instantiate() as ConfirmationPopup
popup.set_popup("Are you sure you want to quit and return to main menu?", "Yes", "No")
popup.completed.connect(return_to_menu)
add_child(popup)
func return_to_menu(confirmation):
if confirmation:
Game.scene_switch_main_menu()
func _on_quit_to_desktop_pressed() -> void:
var popup = confirmation_popup_scene.instantiate() as ConfirmationPopup
popup.set_popup("Are you sure you want to quit?", "Yes", "No")
popup.completed.connect(quit_game)
add_child(popup)
func quit_game(confirmation):
if confirmation:
Game.quit_to_desktop()

46
Scripts/scoreboard.gd Normal file
View File

@ -0,0 +1,46 @@
extends PanelContainer
class_name Scoreboard
signal all_players_ready
var entry_scene = preload("res://Scenes/UI/scoreboard_entry.tscn")
var entries = {}
func _ready() -> void:
$VBoxContainer/DummyEntry1.queue_free()
$VBoxContainer/DummyEntry2.queue_free()
$VBoxContainer/DummyEntry3.queue_free()
func get_player_entry(peer_id) -> ScoreboardEntry:
return entries[peer_id]
func set_player_ready_state(peer_id: int, state: bool):
entries[peer_id].set_ready_state(state)
for id in entries:
if !entries[id].get_ready_state():
return
all_players_ready.emit()
unready_all_players()
func unready_all_players():
for peer_id in entries:
entries[peer_id].set_ready_state(false)
func add_player(peer_id: int, player_profile: PlayerProfile):
var entry = entry_scene.instantiate() as ScoreboardEntry
entry.name = str(peer_id)
entry.set_display_name("", player_profile.get_display_name())
entry.set_character(0, player_profile.get_preferred_class())
player_profile.display_name_changed.connect(entry.set_display_name)
player_profile.preferred_class_changed.connect(entry.set_character)
entries[peer_id] = entry
$VBoxContainer.add_child(entry)
func remove_player(peer_id: int):
entries[peer_id].queue_free()
entries.erase(peer_id)

View File

@ -0,0 +1,28 @@
extends HBoxContainer
class_name ScoreboardEntry
var display_name: String
var character: int
var ready_state: bool
func set_display_name(_old_name: String, new_name: String):
display_name = new_name
$DisplayName.text = new_name
func get_display_name() -> String:
return display_name
func set_character(_old_class: int, new_class: int):
character = new_class
$CharacterName.text = Data.characters[new_class].hero_name
func get_character() -> int:
return character
func set_ready_state(state: bool):
ready_state = state
if state:
$TextureRect.texture.region = Rect2(32, 0, 32, 32)
else:
$TextureRect.texture.region = Rect2(0, 0, 32, 32)
func get_ready_state() -> bool:
return ready_state

18
Scripts/server_form.gd Normal file
View File

@ -0,0 +1,18 @@
extends PanelContainer
class_name ServerForm
signal connect_button_pressed
signal host_button_pressed
func _on_host_pressed() -> void:
host_button_pressed.emit()
queue_free()
func _on_connect_pressed() -> void:
connect_button_pressed.emit()
queue_free()
func get_server_ip() -> String:
return $VBoxContainer/HBoxContainer/ServerIP.text
func get_server_port() -> String:
return $VBoxContainer/HBoxContainer2/ServerPort.text

View File

@ -0,0 +1,41 @@
extends Control
@export var scoreboard : Scoreboard
@export var loadout_editor : LoadoutEditor
@export var chatbox : Chatbox
var connected_players_profiles = {}
func _ready() -> void:
setup_game()
func setup_game():
Game.spawn_level()
scoreboard.add_player(1, Data.player_profile)
scoreboard.all_players_ready.connect(start_game)
Game.game_restarted.connect(setup_the_ui)
setup_the_ui()
chatbox.username = Data.player_profile.display_name
Data.player_profile.display_name_changed.connect(chatbox.change_username)
loadout_editor.character_selected.connect(Data.player_profile.set_preferred_class)
loadout_editor.character_selected.connect(edit_player_profile)
connected_players_profiles[1] = Data.player_profile
func edit_player_profile(_argument):
var profile_dict = Data.player_profile.to_dict()
func start_game():
Game.spawn_players(connected_players_profiles.keys(), connected_players_profiles, chatbox.opened, chatbox.closed)
scoreboard.set_visible(false)
loadout_editor.set_visible(false)
func setup_the_ui():
scoreboard.unready_all_players()
scoreboard.set_visible(true)
loadout_editor.set_visible(true)
$ReadyButton.set_visible(true)
chatbox.set_visible(true)

14
Scripts/target_dummy.gd Normal file
View File

@ -0,0 +1,14 @@
extends EnemyController
class_name Dummy
func _on_health_health_depleted() -> void:
$Dog/Health.max_health = stats.health
$Dog/Health.current_health = stats.health
$Dog/SubViewport/ProgressBar.max_value = stats.health
$Dog/SubViewport/ProgressBar.value = stats.health
func _physics_process(delta: float) -> void:
progress += movement_speed * delta
if progress_ratio >= 1:
progress_ratio = 0

View File

@ -0,0 +1,14 @@
extends PanelContainer
class_name TextInputPopup
signal completed(outcome)
func set_popup(prompt_text, placeholder_text, confirm_text):
$VBoxContainer/LineEdit.text = prompt_text
$VBoxContainer/LineEdit.placeholder_text = placeholder_text
$VBoxContainer/Button.text = confirm_text
func _on_button_pressed() -> void:
completed.emit($VBoxContainer/LineEdit.text)
queue_free()

39
Scripts/tower_base.gd Normal file
View File

@ -0,0 +1,39 @@
extends StaticBody3D
class_name TowerBase
@export var inventory : Inventory
@export var block : CSGBox3D
@export var collider : CollisionShape3D
@export var minimap_icon : Sprite3D
var tower = null
var has_card : bool :
set(_value):
return
get:
return inventory.contents.size() != 0
func add_card(card: Card) -> bool:
var result = inventory.add(card)
if result:
tower = card.turret.instantiate() as Tower
tower.stats = card.tower_stats
minimap_icon.modulate = Color.RED
add_child(tower)
return result
func remove_card() -> Card:
tower.queue_free()
tower = null
minimap_icon.modulate = Color.GREEN
return inventory.remove()
func set_material(value: StandardMaterial3D):
block.material = value
func toggle_collision():
collider.disabled = !collider.disabled

View File

@ -0,0 +1,26 @@
extends Path3D
class_name VisualizedPath
var visual_scene = preload("res://Scenes/path_visual_thing.tscn")
var length := 0.0
var visualizer_points = []
func spawn_visualizer_points() -> void:
var new_length = curve.get_baked_length()
for x in new_length - length:
var point = visual_scene.instantiate()
visualizer_points.append(point)
add_child(point)
length = new_length
for x in visualizer_points.size():
visualizer_points[x].progress_ratio = float(x) / visualizer_points.size()
func disable_visualization():
for x in visualizer_points:
x.set_world_visible(false)
func enable_visualization():
for x in visualizer_points:
x.set_world_visible(true)

21
Scripts/wave_manager.gd Normal file
View File

@ -0,0 +1,21 @@
extends Node
func calculate_spawn_power(wave_number : int, number_of_players : int) -> int:
return 20 + (50 * number_of_players) + (30 * wave_number)
func generate_wave(spawn_power : int, spawn_pool : Array[Enemy]) -> Dictionary:
var wave = {}
#var sp_used = 0
var enemy_types = randi_range(1, 5)
var enemy_choices = spawn_pool.duplicate()
var sp_allotment = spawn_power / enemy_types
for x in enemy_types:
var choice = enemy_choices.pick_random()
enemy_choices.erase(choice)
if sp_allotment / choice.spawn_power > 0:
wave[choice] = sp_allotment / choice.spawn_power
#sp_used += wave[choice] * choice.spawn_power
#print("tried to generate wave with " + str(spawn_power) + " spawn power, used " + str(sp_used))
return wave

View File

@ -0,0 +1,10 @@
extends Control
func _on_quit_button_pressed() -> void:
Game.quit_to_desktop()
func _on_play_button_pressed() -> void:
Game.restart_game()
queue_free()