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:
@@ -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()
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
class_name PropUseSpotData
|
||||
extends RefCounted
|
||||
## Pure validation and conversion helpers for placed-prop Use spots.
|
||||
|
||||
static func validate_saved(raw_spots: Variant, raw_next: Variant) -> String:
|
||||
if not raw_spots is Array:
|
||||
return "Prop Use spots must be an array."
|
||||
if not _is_integer_number(raw_next):
|
||||
return "Prop Use spot counter must be an integer."
|
||||
var next_value: float = float(raw_next)
|
||||
if not is_finite(next_value) or next_value < 1.0:
|
||||
return "Prop Use spot counter must be a positive integer."
|
||||
var ids: Dictionary = {}
|
||||
var numbers: Dictionary = {}
|
||||
var highest: int = 0
|
||||
for raw: Variant in raw_spots as Array:
|
||||
if not raw is Dictionary:
|
||||
return "Prop Use spot entries must be objects."
|
||||
var spot: Dictionary = raw
|
||||
var raw_id: Variant = spot.get("id", null)
|
||||
var raw_number: Variant = spot.get("number", null)
|
||||
var raw_name: Variant = spot.get("name", null)
|
||||
if not raw_id is String or not _is_integer_number(raw_number) or not raw_name is String:
|
||||
return "Prop Use spot fields are invalid."
|
||||
var numeric_number: float = float(raw_number)
|
||||
if not is_finite(numeric_number) or numeric_number < 1.0:
|
||||
return "Prop Use spot numbers must be positive integers."
|
||||
var number: int = int(numeric_number)
|
||||
var spot_id: String = String(raw_id)
|
||||
if spot_id != "spot_%d" % number or ids.has(spot_id) or numbers.has(number):
|
||||
return "Prop Use spot IDs and numbers must be unique and canonical."
|
||||
if not _is_finite_point(spot.get("position", null)):
|
||||
return "Prop Use spot positions must be finite points."
|
||||
ids[spot_id] = true
|
||||
numbers[number] = true
|
||||
highest = maxi(highest, number)
|
||||
if int(next_value) <= highest:
|
||||
return "Prop Use spot counter must exceed every existing spot number."
|
||||
return ""
|
||||
|
||||
|
||||
static func decode(raw_spots: Array) -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
for raw: Variant in raw_spots:
|
||||
if not raw is Dictionary:
|
||||
continue
|
||||
var spot: Dictionary = raw
|
||||
var number: int = int(spot.get("number", 0))
|
||||
var name: String = String(spot.get("name", "")).strip_edges()
|
||||
result.append({
|
||||
"id": String(spot.get("id", "spot_%d" % number)),
|
||||
"number": number,
|
||||
"name": name if not name.is_empty() else fallback_name(number),
|
||||
"position": _point_from_data(spot.get("position", null)),
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
static func encode(spots: Array[Dictionary]) -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
for spot: Dictionary in spots:
|
||||
var number: int = int(spot.get("number", 0))
|
||||
var position: Variant = spot.get("position", Vector2.ZERO)
|
||||
if not position is Vector2:
|
||||
continue
|
||||
var name: String = String(spot.get("name", "")).strip_edges()
|
||||
result.append({
|
||||
"id": String(spot.get("id", "spot_%d" % number)),
|
||||
"number": number,
|
||||
"name": name if not name.is_empty() else fallback_name(number),
|
||||
"position": {"x": position.x, "y": position.y},
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
static func fallback_name(number: int) -> String:
|
||||
return "Spot %d" % maxi(1, number)
|
||||
|
||||
|
||||
static func find(spots: Array[Dictionary], spot_id: String) -> Dictionary:
|
||||
for spot: Dictionary in spots:
|
||||
if String(spot.get("id", "")) == spot_id:
|
||||
return spot.duplicate(true)
|
||||
return {}
|
||||
|
||||
|
||||
static func _is_finite_point(value: Variant) -> bool:
|
||||
if not value is Dictionary:
|
||||
return false
|
||||
var point: Dictionary = value
|
||||
var x: Variant = point.get("x", null)
|
||||
var y: Variant = point.get("y", null)
|
||||
return (x is int or x is float) and (y is int or y is float) and is_finite(float(x)) and is_finite(float(y))
|
||||
|
||||
|
||||
static func _is_integer_number(value: Variant) -> bool:
|
||||
if value is int:
|
||||
return true
|
||||
if not value is float:
|
||||
return false
|
||||
var number: float = value
|
||||
return is_finite(number) and absf(number) <= 9007199254740991.0 and floorf(number) == number
|
||||
|
||||
|
||||
static func _point_from_data(value: Variant) -> Vector2:
|
||||
if not _is_finite_point(value):
|
||||
return Vector2.ZERO
|
||||
var point: Dictionary = value
|
||||
return Vector2(float(point["x"]), float(point["y"]))
|
||||
@@ -0,0 +1 @@
|
||||
uid://vdr0sanaei05
|
||||
Reference in New Issue
Block a user