Add Use Spot Overlay and Target Capture Bar functionality

- Introduced `StageUseTargetCaptureBar` class for managing user interactions with props and their use spots.
- Implemented UI elements including buttons for changing props and canceling actions.
- Added `PropUseSpotData` class for validation and serialization of prop use spots.
- Created tests for validating use spot data migration, serialization, and rule matching.
- Ensured proper handling of edge cases in use spot data, including unique IDs and finite positions.
This commit is contained in:
2026-10-13 11:23:44 -04:00
parent 235c6aa6a6
commit d8b1b1164b
42 changed files with 3847 additions and 50 deletions
+100
View File
@@ -13,6 +13,7 @@ extends RigidBody2D
const PropVisuals := preload("res://scripts/props/prop_visuals.gd")
const PropAnchorDataScript := preload("res://scripts/props/prop_anchor_data.gd")
const PROP_STAGE_ROLES := preload("res://scripts/props/prop_stage_roles.gd")
const PROP_USE_SPOT_DATA := preload("res://scripts/props/prop_use_spot_data.gd")
const STAGE_PHYSICS := preload("res://scripts/stage/stage_physics_policy.gd")
# ---------------------------------------------------------------------------
@@ -33,6 +34,8 @@ var stage_object_id: String = ""
var interaction_anchors: Dictionary = {}
var interaction_reserved_by: int = 0
var interaction_owner_id: int = 0
var use_spots: Array[Dictionary] = []
var next_use_spot_number: int = 1
var stage_roles: Dictionary = {}
var runtime_role: RuntimeRole = RuntimeRole.EDIT
@@ -400,6 +403,103 @@ func get_visual_local_bounds() -> Rect2:
return _visual_local_bounds
func get_use_spots() -> Array[Dictionary]:
var result: Array[Dictionary] = []
for spot: Dictionary in use_spots:
result.append(spot.duplicate(true))
return result
func get_use_spot(spot_id: String) -> Dictionary:
return PROP_USE_SPOT_DATA.find(use_spots, spot_id)
func get_use_spot_local_bounds() -> Rect2:
if _custom_shapes:
return _visual_local_bounds
if shape_type == ShapeType.CIRCLE:
return Rect2(Vector2(-radius, -radius), Vector2(radius * 2.0, radius * 2.0))
if polygon_points.is_empty():
return Rect2()
var minimum: Vector2 = polygon_points[0]
var maximum: Vector2 = polygon_points[0]
for point: Vector2 in polygon_points:
minimum = minimum.min(point)
maximum = maximum.max(point)
return Rect2(minimum, maximum - minimum)
func add_use_spot(local_position: Vector2) -> String:
if not local_position.is_finite() or not get_use_spot_local_bounds().has_point(local_position):
return ""
var number: int = maxi(1, next_use_spot_number)
var spot_id: String = "spot_%d" % number
if not get_use_spot(spot_id).is_empty():
return ""
use_spots.append({"id": spot_id, "number": number, "name": PROP_USE_SPOT_DATA.fallback_name(number), "position": local_position})
next_use_spot_number = number + 1
return spot_id
func rename_use_spot(spot_id: String, spot_name: String) -> bool:
for index: int in use_spots.size():
if String(use_spots[index].get("id", "")) != spot_id:
continue
var number: int = int(use_spots[index].get("number", 1))
var normalized_name: String = spot_name.strip_edges()
use_spots[index]["name"] = normalized_name if not normalized_name.is_empty() else PROP_USE_SPOT_DATA.fallback_name(number)
return true
return false
func move_use_spot(spot_id: String, local_position: Vector2) -> bool:
if not local_position.is_finite() or not get_use_spot_local_bounds().has_point(local_position):
return false
for index: int in use_spots.size():
if String(use_spots[index].get("id", "")) == spot_id:
use_spots[index]["position"] = local_position
return true
return false
func erase_use_spot(spot_id: String) -> bool:
for index: int in use_spots.size():
if String(use_spots[index].get("id", "")) == spot_id:
use_spots.remove_at(index)
return true
return false
func use_spot_world_position(spot_id: String) -> Vector2:
var spot: Dictionary = get_use_spot(spot_id)
if spot.is_empty():
return Vector2.INF
var local_position: Vector2 = spot.get("position", Vector2.ZERO)
var baked_scale: Vector2 = _play_instance_scale if _play_scale_active else Vector2.ONE
return global_transform * (local_position * baked_scale)
func use_spot_local_from_world(world_position: Vector2) -> Vector2:
if not world_position.is_finite() or not _transform_is_invertible(global_transform):
return Vector2.INF
var local_position: Vector2 = to_local(world_position)
return local_position if get_use_spot_local_bounds().has_point(local_position) else Vector2.INF
func set_use_spots(spots: Array[Dictionary], next_number: int) -> void:
use_spots.clear()
for spot: Dictionary in spots:
use_spots.append(spot.duplicate(true))
var highest: int = 0
for spot: Dictionary in use_spots:
highest = maxi(highest, int(spot.get("number", 0)))
next_use_spot_number = maxi(highest + 1, next_number)
func _transform_is_invertible(value: Transform2D) -> bool:
return value.x.is_finite() and value.y.is_finite() and value.origin.is_finite() and is_finite(value.determinant()) and absf(value.determinant()) > 0.000001
func get_interaction_scale() -> Vector2:
return _play_instance_scale if _play_scale_active else scale.abs()