feat: implement modular character skinning system with procedural and imported model support
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
extends Resource
|
||||||
|
class_name CharacterSkin
|
||||||
|
|
||||||
|
## Data-driven character appearance resource.
|
||||||
|
## Supports both procedural (box) models and imported GLTF/GLB models.
|
||||||
|
## Create .tres files to add new skins with zero code changes.
|
||||||
|
|
||||||
|
# ── Identity ──────────────────────────────────────────────────────────────────
|
||||||
|
@export var skin_id: String = "default"
|
||||||
|
@export var display_name: String = "Default"
|
||||||
|
|
||||||
|
# ── Mode ──────────────────────────────────────────────────────────────────────
|
||||||
|
## "procedural" = built-in box humanoid, "model" = imported GLTF/GLB scene
|
||||||
|
@export_enum("procedural", "model") var skin_type: String = "procedural"
|
||||||
|
|
||||||
|
# ── Procedural Mode (box humanoid) ────────────────────────────────────────────
|
||||||
|
@export_group("Procedural Appearance")
|
||||||
|
@export var primary_color: Color = Color(0.2, 0.4, 0.8)
|
||||||
|
@export var accent_color: Color = Color(0.15, 0.3, 0.6)
|
||||||
|
@export var emission_color: Color = Color.BLACK
|
||||||
|
@export var emission_energy: float = 0.0
|
||||||
|
@export var roughness: float = 0.8
|
||||||
|
@export var metallic: float = 0.0
|
||||||
|
|
||||||
|
# ── Model Mode (Sketchfab GLTF/GLB) ──────────────────────────────────────────
|
||||||
|
@export_group("Imported Model")
|
||||||
|
## Path to the imported scene (.glb, .gltf, .tscn)
|
||||||
|
@export var model_scene_path: String = ""
|
||||||
|
## Path to a Mixamo AnimationLibrary resource for retargeted animations
|
||||||
|
@export var animation_library_path: String = ""
|
||||||
|
## Godot SkeletonProfile name for retargeting (e.g. "SkeletonProfileHumanoid")
|
||||||
|
@export var skeleton_profile: String = ""
|
||||||
|
@export var model_scale: Vector3 = Vector3.ONE
|
||||||
|
@export var model_offset: Vector3 = Vector3.ZERO
|
||||||
|
|
||||||
|
# ── Shared Attachments & Effects ──────────────────────────────────────────────
|
||||||
|
@export_group("Attachments & Effects")
|
||||||
|
## Optional path to a hat/accessory scene
|
||||||
|
@export var hat_scene_path: String = ""
|
||||||
|
@export var trail_enabled: bool = false
|
||||||
|
@export var trail_color: Color = Color(0.5, 0.5, 1.0, 0.5)
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
## Creates a StandardMaterial3D from this skin's procedural properties.
|
||||||
|
func create_material() -> StandardMaterial3D:
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = primary_color
|
||||||
|
mat.roughness = roughness
|
||||||
|
mat.metallic = metallic
|
||||||
|
if emission_color != Color.BLACK and emission_energy > 0.0:
|
||||||
|
mat.emission_enabled = true
|
||||||
|
mat.emission = emission_color
|
||||||
|
mat.emission_energy_multiplier = emission_energy
|
||||||
|
return mat
|
||||||
|
|
||||||
|
## Creates a material for accent parts (limbs, etc.)
|
||||||
|
func create_accent_material() -> StandardMaterial3D:
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = accent_color
|
||||||
|
mat.roughness = roughness
|
||||||
|
mat.metallic = metallic
|
||||||
|
if emission_color != Color.BLACK and emission_energy > 0.0:
|
||||||
|
mat.emission_enabled = true
|
||||||
|
mat.emission = emission_color
|
||||||
|
mat.emission_energy_multiplier = emission_energy * 0.5
|
||||||
|
return mat
|
||||||
|
|
||||||
|
## Returns true if this skin uses an imported 3D model.
|
||||||
|
func is_model_skin() -> bool:
|
||||||
|
return skin_type == "model" and model_scene_path != ""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cnf5axdmy05i1
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
extends Node3D
|
extends Node3D
|
||||||
class_name HumanoidModel
|
class_name HumanoidModel
|
||||||
|
|
||||||
@export var color: Color = Color.WHITE
|
@export var skin: CharacterSkin
|
||||||
@export var shadows_only: bool = false
|
@export var shadows_only: bool = false
|
||||||
|
|
||||||
# Internal Pivots
|
# Internal Pivots (Procedural mode)
|
||||||
var root_pivot: Node3D
|
var root_pivot: Node3D
|
||||||
var torso: MeshInstance3D
|
var torso: MeshInstance3D
|
||||||
var head_pivot: Node3D
|
var head_pivot: Node3D
|
||||||
@@ -30,17 +30,112 @@ var thigh_r: MeshInstance3D
|
|||||||
var calf_r_pivot: Node3D
|
var calf_r_pivot: Node3D
|
||||||
var calf_r: MeshInstance3D
|
var calf_r: MeshInstance3D
|
||||||
|
|
||||||
|
# Imported model mode
|
||||||
|
var _imported_model: Node3D
|
||||||
|
var _anim_player: AnimationPlayer
|
||||||
|
|
||||||
# Animation State Variables
|
# Animation State Variables
|
||||||
var current_state: String = "idle"
|
var current_state: String = "idle"
|
||||||
var movement_speed: float = 0.0
|
var movement_speed: float = 0.0
|
||||||
var _anim_time: float = 0.0
|
var _anim_time: float = 0.0
|
||||||
|
|
||||||
|
# Legacy compat: allow setting color directly (falls back to creating a skin)
|
||||||
|
var color: Color = Color(0.2, 0.4, 0.8):
|
||||||
|
set(value):
|
||||||
|
color = value
|
||||||
|
if not skin:
|
||||||
|
skin = CharacterSkin.new()
|
||||||
|
skin.primary_color = value
|
||||||
|
skin.accent_color = value.darkened(0.2)
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
rotation_degrees.y = 180
|
rotation_degrees.y = 180
|
||||||
|
|
||||||
var mat := StandardMaterial3D.new()
|
# If no skin was set, try to get one from SkinManager or create from legacy color
|
||||||
mat.albedo_color = color
|
if not skin:
|
||||||
mat.roughness = 0.8
|
if Engine.has_singleton("SkinManager") or get_node_or_null("/root/SkinManager"):
|
||||||
|
var sm = get_node_or_null("/root/SkinManager")
|
||||||
|
if sm:
|
||||||
|
skin = sm.get_player_skin()
|
||||||
|
if not skin:
|
||||||
|
skin = CharacterSkin.new()
|
||||||
|
skin.primary_color = color
|
||||||
|
skin.accent_color = color.darkened(0.2)
|
||||||
|
|
||||||
|
if skin.is_model_skin():
|
||||||
|
_build_imported_model()
|
||||||
|
else:
|
||||||
|
_build_procedural_model()
|
||||||
|
|
||||||
|
func apply_skin(new_skin: CharacterSkin) -> void:
|
||||||
|
skin = new_skin
|
||||||
|
|
||||||
|
if skin.is_model_skin():
|
||||||
|
# Clear procedural model if present
|
||||||
|
if root_pivot:
|
||||||
|
root_pivot.queue_free()
|
||||||
|
root_pivot = null
|
||||||
|
if _imported_model:
|
||||||
|
_imported_model.queue_free()
|
||||||
|
_imported_model = null
|
||||||
|
_build_imported_model()
|
||||||
|
else:
|
||||||
|
# Clear imported model if present
|
||||||
|
if _imported_model:
|
||||||
|
_imported_model.queue_free()
|
||||||
|
_imported_model = null
|
||||||
|
if root_pivot:
|
||||||
|
# Just update materials on existing procedural model
|
||||||
|
_update_procedural_materials()
|
||||||
|
else:
|
||||||
|
_build_procedural_model()
|
||||||
|
|
||||||
|
# ── Imported Model (GLTF/GLB from Sketchfab) ─────────────────────────────────
|
||||||
|
|
||||||
|
func _build_imported_model() -> void:
|
||||||
|
var scene = load(skin.model_scene_path)
|
||||||
|
if not scene:
|
||||||
|
push_warning("Failed to load model scene: " + skin.model_scene_path)
|
||||||
|
_build_procedural_model()
|
||||||
|
return
|
||||||
|
|
||||||
|
_imported_model = scene.instantiate()
|
||||||
|
_imported_model.scale = skin.model_scale
|
||||||
|
_imported_model.position = skin.model_offset
|
||||||
|
add_child(_imported_model)
|
||||||
|
|
||||||
|
# Find AnimationPlayer in the imported model
|
||||||
|
_anim_player = _find_node_of_type(_imported_model, "AnimationPlayer") as AnimationPlayer
|
||||||
|
|
||||||
|
# Load additional Mixamo animation library if specified
|
||||||
|
if skin.animation_library_path != "":
|
||||||
|
var lib = load(skin.animation_library_path)
|
||||||
|
if lib and _anim_player:
|
||||||
|
_anim_player.add_animation_library("mixamo", lib)
|
||||||
|
|
||||||
|
# Apply shadow setting to all meshes in the imported model
|
||||||
|
if shadows_only:
|
||||||
|
_set_shadow_mode_recursive(_imported_model)
|
||||||
|
|
||||||
|
func _find_node_of_type(root: Node, type_name: String) -> Node:
|
||||||
|
if root.get_class() == type_name:
|
||||||
|
return root
|
||||||
|
for child in root.get_children():
|
||||||
|
var result = _find_node_of_type(child, type_name)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
return null
|
||||||
|
|
||||||
|
func _set_shadow_mode_recursive(node: Node) -> void:
|
||||||
|
if node is GeometryInstance3D:
|
||||||
|
node.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY
|
||||||
|
for child in node.get_children():
|
||||||
|
_set_shadow_mode_recursive(child)
|
||||||
|
|
||||||
|
# ── Procedural Model (box humanoid) ──────────────────────────────────────────
|
||||||
|
|
||||||
|
func _build_procedural_model() -> void:
|
||||||
|
var mat := skin.create_material()
|
||||||
|
|
||||||
var shadow_setting = GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY if shadows_only else GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
var shadow_setting = GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY if shadows_only else GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||||
|
|
||||||
@@ -163,15 +258,31 @@ func _ready() -> void:
|
|||||||
calf_r.cast_shadow = shadow_setting
|
calf_r.cast_shadow = shadow_setting
|
||||||
calf_r_pivot.add_child(calf_r)
|
calf_r_pivot.add_child(calf_r)
|
||||||
|
|
||||||
|
func _update_procedural_materials() -> void:
|
||||||
|
if not skin or not root_pivot:
|
||||||
|
return
|
||||||
|
var mat := skin.create_material()
|
||||||
|
# Update all mesh materials
|
||||||
|
for mesh_node in [torso, head, upper_arm_l, upper_arm_r, lower_arm_l, lower_arm_r,
|
||||||
|
thigh_l, thigh_r, calf_l, calf_r]:
|
||||||
|
if is_instance_valid(mesh_node) and mesh_node.mesh:
|
||||||
|
mesh_node.set_surface_override_material(0, mat)
|
||||||
|
|
||||||
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
|
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
|
||||||
current_state = state
|
current_state = state
|
||||||
movement_speed = speed
|
movement_speed = speed
|
||||||
if is_crouching and current_state == "ground":
|
if is_crouching and current_state == "ground":
|
||||||
current_state = "crouch"
|
current_state = "crouch"
|
||||||
|
|
||||||
|
# For imported models, try to play matching animation
|
||||||
|
if _imported_model and _anim_player:
|
||||||
|
_play_imported_animation()
|
||||||
|
|
||||||
var is_holding_weapon: bool = false
|
var is_holding_weapon: bool = false
|
||||||
|
|
||||||
func set_weapon(script_path: String) -> void:
|
func set_weapon(script_path: String) -> void:
|
||||||
|
if not root_pivot:
|
||||||
|
return
|
||||||
# Clear existing weapons from root_pivot
|
# Clear existing weapons from root_pivot
|
||||||
for child in root_pivot.get_children():
|
for child in root_pivot.get_children():
|
||||||
if child.has_meta("is_third_person_weapon"):
|
if child.has_meta("is_third_person_weapon"):
|
||||||
@@ -212,7 +323,52 @@ func _set_shadows_recursive(node: Node) -> void:
|
|||||||
for child in node.get_children():
|
for child in node.get_children():
|
||||||
_set_shadows_recursive(child)
|
_set_shadows_recursive(child)
|
||||||
|
|
||||||
|
# ── Imported Model Animation ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _play_imported_animation() -> void:
|
||||||
|
if not _anim_player:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Map movement states to common Mixamo animation names
|
||||||
|
var anim_map = {
|
||||||
|
"idle": ["Idle", "idle", "Standing Idle"],
|
||||||
|
"ground": ["Running", "Walking", "Run", "Walk", "running", "walking"],
|
||||||
|
"crouch": ["Crouching", "Crouch Idle", "crouch"],
|
||||||
|
"air": ["Falling", "Jump", "falling", "jump"],
|
||||||
|
"slide": ["Sliding", "Slide", "slide"],
|
||||||
|
"wall_run": ["Running", "Wall Run", "running"],
|
||||||
|
"wall_cling": ["Hanging Idle", "Climbing", "hanging"],
|
||||||
|
"dash": ["Sprint", "Dash", "sprint"],
|
||||||
|
"grapple": ["Falling", "falling"],
|
||||||
|
"death": ["Death", "Dying", "death"],
|
||||||
|
}
|
||||||
|
|
||||||
|
var state_key = current_state
|
||||||
|
if state_key == "ground" and movement_speed <= 1.0:
|
||||||
|
state_key = "idle"
|
||||||
|
|
||||||
|
if anim_map.has(state_key):
|
||||||
|
for anim_name in anim_map[state_key]:
|
||||||
|
# Check default library and mixamo library
|
||||||
|
if _anim_player.has_animation(anim_name):
|
||||||
|
if _anim_player.current_animation != anim_name:
|
||||||
|
_anim_player.play(anim_name)
|
||||||
|
return
|
||||||
|
var mixamo_name = "mixamo/" + anim_name
|
||||||
|
if _anim_player.has_animation(mixamo_name):
|
||||||
|
if _anim_player.current_animation != mixamo_name:
|
||||||
|
_anim_player.play(mixamo_name)
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Procedural Animation (unchanged from original) ──────────────────────────
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
|
# Imported models use AnimationPlayer, skip procedural animation
|
||||||
|
if _imported_model:
|
||||||
|
return
|
||||||
|
if not root_pivot:
|
||||||
|
return
|
||||||
|
|
||||||
var anim_speed = 1.0
|
var anim_speed = 1.0
|
||||||
if current_state == "ground" and movement_speed > 1.0:
|
if current_state == "ground" and movement_speed > 1.0:
|
||||||
anim_speed = movement_speed * 0.4
|
anim_speed = movement_speed * 0.4
|
||||||
|
|||||||
@@ -3,10 +3,18 @@ class_name ProceduralRagdoll
|
|||||||
|
|
||||||
var torso_body: RigidBody3D
|
var torso_body: RigidBody3D
|
||||||
|
|
||||||
func build_ragdoll(color: Color) -> void:
|
func build_ragdoll(skin_or_color = null) -> void:
|
||||||
var mat := StandardMaterial3D.new()
|
var mat: StandardMaterial3D
|
||||||
mat.albedo_color = color
|
if skin_or_color is CharacterSkin:
|
||||||
mat.roughness = 0.8
|
mat = skin_or_color.create_material()
|
||||||
|
elif skin_or_color is Color:
|
||||||
|
mat = StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = skin_or_color
|
||||||
|
mat.roughness = 0.8
|
||||||
|
else:
|
||||||
|
mat = StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = Color(0.2, 0.4, 0.8)
|
||||||
|
mat.roughness = 0.8
|
||||||
|
|
||||||
# Create bodies
|
# Create bodies
|
||||||
torso_body = _create_body(Vector3(0.4, 0.7, 0.25), mat, Vector3(0, 1.15, 0), 20.0, "Torso")
|
torso_body = _create_body(Vector3(0.4, 0.7, 0.25), mat, Vector3(0, 1.15, 0), 20.0, "Torso")
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
## SkinManager — Autoload singleton that manages character skins.
|
||||||
|
## Registers built-in skins on startup. Provides lookup by skin_id.
|
||||||
|
## Persists the player's selected skin to user://settings.json via SettingsManager.
|
||||||
|
|
||||||
|
var skins: Dictionary = {} # skin_id → CharacterSkin
|
||||||
|
var selected_skin_id: String = "default"
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_register_builtin_skins()
|
||||||
|
_load_selected_skin()
|
||||||
|
|
||||||
|
# ── Registration ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func register_skin(skin: CharacterSkin) -> void:
|
||||||
|
skins[skin.skin_id] = skin
|
||||||
|
|
||||||
|
func get_skin(id: String) -> CharacterSkin:
|
||||||
|
if skins.has(id):
|
||||||
|
return skins[id]
|
||||||
|
if skins.has("default"):
|
||||||
|
return skins["default"]
|
||||||
|
# Emergency fallback: create a default on the fly
|
||||||
|
var fallback = CharacterSkin.new()
|
||||||
|
fallback.skin_id = "default"
|
||||||
|
fallback.display_name = "Default"
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
func get_player_skin() -> CharacterSkin:
|
||||||
|
return get_skin(selected_skin_id)
|
||||||
|
|
||||||
|
func set_player_skin(id: String) -> void:
|
||||||
|
if skins.has(id):
|
||||||
|
selected_skin_id = id
|
||||||
|
_save_selected_skin()
|
||||||
|
_apply_skin_to_local_player()
|
||||||
|
|
||||||
|
func get_all_skin_ids() -> Array:
|
||||||
|
return skins.keys()
|
||||||
|
|
||||||
|
# ── Built-in Skins ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _register_builtin_skins() -> void:
|
||||||
|
_add_skin("default", "Default Blue",
|
||||||
|
Color(0.2, 0.4, 0.8), Color(0.15, 0.3, 0.6))
|
||||||
|
|
||||||
|
_add_skin("crimson", "Crimson",
|
||||||
|
Color(0.8, 0.15, 0.15), Color(0.6, 0.1, 0.1))
|
||||||
|
|
||||||
|
_add_skin("neon_green", "Neon Green",
|
||||||
|
Color(0.1, 0.9, 0.3), Color(0.05, 0.7, 0.2),
|
||||||
|
Color(0.1, 1.0, 0.3), 2.0)
|
||||||
|
|
||||||
|
_add_skin("gold", "Gold",
|
||||||
|
Color(0.85, 0.7, 0.2), Color(0.7, 0.55, 0.1),
|
||||||
|
Color.BLACK, 0.0, 0.3, 0.8)
|
||||||
|
|
||||||
|
_add_skin("ghost", "Ghost",
|
||||||
|
Color(0.9, 0.9, 0.95), Color(0.7, 0.7, 0.8),
|
||||||
|
Color(0.5, 0.5, 1.0), 0.5, 0.4, 0.1)
|
||||||
|
|
||||||
|
_add_skin("lava", "Lava",
|
||||||
|
Color(0.3, 0.05, 0.0), Color(0.2, 0.02, 0.0),
|
||||||
|
Color(1.0, 0.3, 0.0), 4.0, 0.9, 0.0)
|
||||||
|
|
||||||
|
_add_skin("midnight", "Midnight",
|
||||||
|
Color(0.08, 0.05, 0.15), Color(0.05, 0.03, 0.1),
|
||||||
|
Color(0.2, 0.1, 0.5), 1.0)
|
||||||
|
|
||||||
|
_add_skin("toxic", "Toxic",
|
||||||
|
Color(0.3, 0.5, 0.05), Color(0.2, 0.4, 0.02),
|
||||||
|
Color(0.5, 1.0, 0.0), 3.0)
|
||||||
|
|
||||||
|
func _add_skin(id: String, display: String, primary: Color, accent: Color,
|
||||||
|
emission: Color = Color.BLACK, emission_energy: float = 0.0,
|
||||||
|
roughness: float = 0.8, metallic: float = 0.0) -> void:
|
||||||
|
var skin = CharacterSkin.new()
|
||||||
|
skin.skin_id = id
|
||||||
|
skin.display_name = display
|
||||||
|
skin.primary_color = primary
|
||||||
|
skin.accent_color = accent
|
||||||
|
skin.emission_color = emission
|
||||||
|
skin.emission_energy = emission_energy
|
||||||
|
skin.roughness = roughness
|
||||||
|
skin.metallic = metallic
|
||||||
|
register_skin(skin)
|
||||||
|
|
||||||
|
# ── Persistence ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _save_selected_skin() -> void:
|
||||||
|
# Piggyback on SettingsManager's save file
|
||||||
|
var save_path = "user://skin_selection.json"
|
||||||
|
var data = {"selected_skin_id": selected_skin_id}
|
||||||
|
var file = FileAccess.open(save_path, FileAccess.WRITE)
|
||||||
|
if file:
|
||||||
|
file.store_string(JSON.stringify(data))
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
func _load_selected_skin() -> void:
|
||||||
|
var save_path = "user://skin_selection.json"
|
||||||
|
if FileAccess.file_exists(save_path):
|
||||||
|
var file = FileAccess.open(save_path, FileAccess.READ)
|
||||||
|
if file:
|
||||||
|
var json_string = file.get_as_text()
|
||||||
|
file.close()
|
||||||
|
var json = JSON.new()
|
||||||
|
if json.parse(json_string) == OK:
|
||||||
|
var data = json.get_data()
|
||||||
|
if data is Dictionary and data.has("selected_skin_id"):
|
||||||
|
var loaded_id = data["selected_skin_id"]
|
||||||
|
if skins.has(loaded_id):
|
||||||
|
selected_skin_id = loaded_id
|
||||||
|
|
||||||
|
# ── Live Update ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _apply_skin_to_local_player() -> void:
|
||||||
|
if not get_tree():
|
||||||
|
return
|
||||||
|
|
||||||
|
var pid = multiplayer.get_unique_id()
|
||||||
|
var players = get_tree().get_nodes_in_group("players")
|
||||||
|
var local_player = null
|
||||||
|
for p in players:
|
||||||
|
if p.name == str(pid):
|
||||||
|
local_player = p
|
||||||
|
break
|
||||||
|
|
||||||
|
if not local_player:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update the synced skin ID. The setter on the player handles updating visuals.
|
||||||
|
if "synced_skin_id" in local_player:
|
||||||
|
local_player.synced_skin_id = selected_skin_id
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://drls0sqj0oknp
|
||||||
@@ -31,6 +31,7 @@ func _build_geometry() -> void:
|
|||||||
|
|
||||||
func _spawn_player(pid: int) -> CharacterBody3D:
|
func _spawn_player(pid: int) -> CharacterBody3D:
|
||||||
var player = super._spawn_player(pid)
|
var player = super._spawn_player(pid)
|
||||||
|
player.add_to_group("players")
|
||||||
player.position = Vector3(randf_range(-2, 2), 1, 50 + randf_range(-2, 2))
|
player.position = Vector3(randf_range(-2, 2), 1, 50 + randf_range(-2, 2))
|
||||||
# Looking at (0,1,0) from (0,1,50) is facing straight down the -Z axis, which is rotation = 0,0,0
|
# Looking at (0,1,0) from (0,1,50) is facing straight down the -Z axis, which is rotation = 0,0,0
|
||||||
player.rotation = Vector3.ZERO
|
player.rotation = Vector3.ZERO
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ func _build_lighting() -> void:
|
|||||||
func _spawn_player(pid: int) -> CharacterBody3D:
|
func _spawn_player(pid: int) -> CharacterBody3D:
|
||||||
var player := CharacterBody3D.new()
|
var player := CharacterBody3D.new()
|
||||||
player.name = str(pid)
|
player.name = str(pid)
|
||||||
|
player.add_to_group("players")
|
||||||
player.set_multiplayer_authority(pid)
|
player.set_multiplayer_authority(pid)
|
||||||
|
|
||||||
# Use a slightly random spawn position to avoid exact overlapping
|
# Use a slightly random spawn position to avoid exact overlapping
|
||||||
@@ -326,6 +327,7 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
client_rep_config.add_property(":synced_loadout_ready")
|
client_rep_config.add_property(":synced_loadout_ready")
|
||||||
client_rep_config.add_property(":synced_is_targeting")
|
client_rep_config.add_property(":synced_is_targeting")
|
||||||
client_rep_config.add_property(":synced_homing_target_pos")
|
client_rep_config.add_property(":synced_homing_target_pos")
|
||||||
|
client_rep_config.add_property(":synced_skin_id")
|
||||||
client_sync.replication_config = client_rep_config
|
client_sync.replication_config = client_rep_config
|
||||||
player.add_child(client_sync)
|
player.add_child(client_sync)
|
||||||
|
|
||||||
@@ -346,7 +348,11 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
# Humanoid Model for Player (Shadows only locally, visible to others)
|
# Humanoid Model for Player (Shadows only locally, visible to others)
|
||||||
var humanoid = load("res://characters/humanoid_model.gd").new()
|
var humanoid = load("res://characters/humanoid_model.gd").new()
|
||||||
humanoid.name = "HumanoidModel"
|
humanoid.name = "HumanoidModel"
|
||||||
humanoid.color = Color(0.2, 0.4, 0.8) # Blueish for player
|
var skin_mgr = get_node_or_null("/root/SkinManager")
|
||||||
|
if skin_mgr and pid == multiplayer.get_unique_id():
|
||||||
|
humanoid.skin = skin_mgr.get_player_skin()
|
||||||
|
else:
|
||||||
|
humanoid.skin = skin_mgr.get_skin("default") if skin_mgr else null
|
||||||
humanoid.shadows_only = (pid == multiplayer.get_unique_id())
|
humanoid.shadows_only = (pid == multiplayer.get_unique_id())
|
||||||
humanoid.position = Vector3(0, -0.9, 0) # Offset from center to feet
|
humanoid.position = Vector3(0, -0.9, 0) # Offset from center to feet
|
||||||
player.add_child(humanoid)
|
player.add_child(humanoid)
|
||||||
|
|||||||
@@ -75,8 +75,38 @@ var synced_loadout_p2: String = ""
|
|||||||
var synced_loadout_sp: String = ""
|
var synced_loadout_sp: String = ""
|
||||||
var synced_loadout_melee: String = ""
|
var synced_loadout_melee: String = ""
|
||||||
var synced_loadout_ready: bool = false
|
var synced_loadout_ready: bool = false
|
||||||
|
@export var synced_skin_id: String = "default":
|
||||||
|
set(value):
|
||||||
|
var old = synced_skin_id
|
||||||
|
synced_skin_id = value
|
||||||
|
if old != value and is_node_ready():
|
||||||
|
_update_skin_visuals()
|
||||||
|
|
||||||
|
func _update_skin_visuals() -> void:
|
||||||
|
var sm = get_node_or_null("/root/SkinManager")
|
||||||
|
if not sm: return
|
||||||
|
|
||||||
|
var skin = sm.get_skin(synced_skin_id)
|
||||||
|
|
||||||
|
var humanoid = get_node_or_null("HumanoidModel")
|
||||||
|
if humanoid and humanoid.has_method("apply_skin"):
|
||||||
|
humanoid.apply_skin(skin)
|
||||||
|
|
||||||
|
var wman = get_node_or_null("HeadPivot/Camera3D/WeaponManager")
|
||||||
|
if wman and wman.has_method("refresh_arm_colors"):
|
||||||
|
wman.refresh_arm_colors()
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
# Set skin ID from SkinManager on authority
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
var sm_node = get_node_or_null("/root/SkinManager")
|
||||||
|
if sm_node:
|
||||||
|
synced_skin_id = sm_node.selected_skin_id
|
||||||
|
|
||||||
|
if not is_node_ready():
|
||||||
|
await ready
|
||||||
|
_update_skin_visuals()
|
||||||
|
|
||||||
var sm := _ensure_machine()
|
var sm := _ensure_machine()
|
||||||
if sm:
|
if sm:
|
||||||
sm.player = self
|
sm.player = self
|
||||||
@@ -783,7 +813,14 @@ func _on_movement_event(ev: String, data: Dictionary) -> void:
|
|||||||
elif ev == "grapple_latch":
|
elif ev == "grapple_latch":
|
||||||
grapple_latch_player.play()
|
grapple_latch_player.play()
|
||||||
|
|
||||||
|
var _last_synced_skin_id: String = ""
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
|
if synced_skin_id != _last_synced_skin_id:
|
||||||
|
_last_synced_skin_id = synced_skin_id
|
||||||
|
if is_node_ready():
|
||||||
|
_update_skin_visuals()
|
||||||
|
|
||||||
# Remote players: apply synced movement state to their HumanoidModel
|
# Remote players: apply synced movement state to their HumanoidModel
|
||||||
if not is_multiplayer_authority():
|
if not is_multiplayer_authority():
|
||||||
if synced_loadout_ready and not has_meta("remote_weapons_built"):
|
if synced_loadout_ready and not has_meta("remote_weapons_built"):
|
||||||
@@ -984,7 +1021,12 @@ func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
|||||||
ragdoll_instance = ragdoll
|
ragdoll_instance = ragdoll
|
||||||
get_tree().current_scene.add_child(ragdoll)
|
get_tree().current_scene.add_child(ragdoll)
|
||||||
ragdoll.global_transform = global_transform
|
ragdoll.global_transform = global_transform
|
||||||
ragdoll.build_ragdoll(Color(0.2, 0.4, 0.8)) # Blueish player color
|
# Use skin for ragdoll appearance
|
||||||
|
var skin_mgr = get_node_or_null("/root/SkinManager")
|
||||||
|
if skin_mgr:
|
||||||
|
ragdoll.build_ragdoll(skin_mgr.get_skin(synced_skin_id))
|
||||||
|
else:
|
||||||
|
ragdoll.build_ragdoll(Color(0.2, 0.4, 0.8))
|
||||||
|
|
||||||
# Wait a frame for physics to initialize then apply velocity
|
# Wait a frame for physics to initialize then apply velocity
|
||||||
get_tree().create_timer(0.01).timeout.connect(_apply_ragdoll_velocity.bind(ragdoll, velocity, impulse))
|
get_tree().create_timer(0.01).timeout.connect(_apply_ragdoll_velocity.bind(ragdoll, velocity, impulse))
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ SettingsManager="*res://globals/settings_manager.gd"
|
|||||||
LoadoutManager="*res://globals/loadout_manager.gd"
|
LoadoutManager="*res://globals/loadout_manager.gd"
|
||||||
PauseMenu="*res://ui/pause_menu.gd"
|
PauseMenu="*res://ui/pause_menu.gd"
|
||||||
NetworkManager="*res://globals/network_manager.gd"
|
NetworkManager="*res://globals/network_manager.gd"
|
||||||
|
SkinManager="*res://characters/skin_manager.gd"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
client_rep_config.add_property(":synced_loadout_ready")
|
client_rep_config.add_property(":synced_loadout_ready")
|
||||||
client_rep_config.add_property(":synced_is_targeting")
|
client_rep_config.add_property(":synced_is_targeting")
|
||||||
client_rep_config.add_property(":synced_homing_target_pos")
|
client_rep_config.add_property(":synced_homing_target_pos")
|
||||||
|
client_rep_config.add_property(":synced_skin_id")
|
||||||
client_sync.replication_config = client_rep_config
|
client_sync.replication_config = client_rep_config
|
||||||
player.add_child(client_sync)
|
player.add_child(client_sync)
|
||||||
|
|
||||||
@@ -144,7 +145,11 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
# Humanoid Model for Player (Shadows only locally, visible to others)
|
# Humanoid Model for Player (Shadows only locally, visible to others)
|
||||||
var humanoid = load("res://characters/humanoid_model.gd").new()
|
var humanoid = load("res://characters/humanoid_model.gd").new()
|
||||||
humanoid.name = "HumanoidModel"
|
humanoid.name = "HumanoidModel"
|
||||||
humanoid.color = Color(0.2, 0.4, 0.8) # Blueish for player
|
var skin_mgr = get_node_or_null("/root/SkinManager")
|
||||||
|
if skin_mgr and pid == multiplayer.get_unique_id():
|
||||||
|
humanoid.skin = skin_mgr.get_player_skin()
|
||||||
|
else:
|
||||||
|
humanoid.skin = skin_mgr.get_skin("default") if skin_mgr else null
|
||||||
humanoid.shadows_only = (pid == multiplayer.get_unique_id())
|
humanoid.shadows_only = (pid == multiplayer.get_unique_id())
|
||||||
humanoid.position = Vector3(0, -0.9, 0) # Offset from center to feet
|
humanoid.position = Vector3(0, -0.9, 0) # Offset from center to feet
|
||||||
player.add_child(humanoid)
|
player.add_child(humanoid)
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ func _ready() -> void:
|
|||||||
func _spawn_player(pid: int) -> CharacterBody3D:
|
func _spawn_player(pid: int) -> CharacterBody3D:
|
||||||
var player := CharacterBody3D.new()
|
var player := CharacterBody3D.new()
|
||||||
player.name = str(pid)
|
player.name = str(pid)
|
||||||
|
player.add_to_group("players")
|
||||||
player.set_multiplayer_authority(pid)
|
player.set_multiplayer_authority(pid)
|
||||||
|
|
||||||
# Alternate spawn sides (Team A / Team B style)
|
# Alternate spawn sides (Team A / Team B style)
|
||||||
@@ -90,6 +91,7 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
client_rep_config.add_property(":synced_loadout_ready")
|
client_rep_config.add_property(":synced_loadout_ready")
|
||||||
client_rep_config.add_property(":synced_is_targeting")
|
client_rep_config.add_property(":synced_is_targeting")
|
||||||
client_rep_config.add_property(":synced_homing_target_pos")
|
client_rep_config.add_property(":synced_homing_target_pos")
|
||||||
|
client_rep_config.add_property(":synced_skin_id")
|
||||||
client_sync.replication_config = client_rep_config
|
client_sync.replication_config = client_rep_config
|
||||||
player.add_child(client_sync)
|
player.add_child(client_sync)
|
||||||
|
|
||||||
@@ -110,7 +112,11 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
# Humanoid Model for Player (Shadows only locally, visible to others)
|
# Humanoid Model for Player (Shadows only locally, visible to others)
|
||||||
var humanoid = load("res://characters/humanoid_model.gd").new()
|
var humanoid = load("res://characters/humanoid_model.gd").new()
|
||||||
humanoid.name = "HumanoidModel"
|
humanoid.name = "HumanoidModel"
|
||||||
humanoid.color = Color(0.2, 0.4, 0.8) # Blueish for player
|
var skin_mgr = get_node_or_null("/root/SkinManager")
|
||||||
|
if skin_mgr and pid == multiplayer.get_unique_id():
|
||||||
|
humanoid.skin = skin_mgr.get_player_skin()
|
||||||
|
else:
|
||||||
|
humanoid.skin = skin_mgr.get_skin("default") if skin_mgr else null
|
||||||
humanoid.shadows_only = (pid == multiplayer.get_unique_id())
|
humanoid.shadows_only = (pid == multiplayer.get_unique_id())
|
||||||
humanoid.position = Vector3(0, -0.9, 0) # Offset from center to feet
|
humanoid.position = Vector3(0, -0.9, 0) # Offset from center to feet
|
||||||
player.add_child(humanoid)
|
player.add_child(humanoid)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ var main_vbox: VBoxContainer
|
|||||||
var resume_btn: Button
|
var resume_btn: Button
|
||||||
var respawn_btn: Button
|
var respawn_btn: Button
|
||||||
var loadouts_btn: Button
|
var loadouts_btn: Button
|
||||||
|
var character_btn: Button
|
||||||
var settings_btn: Button
|
var settings_btn: Button
|
||||||
var return_btn: Button
|
var return_btn: Button
|
||||||
var exit_btn: Button
|
var exit_btn: Button
|
||||||
@@ -51,6 +52,12 @@ var rebind_overlay: ColorRect
|
|||||||
var rebind_label: Label
|
var rebind_label: Label
|
||||||
var waiting_for_input_action: String = ""
|
var waiting_for_input_action: String = ""
|
||||||
|
|
||||||
|
# Character Skin UI
|
||||||
|
var character_editor: Control
|
||||||
|
var character_back_btn: Button
|
||||||
|
var skin_grid: GridContainer
|
||||||
|
var skin_preview_label: Label
|
||||||
|
|
||||||
var quit_dialog: ConfirmationDialog
|
var quit_dialog: ConfirmationDialog
|
||||||
var return_dialog: ConfirmationDialog
|
var return_dialog: ConfirmationDialog
|
||||||
|
|
||||||
@@ -146,6 +153,11 @@ func _build_ui() -> void:
|
|||||||
loadouts_btn.custom_minimum_size = Vector2(200, 50)
|
loadouts_btn.custom_minimum_size = Vector2(200, 50)
|
||||||
main_vbox.add_child(loadouts_btn)
|
main_vbox.add_child(loadouts_btn)
|
||||||
|
|
||||||
|
character_btn = Button.new()
|
||||||
|
character_btn.text = "Character"
|
||||||
|
character_btn.custom_minimum_size = Vector2(200, 50)
|
||||||
|
main_vbox.add_child(character_btn)
|
||||||
|
|
||||||
settings_btn = Button.new()
|
settings_btn = Button.new()
|
||||||
settings_btn.text = "Settings"
|
settings_btn.text = "Settings"
|
||||||
settings_btn.custom_minimum_size = Vector2(200, 50)
|
settings_btn.custom_minimum_size = Vector2(200, 50)
|
||||||
@@ -667,6 +679,48 @@ func _build_ui() -> void:
|
|||||||
rebind_label.add_theme_font_size_override("font_size", 32)
|
rebind_label.add_theme_font_size_override("font_size", 32)
|
||||||
rebind_center.add_child(rebind_label)
|
rebind_center.add_child(rebind_label)
|
||||||
|
|
||||||
|
# ── Character Editor Root ──
|
||||||
|
character_editor = Control.new()
|
||||||
|
character_editor.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
bg.add_child(character_editor)
|
||||||
|
|
||||||
|
var char_center = CenterContainer.new()
|
||||||
|
char_center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
character_editor.add_child(char_center)
|
||||||
|
|
||||||
|
var char_vbox = VBoxContainer.new()
|
||||||
|
char_vbox.custom_minimum_size = Vector2(600, 500)
|
||||||
|
char_vbox.add_theme_constant_override("separation", 15)
|
||||||
|
char_center.add_child(char_vbox)
|
||||||
|
|
||||||
|
var char_title = Label.new()
|
||||||
|
char_title.text = "CHARACTER SKIN"
|
||||||
|
char_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
char_title.add_theme_font_size_override("font_size", 32)
|
||||||
|
char_vbox.add_child(char_title)
|
||||||
|
|
||||||
|
skin_preview_label = Label.new()
|
||||||
|
skin_preview_label.text = "Selected: Default Blue"
|
||||||
|
skin_preview_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
skin_preview_label.add_theme_font_size_override("font_size", 18)
|
||||||
|
char_vbox.add_child(skin_preview_label)
|
||||||
|
|
||||||
|
var skin_scroll = ScrollContainer.new()
|
||||||
|
skin_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
char_vbox.add_child(skin_scroll)
|
||||||
|
|
||||||
|
skin_grid = GridContainer.new()
|
||||||
|
skin_grid.columns = 4
|
||||||
|
skin_grid.add_theme_constant_override("h_separation", 12)
|
||||||
|
skin_grid.add_theme_constant_override("v_separation", 12)
|
||||||
|
skin_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
skin_scroll.add_child(skin_grid)
|
||||||
|
|
||||||
|
character_back_btn = Button.new()
|
||||||
|
character_back_btn.text = "Back to Menu"
|
||||||
|
character_back_btn.custom_minimum_size = Vector2(0, 40)
|
||||||
|
char_vbox.add_child(character_back_btn)
|
||||||
|
|
||||||
|
|
||||||
# ── Quit Dialog ──
|
# ── Quit Dialog ──
|
||||||
quit_dialog = ConfirmationDialog.new()
|
quit_dialog = ConfirmationDialog.new()
|
||||||
@@ -734,6 +788,8 @@ func _connect_signals() -> void:
|
|||||||
save_loadout_btn.pressed.connect(_save_current_loadout)
|
save_loadout_btn.pressed.connect(_save_current_loadout)
|
||||||
quit_dialog.confirmed.connect(_quit_game)
|
quit_dialog.confirmed.connect(_quit_game)
|
||||||
return_dialog.confirmed.connect(_return_to_main_menu)
|
return_dialog.confirmed.connect(_return_to_main_menu)
|
||||||
|
character_btn.pressed.connect(_show_character)
|
||||||
|
character_back_btn.pressed.connect(_show_main_menu)
|
||||||
|
|
||||||
func _show_return_dialog() -> void:
|
func _show_return_dialog() -> void:
|
||||||
return_dialog.popup_centered()
|
return_dialog.popup_centered()
|
||||||
@@ -753,14 +809,75 @@ func _show_main_menu() -> void:
|
|||||||
main_vbox.visible = true
|
main_vbox.visible = true
|
||||||
loadout_editor.visible = false
|
loadout_editor.visible = false
|
||||||
settings_editor.visible = false
|
settings_editor.visible = false
|
||||||
|
character_editor.visible = false
|
||||||
|
|
||||||
func _show_loadouts() -> void:
|
func _show_loadouts() -> void:
|
||||||
main_vbox.visible = false
|
main_vbox.visible = false
|
||||||
settings_editor.visible = false
|
settings_editor.visible = false
|
||||||
|
character_editor.visible = false
|
||||||
loadout_editor.visible = true
|
loadout_editor.visible = true
|
||||||
edit_panel.visible = false
|
edit_panel.visible = false
|
||||||
_populate_loadout_list()
|
_populate_loadout_list()
|
||||||
|
|
||||||
|
func _show_character() -> void:
|
||||||
|
main_vbox.visible = false
|
||||||
|
settings_editor.visible = false
|
||||||
|
loadout_editor.visible = false
|
||||||
|
character_editor.visible = true
|
||||||
|
_populate_skin_grid()
|
||||||
|
|
||||||
|
func _populate_skin_grid() -> void:
|
||||||
|
for c in skin_grid.get_children():
|
||||||
|
c.queue_free()
|
||||||
|
|
||||||
|
var skin_mgr = get_node_or_null("/root/SkinManager")
|
||||||
|
if not skin_mgr: return
|
||||||
|
|
||||||
|
var current_id = skin_mgr.selected_skin_id
|
||||||
|
var all_ids = skin_mgr.get_all_skin_ids()
|
||||||
|
|
||||||
|
for id in all_ids:
|
||||||
|
var skin = skin_mgr.get_skin(id)
|
||||||
|
|
||||||
|
var panel = PanelContainer.new()
|
||||||
|
var style = StyleBoxFlat.new()
|
||||||
|
style.bg_color = skin.primary_color
|
||||||
|
style.corner_radius_top_left = 8
|
||||||
|
style.corner_radius_top_right = 8
|
||||||
|
style.corner_radius_bottom_left = 8
|
||||||
|
style.corner_radius_bottom_right = 8
|
||||||
|
if id == current_id:
|
||||||
|
style.border_width_left = 4
|
||||||
|
style.border_width_right = 4
|
||||||
|
style.border_width_top = 4
|
||||||
|
style.border_width_bottom = 4
|
||||||
|
style.border_color = Color.WHITE
|
||||||
|
skin_preview_label.text = "Selected: " + skin.display_name
|
||||||
|
panel.add_theme_stylebox_override("panel", style)
|
||||||
|
panel.custom_minimum_size = Vector2(120, 120)
|
||||||
|
|
||||||
|
var btn = Button.new()
|
||||||
|
btn.text = skin.display_name
|
||||||
|
btn.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
var btn_style = StyleBoxEmpty.new()
|
||||||
|
btn.add_theme_stylebox_override("normal", btn_style)
|
||||||
|
btn.add_theme_stylebox_override("hover", btn_style)
|
||||||
|
btn.add_theme_stylebox_override("pressed", btn_style)
|
||||||
|
btn.add_theme_stylebox_override("focus", btn_style)
|
||||||
|
|
||||||
|
# Text shadow for readability
|
||||||
|
btn.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
btn.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
btn.add_theme_constant_override("outline_size", 4)
|
||||||
|
|
||||||
|
btn.pressed.connect(func():
|
||||||
|
skin_mgr.set_player_skin(id)
|
||||||
|
_populate_skin_grid() # Refresh borders
|
||||||
|
)
|
||||||
|
|
||||||
|
panel.add_child(btn)
|
||||||
|
skin_grid.add_child(panel)
|
||||||
|
|
||||||
func _populate_video_ui() -> void:
|
func _populate_video_ui() -> void:
|
||||||
if _display_mode_opt:
|
if _display_mode_opt:
|
||||||
for i in range(_display_mode_opt.item_count):
|
for i in range(_display_mode_opt.item_count):
|
||||||
|
|||||||
@@ -253,8 +253,13 @@ func _add_procedural_arms(weapon: Node3D) -> void:
|
|||||||
# Attach to weapon instead of model_root to avoid arms spinning on reload
|
# Attach to weapon instead of model_root to avoid arms spinning on reload
|
||||||
|
|
||||||
var mat = StandardMaterial3D.new()
|
var mat = StandardMaterial3D.new()
|
||||||
mat.albedo_color = Color(0.2, 0.4, 0.8) # Player color
|
var skin_mgr = get_node_or_null("/root/SkinManager")
|
||||||
mat.roughness = 0.8
|
if skin_mgr and is_instance_valid(player) and "synced_skin_id" in player:
|
||||||
|
var skin = skin_mgr.get_skin(player.synced_skin_id)
|
||||||
|
mat = skin.create_material()
|
||||||
|
else:
|
||||||
|
mat.albedo_color = Color(0.2, 0.4, 0.8) # Fallback
|
||||||
|
mat.roughness = 0.8
|
||||||
|
|
||||||
# Right Arm (Main Grip)
|
# Right Arm (Main Grip)
|
||||||
var r_arm = MeshInstance3D.new()
|
var r_arm = MeshInstance3D.new()
|
||||||
@@ -264,6 +269,7 @@ func _add_procedural_arms(weapon: Node3D) -> void:
|
|||||||
r_arm.mesh = r_mesh
|
r_arm.mesh = r_mesh
|
||||||
|
|
||||||
var r_pivot = Node3D.new()
|
var r_pivot = Node3D.new()
|
||||||
|
r_pivot.name = "ProceduralArmPivotR"
|
||||||
var r_shoulder = Vector3(0.25, -0.3, 0.5)
|
var r_shoulder = Vector3(0.25, -0.3, 0.5)
|
||||||
var r_hand = Vector3(0.04, -0.05, 0.05)
|
var r_hand = Vector3(0.04, -0.05, 0.05)
|
||||||
r_pivot.position = r_shoulder
|
r_pivot.position = r_shoulder
|
||||||
@@ -283,6 +289,7 @@ func _add_procedural_arms(weapon: Node3D) -> void:
|
|||||||
l_arm.mesh = l_mesh
|
l_arm.mesh = l_mesh
|
||||||
|
|
||||||
var l_pivot = Node3D.new()
|
var l_pivot = Node3D.new()
|
||||||
|
l_pivot.name = "ProceduralArmPivotL"
|
||||||
var l_shoulder = Vector3(-0.25, -0.3, 0.4)
|
var l_shoulder = Vector3(-0.25, -0.3, 0.4)
|
||||||
var l_hand = Vector3(-0.02, -0.02, -0.3)
|
var l_hand = Vector3(-0.02, -0.02, -0.3)
|
||||||
l_pivot.position = l_shoulder
|
l_pivot.position = l_shoulder
|
||||||
@@ -341,3 +348,19 @@ func _input(event: InputEvent) -> void:
|
|||||||
if weapons.has(4):
|
if weapons.has(4):
|
||||||
if weapons[4].has_method("do_quick_melee"):
|
if weapons[4].has_method("do_quick_melee"):
|
||||||
weapons[4].do_quick_melee(prev)
|
weapons[4].do_quick_melee(prev)
|
||||||
|
|
||||||
|
## Re-equips the current weapon to refresh arm colors after a skin change.
|
||||||
|
func refresh_arm_colors() -> void:
|
||||||
|
var skin_mgr = get_node_or_null("/root/SkinManager")
|
||||||
|
if not skin_mgr or not is_instance_valid(player) or not "synced_skin_id" in player:
|
||||||
|
return
|
||||||
|
var skin = skin_mgr.get_skin(player.synced_skin_id)
|
||||||
|
var mat = skin.create_material()
|
||||||
|
|
||||||
|
for w in weapons.values():
|
||||||
|
if not is_instance_valid(w): continue
|
||||||
|
for child in w.get_children():
|
||||||
|
if child.name.begins_with("ProceduralArmPivot"):
|
||||||
|
for grand_child in child.get_children():
|
||||||
|
if grand_child is MeshInstance3D and grand_child.mesh:
|
||||||
|
grand_child.set_surface_override_material(0, mat)
|
||||||
|
|||||||
Reference in New Issue
Block a user