Add classic HUD, auto reload, furniture pathfinding and death flatline
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
extends CanvasLayer
|
||||
## Classic amber HUD, kept above touch sticks/buttons rather than over them.
|
||||
const AMBER := Color(1.0, 0.66, 0.16)
|
||||
const DANGER := Color(1.0, 0.22, 0.12)
|
||||
var health_value: Label
|
||||
var ammo_value: Label
|
||||
var ammo_state: Label
|
||||
var wave_value: Label
|
||||
var grenade_value: Label
|
||||
var help: Label
|
||||
var root: Control
|
||||
|
||||
func _ready() -> void:
|
||||
layer = 2
|
||||
root = Control.new()
|
||||
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
root.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(root)
|
||||
var health := _panel("Health", false)
|
||||
_label(health, "+ VITA", 16, Vector2(16, 10))
|
||||
health_value = _label(health, "100", 42, Vector2(16, 27))
|
||||
var ammo := _panel("Ammo", true)
|
||||
_label(ammo, "CARICATORE", 16, Vector2(16, 10))
|
||||
ammo_value = _label(ammo, "30 / 30", 36, Vector2(16, 30))
|
||||
ammo_state = _label(ammo, "", 14, Vector2(16, 77))
|
||||
wave_value = _label(root, "", 18, Vector2.ZERO)
|
||||
wave_value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
wave_value.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP)
|
||||
wave_value.offset_left = -160
|
||||
wave_value.offset_right = 160
|
||||
wave_value.offset_top = 15
|
||||
wave_value.offset_bottom = 45
|
||||
grenade_value = _label(root, "", 15, Vector2.ZERO)
|
||||
grenade_value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
grenade_value.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP)
|
||||
grenade_value.offset_left = -160
|
||||
grenade_value.offset_right = 160
|
||||
grenade_value.offset_top = 44
|
||||
grenade_value.offset_bottom = 66
|
||||
help = _label(root, "WASD: muovi | mouse: mira | R: ricarica | destro: granata", 15, Vector2.ZERO)
|
||||
help.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
help.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM)
|
||||
help.offset_left = -420
|
||||
help.offset_right = 420
|
||||
help.offset_top = -30
|
||||
help.offset_bottom = -8
|
||||
|
||||
func _panel(label: String, right: bool) -> Panel:
|
||||
var panel := Panel.new()
|
||||
panel.name = label
|
||||
panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.025, 0.02, 0.008, 0.8)
|
||||
style.border_color = Color(1.0, 0.66, 0.16, 0.45)
|
||||
style.set_border_width_all(1)
|
||||
style.set_corner_radius_all(4)
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
root.add_child(panel)
|
||||
panel.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT if right else Control.PRESET_TOP_LEFT)
|
||||
panel.offset_left = -224 if right else 16
|
||||
panel.offset_right = -16 if right else 224
|
||||
panel.offset_top = 16
|
||||
panel.offset_bottom = 116
|
||||
return panel
|
||||
|
||||
func _label(parent: Control, text: String, size: int, pos: Vector2) -> Label:
|
||||
var label := Label.new()
|
||||
label.text = text
|
||||
label.position = pos
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
label.add_theme_color_override("font_color", AMBER)
|
||||
label.add_theme_color_override("font_shadow_color", Color.BLACK)
|
||||
label.add_theme_constant_override("shadow_offset_x", 1)
|
||||
label.add_theme_constant_override("shadow_offset_y", 1)
|
||||
label.add_theme_font_size_override("font_size", size)
|
||||
parent.add_child(label)
|
||||
return label
|
||||
|
||||
func update_status(player: Node, wave: int, score: int) -> void:
|
||||
health_value.text = str(maxi(0, ceili(player.hp)))
|
||||
health_value.add_theme_color_override("font_color", DANGER if player.hp <= 25 else AMBER)
|
||||
ammo_value.text = "%02d / 30" % player.ammo
|
||||
ammo_value.add_theme_color_override("font_color", DANGER if player.ammo <= 5 else AMBER)
|
||||
ammo_state.text = "RICARICA..." if player.reloading else ""
|
||||
wave_value.text = "ONDATA %d | PUNTI %d" % [wave, score]
|
||||
grenade_value.text = "GRANATA: " + ("PRONTA" if player.grenade_cooldown <= 0 else "%ds" % ceili(player.grenade_cooldown))
|
||||
help.visible = not player.touch_mode
|
||||
@@ -0,0 +1 @@
|
||||
uid://cxxu6fixvsjbs
|
||||
+10
-13
@@ -10,7 +10,8 @@ var room: Node3D
|
||||
var music: AudioStreamPlayer
|
||||
var music_started := false
|
||||
var touch: Node
|
||||
var hud: Label
|
||||
var hud: CanvasLayer
|
||||
var navigation: Node
|
||||
var score := 0
|
||||
var wave := 0
|
||||
var alive := 0
|
||||
@@ -46,12 +47,14 @@ func _ready() -> void:
|
||||
var office := Node3D.new()
|
||||
office.set_script(preload("res://scripts/office_atmosphere.gd"))
|
||||
add_child(office)
|
||||
navigation = Node.new()
|
||||
navigation.set_script(preload("res://scripts/room_navigation.gd"))
|
||||
add_child(navigation)
|
||||
var canvas := CanvasLayer.new()
|
||||
add_child(canvas)
|
||||
hud = Label.new()
|
||||
hud.position = Vector2(24, 20)
|
||||
hud.add_theme_font_size_override("font_size", 24)
|
||||
canvas.add_child(hud)
|
||||
hud = CanvasLayer.new()
|
||||
hud.set_script(preload("res://scripts/game_hud.gd"))
|
||||
add_child(hud)
|
||||
var cross := Label.new()
|
||||
cross.text = "+"
|
||||
cross.add_theme_font_size_override("font_size", 30)
|
||||
@@ -141,14 +144,7 @@ func _restart_music() -> void:
|
||||
|
||||
func _process(_dt: float) -> void:
|
||||
if hud:
|
||||
hud.text = "VITA %d MUNIZIONI %d/30 ONDATA %d PUNTI %d\nWASD · mouse · sinistro: spara · destro: granata · R: ricarica · spazio: salto · Esc: mouse" % [player.hp, player.ammo, wave, score]
|
||||
hud.text += "\nGRANATA: " + ("PRONTA" if player.grenade_cooldown <= 0 else "%d s" % ceili(player.grenade_cooldown))
|
||||
if player.reloading:
|
||||
hud.text += "\nRICARICA..."
|
||||
if player.touch_mode:
|
||||
hud.text += "\ntrascina a sinistra: muovi · a destra: guarda"
|
||||
if player.hp <= 0:
|
||||
hud.text += "\nSEI MORTO — tocca RICOMINCIA" if player.touch_mode else "\nSEI MORTO — premi R per ricominciare"
|
||||
hud.update_status(player, wave, score)
|
||||
|
||||
func zombies_for_wave(number: int) -> int:
|
||||
return 4 + number * 2
|
||||
@@ -173,6 +169,7 @@ func _spawn(index: int) -> void:
|
||||
var zombie := CharacterBody3D.new()
|
||||
zombie.set_script(ZombieScript)
|
||||
zombie.player = player
|
||||
zombie.navigation = navigation
|
||||
zombie.position = centers[index] - inward * 1.2
|
||||
add_child(zombie)
|
||||
alive += 1
|
||||
|
||||
@@ -100,6 +100,8 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
|
||||
## Ricarica: tempi invariati, riusata da tastiera (R) e dal pulsante on-screen.
|
||||
func start_reload() -> void:
|
||||
if reloading or death_started or hp <= 0 or ammo >= 30:
|
||||
return
|
||||
reloading = true
|
||||
weapon_anim.play("reload")
|
||||
Audio.play(self, "items/cliprelease1.wav")
|
||||
@@ -140,6 +142,8 @@ func _physics_process(dt: float) -> void:
|
||||
get_tree().reload_current_scene() # R o pulsante Ricarica a partita finita
|
||||
return
|
||||
_regenerate_health(dt)
|
||||
if ammo <= 0 and not reloading:
|
||||
start_reload()
|
||||
_handle_actions()
|
||||
# Deadzone esplicita piccola (0.2): con la 0.5 delle azioni la corsa utile del
|
||||
# joystick touch si dimezzerebbe, mentre per tastiera/joypad nulla cambia.
|
||||
@@ -176,6 +180,8 @@ func shoot() -> void:
|
||||
if hit and hit.collider.has_method("take_damage"):
|
||||
Blood.impact(get_parent(), hit.position, -camera.global_basis.z)
|
||||
hit.collider.take_damage(35, hit.position.y)
|
||||
if ammo <= 0:
|
||||
start_reload()
|
||||
|
||||
func throw_grenade() -> RigidBody3D:
|
||||
if death_started or hp <= 0 or grenade_cooldown > 0:
|
||||
@@ -219,6 +225,7 @@ func _die() -> void:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
# Original Half-Life player pain sample used as the death event vocal.
|
||||
Audio.play(self, "player/pl_pain7.wav", false, -8)
|
||||
Audio.play(self, "fvox/flatline.wav", false, -9)
|
||||
var layer := CanvasLayer.new()
|
||||
layer.layer = 5
|
||||
add_child(layer)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
extends Node
|
||||
## Shared planar A* map for static office collisions; no runtime navmesh bake.
|
||||
const CELL := 0.5
|
||||
const CLEARANCE := 0.60 # capsule radius .35 + conservative cell/edge margin
|
||||
var grid := AStarGrid2D.new()
|
||||
var obstacles: Array[Rect2] = []
|
||||
|
||||
func _ready() -> void:
|
||||
name = "RoomNavigation"
|
||||
grid.region = Rect2i(-17, -17, 35, 35)
|
||||
grid.cell_size = Vector2(CELL, CELL)
|
||||
grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES
|
||||
grid.default_compute_heuristic = AStarGrid2D.HEURISTIC_OCTILE
|
||||
grid.default_estimate_heuristic = AStarGrid2D.HEURISTIC_OCTILE
|
||||
grid.update() # Must precede solid flags; update clears them.
|
||||
for prop in get_tree().get_nodes_in_group("office_props"):
|
||||
for collider in prop.get_children():
|
||||
if not collider is CollisionShape3D or not collider.shape is BoxShape3D:
|
||||
continue
|
||||
var half: Vector3 = collider.shape.size * 0.5
|
||||
var minimum := Vector2(INF, INF)
|
||||
var maximum := Vector2(-INF, -INF)
|
||||
for x in [-half.x, half.x]:
|
||||
for z in [-half.z, half.z]:
|
||||
var p: Vector3 = collider.global_transform * Vector3(x, 0, z)
|
||||
minimum = minimum.min(Vector2(p.x, p.z))
|
||||
maximum = maximum.max(Vector2(p.x, p.z))
|
||||
obstacles.append(Rect2(minimum, maximum - minimum).grow(CLEARANCE))
|
||||
for x in range(-17, 18):
|
||||
for y in range(-17, 18):
|
||||
var id := Vector2i(x, y)
|
||||
var p := Vector2(id) * CELL
|
||||
for rect in obstacles:
|
||||
if rect.has_point(p):
|
||||
grid.set_point_solid(id)
|
||||
break
|
||||
|
||||
func _free_id(point: Vector3) -> Vector2i:
|
||||
var id := Vector2i(roundi(point.x / CELL), roundi(point.z / CELL))
|
||||
id.x = clampi(id.x, -17, 17)
|
||||
id.y = clampi(id.y, -17, 17)
|
||||
if not grid.is_point_solid(id):
|
||||
return id
|
||||
var best := id
|
||||
var distance := INF
|
||||
for x in range(-17, 18):
|
||||
for y in range(-17, 18):
|
||||
var candidate := Vector2i(x, y)
|
||||
if not grid.is_point_solid(candidate):
|
||||
var d := Vector2(candidate).distance_squared_to(Vector2(point.x, point.z) / CELL)
|
||||
if d < distance:
|
||||
best = candidate
|
||||
distance = d
|
||||
return best
|
||||
|
||||
func route(from: Vector3, to: Vector3) -> PackedVector3Array:
|
||||
var result := PackedVector3Array()
|
||||
for p in grid.get_point_path(_free_id(from), _free_id(to)):
|
||||
result.append(Vector3(p.x, 0, p.y))
|
||||
return result
|
||||
|
||||
func direct_clear(from: Vector3, to: Vector3) -> bool:
|
||||
var a := Vector2(from.x, from.z)
|
||||
var b := Vector2(to.x, to.z)
|
||||
var count := maxi(1, ceili(a.distance_to(b) / 0.15))
|
||||
for i in range(count + 1):
|
||||
var p := a.lerp(b, float(i) / count)
|
||||
if absf(p.x) > 8.6 or absf(p.y) > 8.6:
|
||||
return false
|
||||
for obstacle in obstacles:
|
||||
if obstacle.has_point(p):
|
||||
return false
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://dkb8hopidwlrs
|
||||
+45
-3
@@ -1,6 +1,12 @@
|
||||
extends CharacterBody3D
|
||||
signal killed(headshot: bool)
|
||||
var player: CharacterBody3D
|
||||
var navigation: Node
|
||||
var _path := PackedVector3Array()
|
||||
var _path_index := 0
|
||||
var _repath := 0.0
|
||||
var _direct := false
|
||||
var _nav_started := false
|
||||
var hp := 100.0
|
||||
var speed := 2.0
|
||||
var cooldown := 0.0
|
||||
@@ -30,6 +36,7 @@ func _ready() -> void:
|
||||
for area in model.find_children("*", "Area3D", true, false):
|
||||
area.collision_layer = 0
|
||||
speed = randf_range(1.6, 2.8)
|
||||
_repath = randf_range(0, 0.5)
|
||||
anim.play("walk")
|
||||
Audio.play(self, "zombie/zo_alert%d0.wav" % randi_range(1, 3), true, -15)
|
||||
|
||||
@@ -46,8 +53,10 @@ func _physics_process(dt: float) -> void:
|
||||
var distance := dir.length()
|
||||
if distance > 0.01:
|
||||
rotation.y = atan2(-dir.x, -dir.z)
|
||||
var walking := distance > 1.7
|
||||
var motion := dir.normalized() * speed if walking else Vector3.ZERO
|
||||
var walking := distance > 1.7 or not _attack_clear()
|
||||
var motion := _pursuit_direction(dt) * speed if walking else Vector3.ZERO
|
||||
if motion.length_squared() > 0.01:
|
||||
rotation.y = atan2(-motion.x, -motion.z)
|
||||
velocity.x = motion.x
|
||||
velocity.z = motion.z
|
||||
velocity.y = -1.0 if is_on_floor() else velocity.y - 20.0 * dt
|
||||
@@ -60,8 +69,41 @@ func _physics_process(dt: float) -> void:
|
||||
Audio.play(self, "zombie/zo_attack%d.wav" % randi_range(1, 2), true, -15)
|
||||
get_tree().create_timer(0.5).timeout.connect(_attack)
|
||||
|
||||
func _pursuit_direction(dt: float) -> Vector3:
|
||||
var target := player.global_position
|
||||
_repath -= dt
|
||||
if navigation != null and (_repath <= 0 or not _nav_started):
|
||||
_nav_started = true
|
||||
_repath = 0.55 + float(get_instance_id() % 7) * 0.02
|
||||
_direct = navigation.direct_clear(global_position, target)
|
||||
if not _direct:
|
||||
_path = navigation.route(global_position, target)
|
||||
_path_index = 0
|
||||
if _path.size() > 1 and navigation.direct_clear(global_position, _path[1]):
|
||||
_path_index = 1
|
||||
if navigation == null or _direct:
|
||||
var dir := target - global_position
|
||||
dir.y = 0
|
||||
return dir.normalized()
|
||||
while _path_index < _path.size():
|
||||
var dir := _path[_path_index] - global_position
|
||||
dir.y = 0
|
||||
if dir.length() > 0.22:
|
||||
return dir.normalized()
|
||||
_path_index += 1
|
||||
return Vector3.ZERO
|
||||
|
||||
func _attack_clear() -> bool:
|
||||
# Check below desk-top height: a chest-high ray can pass over a desk
|
||||
# even though the bodies cannot approach each other through it.
|
||||
var start := global_position + Vector3.UP * 0.6
|
||||
var end := player.global_position + Vector3.UP * 0.6
|
||||
var query := PhysicsRayQueryParameters3D.create(start, end, 1)
|
||||
query.exclude = [get_rid(), player.get_rid()]
|
||||
return get_world_3d().direct_space_state.intersect_ray(query).is_empty()
|
||||
|
||||
func _attack() -> void:
|
||||
if not dead and is_instance_valid(player) and player.hp > 0 and global_position.distance_to(player.global_position) < 2.3:
|
||||
if not dead and is_instance_valid(player) and player.hp > 0 and global_position.distance_to(player.global_position) < 2.3 and _attack_clear():
|
||||
player.take_damage(12)
|
||||
Audio.play(self, "zombie/claw_strike%d.wav" % randi_range(1, 3), true, -12)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user