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).
125 lines
4.0 KiB
GDScript
125 lines
4.0 KiB
GDScript
extends Node3D
|
|
class_name LilguyBody
|
|
|
|
const LERP_VELOCITY: float = 0.15
|
|
|
|
@export_category("Objects")
|
|
@export var _character: CharacterBody3D = null
|
|
@export var animation_player: AnimationPlayer = null
|
|
|
|
func apply_rotation(_velocity: Vector3) -> void:
|
|
var new_rotation_y = lerp_angle(rotation.y, atan2(_velocity.x, _velocity.z), LERP_VELOCITY)
|
|
rotation.y = new_rotation_y
|
|
|
|
## Instantly face a direction (no lerp) - used to lock attacks to camera facing
|
|
func snap_rotation(_direction: Vector3) -> void:
|
|
rotation.y = atan2(_direction.x, _direction.z)
|
|
|
|
func animate(_velocity: Vector3) -> void:
|
|
if not animation_player:
|
|
return
|
|
|
|
# Don't override attack animation if it's playing
|
|
if animation_player.is_playing() and animation_player.current_animation.begins_with("Attack"):
|
|
return
|
|
|
|
# Check if we're dashing (defensive check for enemies that don't have this)
|
|
if _character and "_is_dashing" in _character and _character._is_dashing:
|
|
if animation_player.current_animation != "Jump":
|
|
_play_animation("Jump")
|
|
return
|
|
|
|
# Check if on floor (works for any CharacterBody3D)
|
|
var on_floor = _character.is_on_floor() if _character else true
|
|
|
|
if not on_floor:
|
|
if _velocity.y < 0:
|
|
# Falling - use FallIdle animation
|
|
_play_animation("FallIdle")
|
|
else:
|
|
_play_animation("Jump")
|
|
return
|
|
|
|
if _velocity:
|
|
# Check if running (defensive check - enemies don't have is_running)
|
|
var is_running_val = false
|
|
if _character and _character.has_method("is_running"):
|
|
is_running_val = _character.is_running() and on_floor
|
|
elif _velocity.length() > 5.0: # Fallback: high speed = running
|
|
is_running_val = on_floor
|
|
|
|
if is_running_val:
|
|
# Sprint animation = Run for Lilguy
|
|
_play_animation("Run")
|
|
return
|
|
|
|
# Walk animation for normal movement
|
|
_play_animation("Walk")
|
|
return
|
|
|
|
# Idle - use FallIdle or Idle if it exists
|
|
if animation_player.has_animation("Idle"):
|
|
_play_animation("Idle")
|
|
else:
|
|
_play_animation("FallIdle")
|
|
|
|
func _play_animation(anim_name: String):
|
|
if animation_player and animation_player.has_animation(anim_name):
|
|
animation_player.play(anim_name)
|
|
# Silently ignore if animation doesn't exist
|
|
|
|
func play_attack(anim_name: String = "Attack_OneHand") -> void:
|
|
if animation_player:
|
|
# Play attack animation once (don't loop)
|
|
if animation_player.has_animation(anim_name):
|
|
animation_player.play(anim_name, -1, 1.0)
|
|
animation_player.animation_set_next(anim_name, "")
|
|
else:
|
|
# Fallback to default if animation doesn't exist
|
|
#push_warning("Animation '%s' not found, using Attack_OneHand" % anim_name)
|
|
animation_player.play("Attack_OneHand", -1, 1.0)
|
|
animation_player.animation_set_next("Attack_OneHand", "")
|
|
|
|
## Apply hue shift to all mesh instances in the character
|
|
func set_character_color(hue_shift: float) -> void:
|
|
# Find all MeshInstance3D children recursively
|
|
var mesh_instances = _find_mesh_instances(self)
|
|
|
|
for mesh in mesh_instances:
|
|
_apply_hue_to_mesh(mesh, hue_shift)
|
|
|
|
## Recursively find all MeshInstance3D nodes
|
|
func _find_mesh_instances(node: Node) -> Array[MeshInstance3D]:
|
|
var meshes: Array[MeshInstance3D] = []
|
|
|
|
if node is MeshInstance3D:
|
|
meshes.append(node)
|
|
|
|
for child in node.get_children():
|
|
meshes.append_array(_find_mesh_instances(child))
|
|
|
|
return meshes
|
|
|
|
## Apply hue shift to a mesh instance
|
|
func _apply_hue_to_mesh(mesh_instance: MeshInstance3D, hue_shift: float) -> void:
|
|
if not mesh_instance:
|
|
return
|
|
|
|
# Get or create material for each surface
|
|
for i in range(mesh_instance.get_surface_override_material_count()):
|
|
var material = mesh_instance.get_surface_override_material(i)
|
|
|
|
# If no override material, get the base material and duplicate it
|
|
if not material:
|
|
material = mesh_instance.mesh.surface_get_material(i)
|
|
if material:
|
|
material = material.duplicate()
|
|
mesh_instance.set_surface_override_material(i, material)
|
|
|
|
# Apply hue shift if it's a StandardMaterial3D
|
|
if material and material is StandardMaterial3D:
|
|
var std_mat = material as StandardMaterial3D
|
|
# Create a modulate color from hue
|
|
var color = Color.from_hsv(hue_shift, 0.6, 1.0)
|
|
std_mat.albedo_color = color
|