extends Node ## Autoload: manages all available player skins. ## ## Skins come from two places: ## 1. Built-in color tints for the procedural model (registered below). ## 2. assets/characters/skins/skins.json — written automatically by ## tools/pipeline.py for every imported character. No code changes needed ## to add a new model: run the pipeline, restart the game, it's there. ## ## The selected skin persists to user://skin_selection.cfg and is synced to ## other players via PlayerMovementController.synced_skin_id. signal skin_changed(skin_id: String) const SKINS_JSON := "res://assets/characters/skins/skins.json" const SELECTION_CFG := "user://skin_selection.cfg" var skins: Dictionary = {} var active_skin_id: String = "default" func _ready() -> void: _register_builtin_skins() _load_skins_json() _load_selection() print("SkinManager: %d skins available" % skins.size()) func _register_builtin_skins() -> void: _add_color_skin("default", "Default", "Standard issue", Color(0.2, 0.4, 0.8)) _add_color_skin("red_team", "Red Team", "Red team colors", Color(0.8, 0.2, 0.2)) _add_color_skin("forest", "Forest", "Forest camouflage", Color(0.2, 0.6, 0.2)) func _add_color_skin(id: String, display_name: String, description: String, tint: Color) -> void: var skin := PlayerSkin.new() skin.skin_name = display_name skin.description = description skin.color_tint = tint skin.is_unlocked = true skins[id] = skin func _load_skins_json() -> void: if not FileAccess.file_exists(SKINS_JSON): return var file := FileAccess.open(SKINS_JSON, FileAccess.READ) if not file: return var data = JSON.parse_string(file.get_as_text()) file.close() if not data is Dictionary or not data.has("skins"): push_warning("SkinManager: skins.json is malformed") return for entry in data["skins"]: if not entry is Dictionary or not entry.has("id"): continue var skin := PlayerSkin.new() skin.skin_name = entry.get("name", entry["id"]) skin.description = entry.get("description", "") skin.model_path = entry.get("model", "") skin.is_unlocked = entry.get("unlocked", true) skins[entry["id"]] = skin # ── Selection ───────────────────────────────────────────────────────────────── func get_skin(skin_id: String) -> PlayerSkin: return skins.get(skin_id, skins["default"]) func get_active_skin() -> PlayerSkin: return get_skin(active_skin_id) func set_active_skin(skin_id: String) -> void: if not skins.has(skin_id) or not skins[skin_id].is_unlocked: return active_skin_id = skin_id _save_selection() skin_changed.emit(skin_id) func get_skin_ids() -> Array: return skins.keys() func _save_selection() -> void: var cfg := ConfigFile.new() cfg.set_value("skin", "active", active_skin_id) cfg.save(SELECTION_CFG) func _load_selection() -> void: var cfg := ConfigFile.new() if cfg.load(SELECTION_CFG) == OK: var saved: String = cfg.get_value("skin", "active", "default") if skins.has(saved): active_skin_id = saved