Compare commits
10 Commits
33a1c0aaf9
...
48b9e8f8f3
| Author | SHA1 | Date | |
|---|---|---|---|
| 48b9e8f8f3 | |||
| ce697edd2b | |||
| 2f5e9d99a6 | |||
| 60fba44316 | |||
| 1a9d916293 | |||
| 9be67c8dfe | |||
| 15ac8666f8 | |||
| 2c576ad419 | |||
| 80cecf732d | |||
| cc5f205a7e |
+4
-4
@@ -32,6 +32,10 @@ window/size/viewport_height=720
|
|||||||
window/stretch/mode="canvas_items"
|
window/stretch/mode="canvas_items"
|
||||||
window/stretch/aspect="expand"
|
window/stretch/aspect="expand"
|
||||||
|
|
||||||
|
[editor_plugins]
|
||||||
|
|
||||||
|
enabled=PackedStringArray("res://addons/gut/plugin.cfg")
|
||||||
|
|
||||||
[input_devices]
|
[input_devices]
|
||||||
|
|
||||||
pointing/emulate_touch_from_mouse=true
|
pointing/emulate_touch_from_mouse=true
|
||||||
@@ -41,7 +45,3 @@ pointing/emulate_touch_from_mouse=true
|
|||||||
renderer/rendering_method="gl_compatibility"
|
renderer/rendering_method="gl_compatibility"
|
||||||
renderer/rendering_method.mobile="gl_compatibility"
|
renderer/rendering_method.mobile="gl_compatibility"
|
||||||
textures/vram_compression/import_etc2_astc=true
|
textures/vram_compression/import_etc2_astc=true
|
||||||
|
|
||||||
[editor_plugins]
|
|
||||||
|
|
||||||
enabled=PackedStringArray("res://addons/gut/plugin.cfg")
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
[gd_scene load_steps=4 format=3 uid="uid://cozypaw_char"]
|
[gd_scene load_steps=5 format=3 uid="uid://cozypaw_char"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scripts/characters/character.gd" id="1_char"]
|
[ext_resource type="Script" path="res://scripts/characters/character.gd" id="1_char"]
|
||||||
[ext_resource type="Script" path="res://scripts/systems/drag_drop_component.gd" id="2_drag"]
|
[ext_resource type="Script" path="res://scripts/systems/drag_drop_component.gd" id="2_drag"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/characters/snap_receiver.gd" id="3_snap_recv"]
|
||||||
|
|
||||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_char"]
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_char"]
|
||||||
size = Vector2(64, 80)
|
size = Vector2(64, 80)
|
||||||
@@ -49,3 +50,27 @@ input_pickable = true
|
|||||||
[node name="CollisionShape" type="CollisionShape2D" parent="CollisionArea"]
|
[node name="CollisionShape" type="CollisionShape2D" parent="CollisionArea"]
|
||||||
shape = SubResource("RectangleShape2D_char")
|
shape = SubResource("RectangleShape2D_char")
|
||||||
position = Vector2(0, -40)
|
position = Vector2(0, -40)
|
||||||
|
|
||||||
|
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="OutfitLayer1" type="Sprite2D" parent="."]
|
||||||
|
position = Vector2(0, -40)
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="OutfitLayer2" type="Sprite2D" parent="."]
|
||||||
|
position = Vector2(0, -40)
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="OutfitLayer3" type="Sprite2D" parent="."]
|
||||||
|
position = Vector2(0, -40)
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="HandLeft" type="Node2D" parent="."]
|
||||||
|
position = Vector2(-32, -30)
|
||||||
|
|
||||||
|
[node name="HandRight" type="Node2D" parent="."]
|
||||||
|
position = Vector2(32, -30)
|
||||||
|
|
||||||
|
[node name="SnapReceiver" type="Node" parent="."]
|
||||||
|
script = ExtResource("3_snap_recv")
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ signal state_changed(new_state: CharacterData.State)
|
|||||||
@export var data: CharacterData
|
@export var data: CharacterData
|
||||||
|
|
||||||
var _is_held: bool = false
|
var _is_held: bool = false
|
||||||
|
var _current_anim: String = "idle"
|
||||||
|
|
||||||
const _STATE_COLORS: Dictionary = {
|
const _STATE_COLORS: Dictionary = {
|
||||||
CharacterData.State.HEALTHY: Color(0.6, 0.8, 1.0),
|
CharacterData.State.HEALTHY: Color(0.6, 0.8, 1.0),
|
||||||
@@ -30,6 +31,7 @@ func _ready() -> void:
|
|||||||
drag.drag_released.connect(_on_drag_released)
|
drag.drag_released.connect(_on_drag_released)
|
||||||
if data != null:
|
if data != null:
|
||||||
_update_visual_state()
|
_update_visual_state()
|
||||||
|
_refresh_outfit_layers()
|
||||||
|
|
||||||
|
|
||||||
func set_state(new_state: CharacterData.State) -> void:
|
func set_state(new_state: CharacterData.State) -> void:
|
||||||
@@ -40,6 +42,94 @@ func set_state(new_state: CharacterData.State) -> void:
|
|||||||
state_changed.emit(new_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
|
||||||
|
slot.remove_child(item)
|
||||||
|
var scene_parent: Node = get_parent()
|
||||||
|
if scene_parent != null:
|
||||||
|
scene_parent.add_child(item)
|
||||||
|
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:
|
func _update_visual_state() -> void:
|
||||||
if data == null:
|
if data == null:
|
||||||
return
|
return
|
||||||
@@ -58,12 +148,23 @@ func _update_visual_state() -> void:
|
|||||||
|
|
||||||
func _on_drag_picked_up(_pos: Vector2) -> void:
|
func _on_drag_picked_up(_pos: Vector2) -> void:
|
||||||
_is_held = true
|
_is_held = true
|
||||||
|
set_animation_state("held")
|
||||||
character_picked_up.emit(self)
|
character_picked_up.emit(self)
|
||||||
|
|
||||||
|
|
||||||
func _on_drag_released(pos: Vector2) -> void:
|
func _on_drag_released(pos: Vector2) -> void:
|
||||||
_is_held = false
|
_is_held = false
|
||||||
|
set_animation_state("idle")
|
||||||
if data == null or data.id.is_empty():
|
if data == null or data.id.is_empty():
|
||||||
return
|
return
|
||||||
GameState.set_character_position(character_id, global_position)
|
GameState.set_character_position(character_id, global_position)
|
||||||
character_placed.emit(self, 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()
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ enum Species { BUNNY, KITTEN }
|
|||||||
@export var state: State = State.HEALTHY
|
@export var state: State = State.HEALTHY
|
||||||
@export var current_floor: int = 0
|
@export var current_floor: int = 0
|
||||||
@export var position: Vector2 = Vector2.ZERO
|
@export var position: Vector2 = Vector2.ZERO
|
||||||
|
@export var outfit: Array[String] = ["", "", ""]
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
## SnapReceiver — scans for nearby SnapPoints when the parent Character is released.
|
||||||
|
## Attach as child of Character. Connects automatically to DragDropComponent signals.
|
||||||
|
class_name SnapReceiver extends Node
|
||||||
|
|
||||||
|
const SCAN_RADIUS: float = 80.0
|
||||||
|
|
||||||
|
var _current_snap: SnapPoint = null
|
||||||
|
var _character: Character
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_character = get_parent() as Character
|
||||||
|
var drag: DragDropComponent = _character.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)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_drag_picked_up(_pos: Vector2) -> void:
|
||||||
|
if _current_snap != null:
|
||||||
|
_current_snap.unsnap()
|
||||||
|
_current_snap = null
|
||||||
|
_character.set_animation_state("held")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_drag_released(_pos: Vector2) -> void:
|
||||||
|
var nearest: SnapPoint = _find_nearest_accepting_snap()
|
||||||
|
if nearest != null:
|
||||||
|
_current_snap = nearest
|
||||||
|
nearest.snap(_character)
|
||||||
|
_character.global_position = nearest.global_position
|
||||||
|
_character.set_animation_state(nearest.pose)
|
||||||
|
else:
|
||||||
|
_character.set_animation_state("idle")
|
||||||
|
|
||||||
|
|
||||||
|
func _find_nearest_accepting_snap() -> SnapPoint:
|
||||||
|
var best: SnapPoint = null
|
||||||
|
var best_dist: float = SCAN_RADIUS
|
||||||
|
for node: Node in get_tree().get_nodes_in_group("snap_points"):
|
||||||
|
var snap_point: SnapPoint = node as SnapPoint
|
||||||
|
if snap_point == null:
|
||||||
|
continue
|
||||||
|
if not snap_point.accepts(_character):
|
||||||
|
continue
|
||||||
|
var dist: float = _character.global_position.distance_to(snap_point.global_position)
|
||||||
|
if dist < best_dist:
|
||||||
|
best_dist = dist
|
||||||
|
best = snap_point
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
func get_current_snap() -> SnapPoint:
|
||||||
|
return _current_snap
|
||||||
|
|
||||||
|
|
||||||
|
func force_unsnap() -> void:
|
||||||
|
if _current_snap == null:
|
||||||
|
return
|
||||||
|
_current_snap.unsnap()
|
||||||
|
_current_snap = null
|
||||||
|
_character.set_animation_state("idle")
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
## SnapPoint — attachment position on furniture where a Character can snap into a pose.
|
||||||
|
## Add to any furniture node. The node auto-registers in the "snap_points" group on _ready.
|
||||||
|
class_name SnapPoint extends Node2D
|
||||||
|
|
||||||
|
signal character_snapped(character: Character)
|
||||||
|
signal character_unsnapped(character: Character)
|
||||||
|
|
||||||
|
@export var pose: String = "sitting"
|
||||||
|
@export var baby_only: bool = false
|
||||||
|
|
||||||
|
var occupant: Character = null
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
add_to_group("snap_points")
|
||||||
|
|
||||||
|
|
||||||
|
func is_free() -> bool:
|
||||||
|
return occupant == null
|
||||||
|
|
||||||
|
|
||||||
|
func accepts(character: Character) -> bool:
|
||||||
|
if not is_free():
|
||||||
|
return false
|
||||||
|
if not baby_only:
|
||||||
|
return true
|
||||||
|
if character.data == null:
|
||||||
|
return false
|
||||||
|
return character.data.state == CharacterData.State.BABY
|
||||||
|
|
||||||
|
|
||||||
|
func snap(character: Character) -> void:
|
||||||
|
occupant = character
|
||||||
|
character_snapped.emit(character)
|
||||||
|
|
||||||
|
|
||||||
|
func unsnap() -> void:
|
||||||
|
if occupant == null:
|
||||||
|
return
|
||||||
|
var prev: Character = occupant
|
||||||
|
occupant = null
|
||||||
|
character_unsnapped.emit(prev)
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
## Tests for Character System v2 — animation state, outfit layers, hand slots.
|
||||||
|
extends GutTest
|
||||||
|
|
||||||
|
var _char: Character
|
||||||
|
|
||||||
|
|
||||||
|
func before_each() -> void:
|
||||||
|
_char = preload("res://scenes/characters/Character.tscn").instantiate() as Character
|
||||||
|
add_child_autofree(_char)
|
||||||
|
var cd: CharacterData = CharacterData.new()
|
||||||
|
_char.data = cd
|
||||||
|
|
||||||
|
|
||||||
|
func test_animated_sprite_node_exists() -> void:
|
||||||
|
assert_not_null(_char.get_node_or_null("AnimatedSprite2D"))
|
||||||
|
|
||||||
|
|
||||||
|
func test_outfit_layer_1_node_exists() -> void:
|
||||||
|
assert_not_null(_char.get_node_or_null("OutfitLayer1"))
|
||||||
|
|
||||||
|
|
||||||
|
func test_outfit_layer_2_node_exists() -> void:
|
||||||
|
assert_not_null(_char.get_node_or_null("OutfitLayer2"))
|
||||||
|
|
||||||
|
|
||||||
|
func test_outfit_layer_3_node_exists() -> void:
|
||||||
|
assert_not_null(_char.get_node_or_null("OutfitLayer3"))
|
||||||
|
|
||||||
|
|
||||||
|
func test_hand_left_node_exists() -> void:
|
||||||
|
assert_not_null(_char.get_node_or_null("HandLeft"))
|
||||||
|
|
||||||
|
|
||||||
|
func test_hand_right_node_exists() -> void:
|
||||||
|
assert_not_null(_char.get_node_or_null("HandRight"))
|
||||||
|
|
||||||
|
|
||||||
|
func test_snap_receiver_node_exists() -> void:
|
||||||
|
assert_not_null(_char.get_node_or_null("SnapReceiver"))
|
||||||
|
|
||||||
|
|
||||||
|
func test_character_data_outfit_has_three_empty_slots() -> void:
|
||||||
|
assert_eq(_char.data.outfit.size(), 3)
|
||||||
|
assert_eq(_char.data.outfit[0], "")
|
||||||
|
assert_eq(_char.data.outfit[1], "")
|
||||||
|
assert_eq(_char.data.outfit[2], "")
|
||||||
|
|
||||||
|
|
||||||
|
func test_default_animation_state_is_idle() -> void:
|
||||||
|
assert_eq(_char.get_animation_state(), "idle")
|
||||||
|
|
||||||
|
|
||||||
|
func test_set_animation_state_sitting() -> void:
|
||||||
|
_char.set_animation_state("sitting")
|
||||||
|
assert_eq(_char.get_animation_state(), "sitting")
|
||||||
|
|
||||||
|
|
||||||
|
func test_set_animation_state_lying() -> void:
|
||||||
|
_char.set_animation_state("lying")
|
||||||
|
assert_eq(_char.get_animation_state(), "lying")
|
||||||
|
|
||||||
|
|
||||||
|
func test_set_animation_state_held() -> void:
|
||||||
|
_char.set_animation_state("held")
|
||||||
|
assert_eq(_char.get_animation_state(), "held")
|
||||||
|
|
||||||
|
|
||||||
|
func test_set_animation_state_happy() -> void:
|
||||||
|
_char.set_animation_state("happy")
|
||||||
|
assert_eq(_char.get_animation_state(), "happy")
|
||||||
|
|
||||||
|
|
||||||
|
func test_set_animation_state_sleeping() -> void:
|
||||||
|
_char.set_animation_state("sleeping")
|
||||||
|
assert_eq(_char.get_animation_state(), "sleeping")
|
||||||
|
|
||||||
|
|
||||||
|
func test_get_outfit_returns_empty_for_all_layers_initially() -> void:
|
||||||
|
assert_eq(_char.get_outfit(1), "")
|
||||||
|
assert_eq(_char.get_outfit(2), "")
|
||||||
|
assert_eq(_char.get_outfit(3), "")
|
||||||
|
|
||||||
|
|
||||||
|
func test_set_outfit_stores_item_id() -> void:
|
||||||
|
_char.set_outfit(1, "white_coat", null)
|
||||||
|
assert_eq(_char.get_outfit(1), "white_coat")
|
||||||
|
|
||||||
|
|
||||||
|
func test_set_outfit_does_not_affect_other_layers() -> void:
|
||||||
|
_char.set_outfit(1, "white_coat", null)
|
||||||
|
assert_eq(_char.get_outfit(2), "")
|
||||||
|
assert_eq(_char.get_outfit(3), "")
|
||||||
|
|
||||||
|
|
||||||
|
func test_clear_outfit_returns_item_id() -> void:
|
||||||
|
_char.set_outfit(2, "cast_arm", null)
|
||||||
|
var returned: String = _char.clear_outfit(2)
|
||||||
|
assert_eq(returned, "cast_arm")
|
||||||
|
|
||||||
|
|
||||||
|
func test_clear_outfit_empties_layer() -> void:
|
||||||
|
_char.set_outfit(2, "cast_arm", null)
|
||||||
|
_char.clear_outfit(2)
|
||||||
|
assert_eq(_char.get_outfit(2), "")
|
||||||
|
|
||||||
|
|
||||||
|
func test_set_outfit_invalid_layer_zero_is_noop() -> void:
|
||||||
|
_char.set_outfit(0, "white_coat", null)
|
||||||
|
assert_eq(_char.get_outfit(1), "")
|
||||||
|
|
||||||
|
|
||||||
|
func test_set_outfit_invalid_layer_four_is_noop() -> void:
|
||||||
|
_char.set_outfit(4, "white_coat", null)
|
||||||
|
assert_eq(_char.get_outfit(3), "")
|
||||||
|
|
||||||
|
|
||||||
|
func test_both_hands_free_initially() -> void:
|
||||||
|
assert_true(_char.is_hand_free("left"))
|
||||||
|
assert_true(_char.is_hand_free("right"))
|
||||||
|
|
||||||
|
|
||||||
|
func test_attach_item_to_left_hand() -> void:
|
||||||
|
var item: Node2D = Node2D.new()
|
||||||
|
add_child_autofree(item)
|
||||||
|
_char.attach_item("left", item)
|
||||||
|
assert_false(_char.is_hand_free("left"))
|
||||||
|
|
||||||
|
|
||||||
|
func test_get_held_item_returns_attached_item() -> void:
|
||||||
|
var item: Node2D = Node2D.new()
|
||||||
|
add_child_autofree(item)
|
||||||
|
_char.attach_item("right", item)
|
||||||
|
assert_eq(_char.get_held_item("right"), item)
|
||||||
|
|
||||||
|
|
||||||
|
func test_attach_to_occupied_hand_returns_false() -> void:
|
||||||
|
var item1: Node2D = Node2D.new()
|
||||||
|
var item2: Node2D = Node2D.new()
|
||||||
|
add_child_autofree(item1)
|
||||||
|
add_child_autofree(item2)
|
||||||
|
_char.attach_item("left", item1)
|
||||||
|
var result: bool = _char.attach_item("left", item2)
|
||||||
|
assert_false(result)
|
||||||
|
|
||||||
|
|
||||||
|
func test_attach_returns_true_on_success() -> void:
|
||||||
|
var item: Node2D = Node2D.new()
|
||||||
|
add_child_autofree(item)
|
||||||
|
var result: bool = _char.attach_item("right", item)
|
||||||
|
assert_true(result)
|
||||||
|
|
||||||
|
|
||||||
|
func test_detach_item_frees_hand() -> void:
|
||||||
|
var item: Node2D = Node2D.new()
|
||||||
|
add_child_autofree(item)
|
||||||
|
_char.attach_item("left", item)
|
||||||
|
_char.detach_item("left")
|
||||||
|
assert_true(_char.is_hand_free("left"))
|
||||||
|
|
||||||
|
|
||||||
|
func test_detach_returns_item() -> void:
|
||||||
|
var item: Node2D = Node2D.new()
|
||||||
|
add_child_autofree(item)
|
||||||
|
_char.attach_item("right", item)
|
||||||
|
var returned: Node2D = _char.detach_item("right")
|
||||||
|
assert_eq(returned, item)
|
||||||
|
|
||||||
|
|
||||||
|
func test_detach_from_empty_hand_returns_null() -> void:
|
||||||
|
var returned: Node2D = _char.detach_item("left")
|
||||||
|
assert_null(returned)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
## Tests for SnapPoint — attachment node on furniture.
|
||||||
|
extends GutTest
|
||||||
|
|
||||||
|
const SnapPointScript: GDScript = preload("res://scripts/objects/snap_point.gd")
|
||||||
|
|
||||||
|
var _snap: SnapPoint
|
||||||
|
var _char: Character
|
||||||
|
|
||||||
|
|
||||||
|
func before_each() -> void:
|
||||||
|
_snap = SnapPointScript.new()
|
||||||
|
_snap.pose = "sitting"
|
||||||
|
add_child_autofree(_snap)
|
||||||
|
_char = preload("res://scenes/characters/Character.tscn").instantiate() as Character
|
||||||
|
add_child_autofree(_char)
|
||||||
|
|
||||||
|
|
||||||
|
func test_is_free_when_no_occupant() -> void:
|
||||||
|
assert_true(_snap.is_free())
|
||||||
|
|
||||||
|
|
||||||
|
func test_is_not_free_when_occupied() -> void:
|
||||||
|
_snap.snap(_char)
|
||||||
|
assert_false(_snap.is_free())
|
||||||
|
|
||||||
|
|
||||||
|
func test_snap_sets_occupant() -> void:
|
||||||
|
_snap.snap(_char)
|
||||||
|
assert_eq(_snap.occupant, _char)
|
||||||
|
|
||||||
|
|
||||||
|
func test_unsnap_clears_occupant() -> void:
|
||||||
|
_snap.snap(_char)
|
||||||
|
_snap.unsnap()
|
||||||
|
assert_null(_snap.occupant)
|
||||||
|
|
||||||
|
|
||||||
|
func test_accepts_any_character_when_baby_only_false() -> void:
|
||||||
|
assert_true(_snap.accepts(_char))
|
||||||
|
|
||||||
|
|
||||||
|
func test_does_not_accept_when_occupied() -> void:
|
||||||
|
_snap.snap(_char)
|
||||||
|
var other: Character = preload("res://scenes/characters/Character.tscn").instantiate() as Character
|
||||||
|
add_child_autofree(other)
|
||||||
|
assert_false(_snap.accepts(other))
|
||||||
|
|
||||||
|
|
||||||
|
func test_baby_only_rejects_healthy_character() -> void:
|
||||||
|
_snap.baby_only = true
|
||||||
|
var cd: CharacterData = CharacterData.new()
|
||||||
|
cd.state = CharacterData.State.HEALTHY
|
||||||
|
_char.data = cd
|
||||||
|
assert_false(_snap.accepts(_char))
|
||||||
|
|
||||||
|
|
||||||
|
func test_baby_only_accepts_baby_state_character() -> void:
|
||||||
|
_snap.baby_only = true
|
||||||
|
var cd: CharacterData = CharacterData.new()
|
||||||
|
cd.state = CharacterData.State.BABY
|
||||||
|
_char.data = cd
|
||||||
|
assert_true(_snap.accepts(_char))
|
||||||
|
|
||||||
|
|
||||||
|
func test_snap_emits_character_snapped() -> void:
|
||||||
|
watch_signals(_snap)
|
||||||
|
_snap.snap(_char)
|
||||||
|
assert_signal_emitted(_snap, "character_snapped")
|
||||||
|
|
||||||
|
|
||||||
|
func test_unsnap_emits_character_unsnapped() -> void:
|
||||||
|
_snap.snap(_char)
|
||||||
|
watch_signals(_snap)
|
||||||
|
_snap.unsnap()
|
||||||
|
assert_signal_emitted(_snap, "character_unsnapped")
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
## Tests for SnapReceiver — snap detection when Character is released near a SnapPoint.
|
||||||
|
extends GutTest
|
||||||
|
|
||||||
|
var _char: Character
|
||||||
|
var _snap: SnapPoint
|
||||||
|
|
||||||
|
|
||||||
|
func before_each() -> void:
|
||||||
|
_char = preload("res://scenes/characters/Character.tscn").instantiate() as Character
|
||||||
|
add_child_autofree(_char)
|
||||||
|
_snap = SnapPoint.new()
|
||||||
|
_snap.pose = "sitting"
|
||||||
|
add_child_autofree(_snap)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_receiver() -> SnapReceiver:
|
||||||
|
return _char.get_node("SnapReceiver") as SnapReceiver
|
||||||
|
|
||||||
|
|
||||||
|
func test_snap_receiver_exists_on_character() -> void:
|
||||||
|
assert_not_null(_get_receiver())
|
||||||
|
|
||||||
|
|
||||||
|
func test_no_current_snap_initially() -> void:
|
||||||
|
assert_null(_get_receiver().get_current_snap())
|
||||||
|
|
||||||
|
|
||||||
|
func test_snap_detected_when_character_released_within_radius() -> void:
|
||||||
|
_snap.global_position = Vector2(0.0, 0.0)
|
||||||
|
_char.global_position = Vector2(50.0, 0.0)
|
||||||
|
_get_receiver()._on_drag_released(Vector2(50.0, 0.0))
|
||||||
|
assert_eq(_get_receiver().get_current_snap(), _snap)
|
||||||
|
|
||||||
|
|
||||||
|
func test_no_snap_when_released_outside_radius() -> void:
|
||||||
|
_snap.global_position = Vector2(0.0, 0.0)
|
||||||
|
_char.global_position = Vector2(200.0, 0.0)
|
||||||
|
_get_receiver()._on_drag_released(Vector2(200.0, 0.0))
|
||||||
|
assert_null(_get_receiver().get_current_snap())
|
||||||
|
|
||||||
|
|
||||||
|
func test_character_position_set_to_snap_point_on_snap() -> void:
|
||||||
|
_snap.global_position = Vector2(100.0, 100.0)
|
||||||
|
_char.global_position = Vector2(120.0, 100.0)
|
||||||
|
_get_receiver()._on_drag_released(Vector2(120.0, 100.0))
|
||||||
|
assert_eq(_char.global_position, Vector2(100.0, 100.0))
|
||||||
|
|
||||||
|
|
||||||
|
func test_character_animation_set_to_snap_pose_on_snap() -> void:
|
||||||
|
_snap.global_position = Vector2(0.0, 0.0)
|
||||||
|
_snap.pose = "lying"
|
||||||
|
_char.global_position = Vector2(50.0, 0.0)
|
||||||
|
_get_receiver()._on_drag_released(Vector2(50.0, 0.0))
|
||||||
|
assert_eq(_char.get_animation_state(), "lying")
|
||||||
|
|
||||||
|
|
||||||
|
func test_pickup_clears_current_snap() -> void:
|
||||||
|
_snap.global_position = Vector2(0.0, 0.0)
|
||||||
|
_char.global_position = Vector2(50.0, 0.0)
|
||||||
|
_get_receiver()._on_drag_released(Vector2(50.0, 0.0))
|
||||||
|
_get_receiver()._on_drag_picked_up(Vector2(50.0, 0.0))
|
||||||
|
assert_null(_get_receiver().get_current_snap())
|
||||||
|
|
||||||
|
|
||||||
|
func test_pickup_frees_snap_point() -> void:
|
||||||
|
_snap.global_position = Vector2(0.0, 0.0)
|
||||||
|
_char.global_position = Vector2(50.0, 0.0)
|
||||||
|
_get_receiver()._on_drag_released(Vector2(50.0, 0.0))
|
||||||
|
_get_receiver()._on_drag_picked_up(Vector2(50.0, 0.0))
|
||||||
|
assert_true(_snap.is_free())
|
||||||
|
|
||||||
|
|
||||||
|
func test_second_character_cannot_snap_to_occupied_point() -> void:
|
||||||
|
_snap.global_position = Vector2(0.0, 0.0)
|
||||||
|
_char.global_position = Vector2(50.0, 0.0)
|
||||||
|
_get_receiver()._on_drag_released(Vector2(50.0, 0.0))
|
||||||
|
var char2: Character = preload("res://scenes/characters/Character.tscn").instantiate() as Character
|
||||||
|
add_child_autofree(char2)
|
||||||
|
char2.global_position = Vector2(60.0, 0.0)
|
||||||
|
var recv2: SnapReceiver = char2.get_node("SnapReceiver") as SnapReceiver
|
||||||
|
recv2._on_drag_released(Vector2(60.0, 0.0))
|
||||||
|
assert_null(recv2.get_current_snap())
|
||||||
Reference in New Issue
Block a user