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).
339 lines
11 KiB
GDScript
339 lines
11 KiB
GDScript
extends Control
|
|
class_name CharacterSheet
|
|
## Character sheet / spellbook that displays player stats, weapons, and abilities
|
|
## Opens with Tab key
|
|
|
|
# References
|
|
@onready var stats_container: VBoxContainer = $Panel/MarginContainer/VBoxContainer/HBoxContainer/LeftPage/ScrollContainer/StatsContainer
|
|
@onready var weapons_container: VBoxContainer = $Panel/MarginContainer/VBoxContainer/HBoxContainer/RightPage/ScrollContainer/ContentContainer/WeaponsContainer
|
|
@onready var abilities_container: VBoxContainer = $Panel/MarginContainer/VBoxContainer/HBoxContainer/RightPage/ScrollContainer/ContentContainer/AbilitiesContainer
|
|
|
|
# Player reference
|
|
var player: Character = null
|
|
|
|
# Preview mode: renders from a loadout dictionary instead of a live player.
|
|
# Used by the outfitting screen at character creation.
|
|
# preview_data keys: "nick" (String), "main" (WeaponData or null), "offhand" (WeaponData or null)
|
|
var preview_mode: bool = false
|
|
var preview_data: Dictionary = {}
|
|
|
|
# Character defaults shown in preview (match player.gd export defaults)
|
|
const PREVIEW_MAX_HEALTH := 100.0
|
|
const PREVIEW_ATTACK_DAMAGE := 10.0
|
|
const PREVIEW_ATTACK_RANGE := 3.0
|
|
const PREVIEW_ATTACK_COOLDOWN := 0.5
|
|
const PREVIEW_DASH_COOLDOWN := 4.0
|
|
const PREVIEW_DASH_DURATION := 0.25
|
|
const PREVIEW_DASH_MULTIPLIER := 2.0
|
|
|
|
# Visibility
|
|
var is_visible: bool = false
|
|
|
|
func _ready():
|
|
if preview_mode:
|
|
_configure_preview_layout()
|
|
show()
|
|
is_visible = true
|
|
refresh_all()
|
|
else:
|
|
# Start hidden (in-game Tab toggle behavior)
|
|
hide()
|
|
is_visible = false
|
|
|
|
func _input(event):
|
|
# Embedded preview never toggles - it's part of the outfitting screen
|
|
if preview_mode:
|
|
return
|
|
|
|
# Close on Escape press if open
|
|
if event.is_action_pressed("quit") and is_visible:
|
|
toggle_sheet()
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
|
|
# Toggle on Tab press
|
|
if event.is_action_pressed("toggle_character_sheet"):
|
|
toggle_sheet()
|
|
get_viewport().set_input_as_handled()
|
|
|
|
## Set the player reference and update display
|
|
func set_player(p: Character):
|
|
player = p
|
|
if player:
|
|
refresh_all()
|
|
|
|
## Feed the sheet a loadout to preview (outfitting screen).
|
|
## Safe to call before or after the sheet enters the tree.
|
|
func set_preview(data: Dictionary):
|
|
preview_mode = true
|
|
preview_data = data
|
|
if is_inside_tree():
|
|
refresh_all()
|
|
|
|
## Re-shape the fullscreen overlay into an embeddable panel:
|
|
## no dark backdrop, panel fills the space its parent gives it.
|
|
func _configure_preview_layout():
|
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
var backdrop = get_node_or_null("DarkBackground")
|
|
if backdrop:
|
|
backdrop.hide()
|
|
var panel = get_node_or_null("Panel")
|
|
if panel:
|
|
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
panel.offset_left = 0
|
|
panel.offset_top = 0
|
|
panel.offset_right = 0
|
|
panel.offset_bottom = 0
|
|
var title = get_node_or_null("Panel/MarginContainer/VBoxContainer/TitleLabel")
|
|
if title:
|
|
title.text = "YOUR GLADIATOR"
|
|
var close_hint = get_node_or_null("Panel/MarginContainer/VBoxContainer/CloseHint")
|
|
if close_hint:
|
|
close_hint.hide()
|
|
|
|
## Toggle character sheet visibility
|
|
func toggle_sheet():
|
|
is_visible = !is_visible
|
|
if is_visible:
|
|
show()
|
|
refresh_all()
|
|
# Release mouse when opening sheet
|
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
|
else:
|
|
hide()
|
|
# Recapture mouse when closing sheet
|
|
if player and player.is_multiplayer_authority():
|
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
|
|
|
## Refresh all data in the sheet
|
|
func refresh_all():
|
|
if not player and not preview_mode:
|
|
return
|
|
|
|
_refresh_stats()
|
|
_refresh_weapons()
|
|
_refresh_abilities()
|
|
|
|
## Refresh player stats
|
|
func _refresh_stats():
|
|
if not stats_container:
|
|
return
|
|
|
|
# Clear existing stats
|
|
for child in stats_container.get_children():
|
|
child.queue_free()
|
|
|
|
# Add title
|
|
var title = Label.new()
|
|
title.text = "CHARACTER STATS"
|
|
title.add_theme_font_size_override("font_size", 24)
|
|
title.add_theme_color_override("font_color", Color(1.0, 0.8, 0.0))
|
|
stats_container.add_child(title)
|
|
|
|
# Spacer
|
|
var spacer1 = Control.new()
|
|
spacer1.custom_minimum_size = Vector2(0, 10)
|
|
stats_container.add_child(spacer1)
|
|
|
|
# Resolve stats from either the live player or the preview loadout
|
|
var player_name: String
|
|
var current_hp: int
|
|
var max_hp: int
|
|
var atk_damage: float
|
|
var atk_range: float
|
|
var atk_cooldown: float
|
|
|
|
if preview_mode:
|
|
player_name = preview_data.get("nick", "Gladiator")
|
|
if player_name.strip_edges() == "":
|
|
player_name = "Gladiator"
|
|
current_hp = int(PREVIEW_MAX_HEALTH)
|
|
max_hp = int(PREVIEW_MAX_HEALTH)
|
|
# Combat stats come from the selected main weapon, or bare fists
|
|
var main_weapon = preview_data.get("main")
|
|
if main_weapon is WeaponData:
|
|
atk_damage = main_weapon.damage
|
|
atk_range = main_weapon.attack_range
|
|
atk_cooldown = main_weapon.attack_cooldown
|
|
else:
|
|
atk_damage = PREVIEW_ATTACK_DAMAGE
|
|
atk_range = PREVIEW_ATTACK_RANGE
|
|
atk_cooldown = PREVIEW_ATTACK_COOLDOWN
|
|
else:
|
|
var player_id = player.name.to_int()
|
|
player_name = "Player"
|
|
if Network.players.has(player_id):
|
|
player_name = Network.players[player_id]["nick"]
|
|
current_hp = int(player.current_health)
|
|
max_hp = int(player.max_health)
|
|
atk_damage = player.attack_damage
|
|
atk_range = player.attack_range
|
|
atk_cooldown = player.attack_cooldown
|
|
|
|
_add_stat_label("Name: " + player_name)
|
|
_add_stat_label("Level: 1") # Hardcoded for now
|
|
_add_stat_label("")
|
|
|
|
# Health stats
|
|
_add_stat_label("=== HEALTH ===", Color(0.8, 0.8, 0.8))
|
|
_add_stat_label("Current HP: " + str(current_hp))
|
|
_add_stat_label("Max HP: " + str(max_hp))
|
|
var hp_percent = int(float(current_hp) / max_hp * 100) if max_hp > 0 else 0
|
|
_add_stat_label("Health: " + str(hp_percent) + "%")
|
|
_add_stat_label("")
|
|
|
|
# Movement stats (class constants - same for live and preview)
|
|
_add_stat_label("=== MOVEMENT ===", Color(0.8, 0.8, 0.8))
|
|
_add_stat_label("Walk Speed: " + str(Character.NORMAL_SPEED))
|
|
_add_stat_label("Sprint Speed: " + str(Character.SPRINT_SPEED))
|
|
_add_stat_label("Jump Power: " + str(Character.JUMP_VELOCITY))
|
|
_add_stat_label("")
|
|
|
|
# Combat stats
|
|
_add_stat_label("=== COMBAT ===", Color(0.8, 0.8, 0.8))
|
|
if preview_mode and preview_data.get("main") is WeaponData:
|
|
_add_stat_label("Weapon Damage: " + str(atk_damage))
|
|
else:
|
|
_add_stat_label("Base Damage: " + str(atk_damage))
|
|
_add_stat_label("Attack Range: " + str(atk_range))
|
|
_add_stat_label("Attack Cooldown: " + str(atk_cooldown) + "s")
|
|
|
|
# Off-hand block bonus (preview only - handy when choosing a shield)
|
|
if preview_mode:
|
|
var offhand = preview_data.get("offhand")
|
|
if offhand is WeaponData and offhand.can_block:
|
|
_add_stat_label("")
|
|
_add_stat_label("=== DEFENSE ===", Color(0.8, 0.8, 0.8))
|
|
_add_stat_label("Block Reduction: " + str(int(offhand.block_reduction * 100)) + "%")
|
|
|
|
## Refresh equipped weapons
|
|
func _refresh_weapons():
|
|
if not weapons_container:
|
|
return
|
|
|
|
# Clear existing
|
|
for child in weapons_container.get_children():
|
|
child.queue_free()
|
|
|
|
# Add title
|
|
var title = Label.new()
|
|
title.text = "EQUIPPED WEAPONS"
|
|
title.add_theme_font_size_override("font_size", 20)
|
|
title.add_theme_color_override("font_color", Color(1.0, 0.8, 0.0))
|
|
weapons_container.add_child(title)
|
|
|
|
# Spacer
|
|
var spacer = Control.new()
|
|
spacer.custom_minimum_size = Vector2(0, 10)
|
|
weapons_container.add_child(spacer)
|
|
|
|
# Create horizontal container for weapon slots
|
|
var slots_container = HBoxContainer.new()
|
|
slots_container.add_theme_constant_override("separation", 15)
|
|
weapons_container.add_child(slots_container)
|
|
|
|
# Load weapon slot scene
|
|
var weapon_slot_scene = load("res://level/ui/scenes/weapon_slot.tscn")
|
|
if not weapon_slot_scene:
|
|
push_error("[CharacterSheet] Failed to load weapon_slot.tscn")
|
|
return
|
|
|
|
# Resolve weapon data from preview loadout or live player
|
|
var main_data: WeaponData = null
|
|
var offhand_data: WeaponData = null
|
|
if preview_mode:
|
|
if preview_data.get("main") is WeaponData:
|
|
main_data = preview_data["main"]
|
|
if preview_data.get("offhand") is WeaponData:
|
|
offhand_data = preview_data["offhand"]
|
|
else:
|
|
if player.equipped_weapon and player.equipped_weapon.weapon_data:
|
|
main_data = player.equipped_weapon.weapon_data
|
|
if player.equipped_offhand and player.equipped_offhand.weapon_data:
|
|
offhand_data = player.equipped_offhand.weapon_data
|
|
|
|
# Create main hand slot
|
|
var main_hand_slot = weapon_slot_scene.instantiate()
|
|
slots_container.add_child(main_hand_slot)
|
|
|
|
if main_data:
|
|
main_hand_slot.set_weapon(main_data, "Main Hand")
|
|
else:
|
|
main_hand_slot.clear_weapon()
|
|
|
|
# Create off-hand slot
|
|
var off_hand_slot = weapon_slot_scene.instantiate()
|
|
slots_container.add_child(off_hand_slot)
|
|
|
|
if offhand_data:
|
|
off_hand_slot.set_weapon(offhand_data, "Off-Hand")
|
|
else:
|
|
off_hand_slot.clear_weapon()
|
|
|
|
## Refresh abilities list
|
|
func _refresh_abilities():
|
|
if not abilities_container:
|
|
return
|
|
|
|
# Clear existing
|
|
for child in abilities_container.get_children():
|
|
child.queue_free()
|
|
|
|
# Add title
|
|
var title = Label.new()
|
|
title.text = "ABILITIES"
|
|
title.add_theme_font_size_override("font_size", 20)
|
|
title.add_theme_color_override("font_color", Color(1.0, 0.8, 0.0))
|
|
abilities_container.add_child(title)
|
|
|
|
# Spacer
|
|
var spacer = Control.new()
|
|
spacer.custom_minimum_size = Vector2(0, 10)
|
|
abilities_container.add_child(spacer)
|
|
|
|
# Dash ability (defaults in preview, live values in game)
|
|
var dash_cd = PREVIEW_DASH_COOLDOWN if preview_mode else player.dash_cooldown
|
|
var dash_dur = PREVIEW_DASH_DURATION if preview_mode else player.dash_duration
|
|
var dash_mult = PREVIEW_DASH_MULTIPLIER if preview_mode else player.dash_speed_multiplier
|
|
|
|
_add_stat_label("--- DASH (F) ---", Color(0.8, 0.8, 0.8))
|
|
_add_stat_label("Cooldown: " + str(dash_cd) + "s")
|
|
_add_stat_label("Duration: " + str(dash_dur) + "s")
|
|
_add_stat_label("Speed: " + str(dash_mult) + "x")
|
|
_add_stat_label("Description: Dash in movement direction")
|
|
if preview_mode:
|
|
_add_stat_label("Status: READY", Color(0.0, 1.0, 0.0))
|
|
else:
|
|
var dash_remaining = player._dash_cooldown_timer
|
|
if dash_remaining > 0:
|
|
_add_stat_label("Ready in: " + str(ceil(dash_remaining)) + "s", Color(1.0, 0.5, 0.5))
|
|
else:
|
|
_add_stat_label("Status: READY", Color(0.0, 1.0, 0.0))
|
|
|
|
# Spacer
|
|
var spacer2 = Control.new()
|
|
spacer2.custom_minimum_size = Vector2(0, 10)
|
|
abilities_container.add_child(spacer2)
|
|
|
|
# Jump ability
|
|
_add_stat_label("--- JUMP (Space) ---", Color(0.8, 0.8, 0.8))
|
|
_add_stat_label("Power: " + str(Character.JUMP_VELOCITY))
|
|
_add_stat_label("Description: Jump into the air")
|
|
_add_stat_label("Status: ALWAYS READY", Color(0.0, 1.0, 0.0))
|
|
|
|
## Helper to add a stat label
|
|
func _add_stat_label(text: String, color: Color = Color.WHITE):
|
|
var label = Label.new()
|
|
label.text = text
|
|
label.add_theme_color_override("font_color", color)
|
|
label.add_theme_color_override("font_outline_color", Color.BLACK)
|
|
label.add_theme_constant_override("outline_size", 1)
|
|
label.add_theme_font_size_override("font_size", 16)
|
|
|
|
if stats_container:
|
|
stats_container.add_child(label)
|
|
elif weapons_container:
|
|
weapons_container.add_child(label)
|
|
elif abilities_container:
|
|
abilities_container.add_child(label)
|