## Character — base class for all playable figures (bunny, cat, etc.). class_name Character extends Node2D const _CharacterData = preload("res://scripts/characters/character_data.gd") signal character_picked_up(character: Character) signal character_placed(character: Character, position: Vector2) signal state_changed(new_state: CharacterData.State) @export var character_id: String = "" @export var display_name: String = "" @export var data: CharacterData var _is_held: bool = false var _current_anim: String = "idle" var _drag_start_position: Vector2 = Vector2.ZERO var _outfit_item_refs: Array = [null, null, null] const _TAP_THRESHOLD: float = 10.0 const _ITEM_DROP_OFFSET: Vector2 = Vector2(0.0, 60.0) const _STATE_COLORS: Dictionary = { CharacterData.State.HEALTHY: Color(0.6, 0.8, 1.0), CharacterData.State.SICK: Color(0.7, 0.9, 0.7), CharacterData.State.SLEEPING: Color(0.8, 0.7, 0.95), CharacterData.State.TIRED: Color(1.0, 0.95, 0.6), CharacterData.State.PREGNANT: Color(1.0, 0.85, 0.9), CharacterData.State.BABY: Color(0.9, 0.95, 1.0), } func _ready() -> void: var drag: DragDropComponent = get_node_or_null("DragDropComponent") as DragDropComponent if drag != null: drag.drag_picked_up.connect(_on_drag_picked_up) drag.drag_released.connect(_on_drag_released) if data != null: _update_visual_state() _refresh_outfit_layers() add_to_group("characters") func set_state(new_state: CharacterData.State) -> void: if data == null: return data.state = new_state _update_visual_state() state_changed.emit(new_state) func set_animation_state(anim: String) -> void: _current_anim = anim var sprite: AnimatedSprite2D = get_node_or_null("AnimatedSprite2D") as AnimatedSprite2D if sprite == null or sprite.sprite_frames == null: return if sprite.sprite_frames.has_animation(anim): sprite.play(anim) func get_animation_state() -> String: return _current_anim func set_outfit(layer: int, item_id: String, texture: Texture2D) -> void: if layer < 1 or layer > 3: return if data != null: data.outfit[layer - 1] = item_id var layer_node: Sprite2D = get_node_or_null("OutfitLayer%d" % layer) as Sprite2D if layer_node == null: return layer_node.texture = texture layer_node.visible = not item_id.is_empty() func clear_outfit(layer: int) -> String: if layer < 1 or layer > 3: return "" var old_id: String = "" if data != null: old_id = data.outfit[layer - 1] data.outfit[layer - 1] = "" var layer_node: Sprite2D = get_node_or_null("OutfitLayer%d" % layer) as Sprite2D if layer_node != null: layer_node.texture = null layer_node.visible = false return old_id func get_outfit(layer: int) -> String: if data == null or layer < 1 or layer > 3: return "" return data.outfit[layer - 1] func attach_item(hand: String, item: Node2D) -> bool: if hand != "left" and hand != "right": return false var slot: Node2D = get_node_or_null("Hand" + hand.capitalize()) as Node2D if slot == null: return false if slot.get_child_count() > 0: return false var old_parent: Node = item.get_parent() if old_parent != null: old_parent.remove_child(item) slot.add_child(item) item.position = Vector2.ZERO return true func detach_item(hand: String) -> Node2D: if hand != "left" and hand != "right": return null var slot: Node2D = get_node_or_null("Hand" + hand.capitalize()) as Node2D if slot == null or slot.get_child_count() == 0: return null var item: Node2D = slot.get_child(0) as Node2D var saved_pos: Vector2 = item.global_position slot.remove_child(item) var scene_parent: Node = get_parent() if scene_parent != null: scene_parent.add_child(item) item.global_position = saved_pos return item func get_held_item(hand: String) -> Node2D: if hand != "left" and hand != "right": return null var slot: Node2D = get_node_or_null("Hand" + hand.capitalize()) as Node2D if slot == null or slot.get_child_count() == 0: return null return slot.get_child(0) as Node2D func is_hand_free(hand: String) -> bool: return get_held_item(hand) == null func _update_visual_state() -> void: if data == null: return var visual: ColorRect = get_node_or_null("Visual") as ColorRect if visual == null: return var color: Color = _STATE_COLORS.get(data.state, Color(0.6, 0.8, 1.0)) visual.color = color var ear_left: ColorRect = get_node_or_null("Ears/EarLeft") as ColorRect var ear_right: ColorRect = get_node_or_null("Ears/EarRight") as ColorRect if ear_left != null: ear_left.color = color if ear_right != null: ear_right.color = color func apply_outfit_item(layer: int, item_id: String, texture: Texture2D, item_node: Node2D) -> void: if layer < 1 or layer > 3: return var i: int = layer - 1 var existing: Node2D = _outfit_item_refs[i] as Node2D if existing != null: existing.global_position = global_position + _ITEM_DROP_OFFSET existing.visible = true _outfit_item_refs[i] = item_node set_outfit(layer, item_id, texture) if item_node != null: item_node.visible = false func remove_outfit(layer: int) -> void: if layer < 1 or layer > 3: return var i: int = layer - 1 clear_outfit(layer) var item_ref: Node2D = _outfit_item_refs[i] as Node2D if item_ref != null: _outfit_item_refs[i] = null item_ref.global_position = global_position + _ITEM_DROP_OFFSET item_ref.visible = true func _handle_outfit_tap() -> void: for layer: int in range(3, 0, -1): if not get_outfit(layer).is_empty(): remove_outfit(layer) return func _on_drag_picked_up(pos: Vector2) -> void: AudioManager.play_sfx("character_pickup") _is_held = true _drag_start_position = pos set_animation_state("held") character_picked_up.emit(self) func _on_drag_released(pos: Vector2) -> void: _is_held = false var drag_distance: float = pos.distance_to(_drag_start_position) if drag_distance < _TAP_THRESHOLD: AudioManager.play_sfx("character_tap") set_animation_state("idle") _handle_outfit_tap() return AudioManager.play_sfx("character_place") set_animation_state("idle") if data == null or data.id.is_empty(): return GameState.set_character_position(character_id, global_position) character_placed.emit(self, global_position) func _refresh_outfit_layers() -> void: if data == null: return for i: int in range(3): var layer_node: Sprite2D = get_node_or_null("OutfitLayer%d" % (i + 1)) as Sprite2D if layer_node != null: layer_node.visible = not data.outfit[i].is_empty()