Gladiator arena: gold economy, outfitting shop, permadeath runs
Turns the sandbox into a survival roguelike loop: outfit your gladiator with banked gold, enter the arena, earn gold from kills and wave clears, die, keep 10% of your total value (floored at the 500 fresh-start). Economy (GameState autoload): - banked_gold, owned items, and loadout persist to user://save.cfg - Kill gold (basic 10g, armed 25g) credited server-side to the killer - Wave-clear bonus (25 + 5/wave) for all players; wave synced to peers - Death settlement liquidates gear, guarded against double-fire Character creation / shop: - Menu reframed: armory shop + live Character Sheet loadout preview - Character Sheet gains preview mode (renders from loadout, no player) - Buy/Equip with hand rules; two-handers and off-hands displace each other symmetrically (shop and in-game pickups) - Chosen loadout auto-equips on spawn via existing RPC path Combat feel: - Attacks snap to camera facing and lock direction for the swing - Dash commits to its direction for the full duration - Weapon slots show name text when no icon is set - Hitbox/hurtbox debug meshes hidden by default (H toggles) Death is final: players no longer respawn; a results screen shows waves, time, kills, and the settlement, then returns to camp. Armed enemies no longer respawn (were an infinite gold farm and respawned invisible).
This commit is contained in:
+53
-5
@@ -59,6 +59,9 @@ var _dash_cooldown_timer: float = 0.0
|
||||
var _is_dashing: bool = false
|
||||
var _dash_direction: Vector3 = Vector3.ZERO
|
||||
|
||||
# Facing lock: while > 0 the body cannot turn (attacks aim at camera, dashes stay straight)
|
||||
var _facing_locked_timer: float = 0.0
|
||||
|
||||
# UI Signals
|
||||
signal dash_cooldown_updated(remaining: float, total: float)
|
||||
signal attack_cooldown_updated(remaining: float, total: float)
|
||||
@@ -74,6 +77,9 @@ func _ready():
|
||||
health_regen = 3.0
|
||||
regen_delay = 5.0
|
||||
|
||||
# Gladiator arena: death ends your run - no respawning
|
||||
can_respawn = false
|
||||
|
||||
super._ready()
|
||||
# Set respawn point to current position (where we spawned) - base_unit._ready already does this
|
||||
# Don't override with a hardcoded position
|
||||
@@ -240,6 +246,10 @@ func _process(delta):
|
||||
_attack_timer -= delta
|
||||
attack_cooldown_updated.emit(_attack_timer, attack_cooldown)
|
||||
|
||||
# Update facing lock (attack/dash direction commitment)
|
||||
if _facing_locked_timer > 0:
|
||||
_facing_locked_timer -= delta
|
||||
|
||||
# Update dash timers
|
||||
if _dash_timer > 0:
|
||||
_dash_timer -= delta
|
||||
@@ -295,12 +305,10 @@ func freeze():
|
||||
_body.animate(Vector3.ZERO)
|
||||
|
||||
func _move() -> void:
|
||||
# If dashing, use dash movement
|
||||
# If dashing, use dash movement (facing already snapped and locked at dash start)
|
||||
if _is_dashing:
|
||||
velocity.x = _dash_direction.x * _current_speed * dash_speed_multiplier
|
||||
velocity.z = _dash_direction.z * _current_speed * dash_speed_multiplier
|
||||
if _body:
|
||||
_body.apply_rotation(velocity)
|
||||
return
|
||||
|
||||
var _input_direction: Vector2 = Vector2.ZERO
|
||||
@@ -319,13 +327,35 @@ func _move() -> void:
|
||||
if _direction:
|
||||
velocity.x = _direction.x * _current_speed
|
||||
velocity.z = _direction.z * _current_speed
|
||||
if _body:
|
||||
# Body only turns toward movement while facing isn't locked by an attack/dash
|
||||
if _body and _facing_locked_timer <= 0:
|
||||
_body.apply_rotation(velocity)
|
||||
return
|
||||
|
||||
velocity.x = move_toward(velocity.x, 0, _current_speed)
|
||||
velocity.z = move_toward(velocity.z, 0, _current_speed)
|
||||
|
||||
## Horizontal direction the camera is looking (movement-space, matches _move math)
|
||||
func _get_camera_forward() -> Vector3:
|
||||
var fwd: Vector3 = transform.basis * Vector3(0, 0, -1)
|
||||
if _spring_arm_offset:
|
||||
fwd = fwd.rotated(Vector3.UP, _spring_arm_offset.rotation.y)
|
||||
fwd.y = 0
|
||||
return fwd.normalized()
|
||||
|
||||
## Snap the body to face a direction and hold it there for `duration` seconds
|
||||
func _lock_facing(direction: Vector3, duration: float):
|
||||
_facing_locked_timer = max(_facing_locked_timer, duration)
|
||||
if _body and direction.length_squared() > 0.001:
|
||||
if _body.has_method("snap_rotation"):
|
||||
_body.snap_rotation(direction)
|
||||
else:
|
||||
_body.apply_rotation(direction)
|
||||
|
||||
## Attacks commit to where the camera looks, not where the character was walking
|
||||
func _lock_facing_to_camera(duration: float):
|
||||
_lock_facing(_get_camera_forward(), duration)
|
||||
|
||||
func is_running() -> bool:
|
||||
if Input.is_action_pressed("shift"):
|
||||
_current_speed = SPRINT_SPEED
|
||||
@@ -423,11 +453,14 @@ func _perform_attack():
|
||||
|
||||
# Use main hand weapon if available
|
||||
if equipped_weapon and equipped_weapon.can_attack():
|
||||
# Aim the swing at the camera direction and commit to it
|
||||
_lock_facing_to_camera(equipped_weapon.weapon_data.startup_time + equipped_weapon.weapon_data.active_time)
|
||||
equipped_weapon.perform_attack()
|
||||
return
|
||||
|
||||
# Or use off-hand weapon if available
|
||||
if equipped_offhand and equipped_offhand.can_attack():
|
||||
_lock_facing_to_camera(equipped_offhand.weapon_data.startup_time + equipped_offhand.weapon_data.active_time)
|
||||
equipped_offhand.perform_attack()
|
||||
return
|
||||
|
||||
@@ -445,6 +478,9 @@ func _perform_attack():
|
||||
_attack_timer = cooldown
|
||||
_is_unarmed_attacking = true
|
||||
|
||||
# Aim the punch at the camera direction and commit to it
|
||||
_lock_facing_to_camera(total_duration)
|
||||
|
||||
# Play attack animation once
|
||||
if _body:
|
||||
_body.play_attack("Attack_OneHand")
|
||||
@@ -517,7 +553,7 @@ func _on_died(killer_id: int):
|
||||
|
||||
# Show death message on UI
|
||||
if has_node("HealthUI/HealthText"):
|
||||
get_node("HealthUI/HealthText").text = "DEAD - Respawning..."
|
||||
get_node("HealthUI/HealthText").text = "DEAD - Run Over"
|
||||
|
||||
func _on_respawned():
|
||||
print("[Player ", name, "] _on_respawned called. Authority: ", is_multiplayer_authority(), " Position: ", global_position)
|
||||
@@ -560,6 +596,9 @@ func _perform_dash():
|
||||
_dash_timer = dash_duration
|
||||
_dash_cooldown_timer = dash_cooldown
|
||||
|
||||
# Commit to the dash direction - no steering or turning mid-dash
|
||||
_lock_facing(_dash_direction, dash_duration)
|
||||
|
||||
# Animation is handled by the Body's animate function (Jump animation plays during dash)
|
||||
|
||||
## Sync attack animation to all clients
|
||||
@@ -715,6 +754,15 @@ func equip_weapon(data: WeaponData):
|
||||
# Determine which hand based on weapon type
|
||||
var is_offhand = (data.hand_type == WeaponData.Hand.OFF_HAND)
|
||||
|
||||
# Two-handed weapons occupy both hands - clear the off-hand
|
||||
if data.hand_type == WeaponData.Hand.TWO_HAND and equipped_offhand:
|
||||
unequip_weapon(true)
|
||||
|
||||
# And symmetrically: an off-hand displaces a two-handed main weapon
|
||||
if is_offhand and equipped_weapon and equipped_weapon.weapon_data \
|
||||
and equipped_weapon.weapon_data.hand_type == WeaponData.Hand.TWO_HAND:
|
||||
unequip_weapon(false)
|
||||
|
||||
# Unequip current weapon in that hand first
|
||||
if is_offhand:
|
||||
if equipped_offhand:
|
||||
|
||||
Reference in New Issue
Block a user