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)