131 lines
4.4 KiB
GDScript
131 lines
4.4 KiB
GDScript
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.viewmodel_sleeve_color = tint.darkened(0.18)
|
|
skin.viewmodel_accent_color = tint.lightened(0.22)
|
|
skin.viewmodel_glove_color = tint.darkened(0.48)
|
|
skin.viewmodel_nail_color = tint.lightened(0.34)
|
|
skin.viewmodel_hand_style = "fingerless"
|
|
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)
|
|
var viewmodel = entry.get("viewmodel", {})
|
|
if viewmodel is Dictionary:
|
|
skin.viewmodel_sleeve_color = _json_color(
|
|
viewmodel.get("sleeve", ""), skin.viewmodel_sleeve_color)
|
|
skin.viewmodel_accent_color = _json_color(
|
|
viewmodel.get("accent", ""), skin.viewmodel_accent_color)
|
|
skin.viewmodel_skin_color = _json_color(
|
|
viewmodel.get("skin", ""), skin.viewmodel_skin_color)
|
|
skin.viewmodel_glove_color = _json_color(
|
|
viewmodel.get("glove", ""), skin.viewmodel_glove_color)
|
|
skin.viewmodel_nail_color = _json_color(
|
|
viewmodel.get("nail", ""), skin.viewmodel_nail_color)
|
|
var hand_style: String = str(viewmodel.get(
|
|
"hand_style", skin.viewmodel_hand_style))
|
|
if hand_style in ["bare", "fingerless", "glove", "android"]:
|
|
skin.viewmodel_hand_style = hand_style
|
|
skin.viewmodel_sleeve_ratio = clampf(
|
|
float(viewmodel.get("sleeve_ratio", skin.viewmodel_sleeve_ratio)),
|
|
0.2, 0.9)
|
|
skins[entry["id"]] = skin
|
|
|
|
|
|
func _json_color(value: Variant, fallback: Color) -> Color:
|
|
if value is String and not value.is_empty():
|
|
return Color.from_string(value, fallback)
|
|
return fallback
|
|
|
|
|
|
# ── 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
|