|
|
|
@@ -0,0 +1,242 @@
|
|
|
|
|
class_name StageRuleEngine
|
|
|
|
|
extends RefCounted
|
|
|
|
|
## Runtime event matching, area-entry edges, and collision contact bookkeeping.
|
|
|
|
|
|
|
|
|
|
const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd")
|
|
|
|
|
const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd")
|
|
|
|
|
const STICKMAN_RIG_SCRIPT := preload("res://scripts/characters/stickman/stickman_rig.gd")
|
|
|
|
|
|
|
|
|
|
const WAYPOINT_MATCH_EPSILON: float = 24.0
|
|
|
|
|
|
|
|
|
|
var _area_overlap: Dictionary = {}
|
|
|
|
|
var _area_previous_bounds: Dictionary = {}
|
|
|
|
|
var _collision_pairs: Dictionary = {}
|
|
|
|
|
var _suppressed_contact_pairs: Dictionary = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func handle_event(event: Dictionary, rules: Array[Dictionary]) -> Array[Dictionary]:
|
|
|
|
|
var matched_rules: Array[Dictionary] = []
|
|
|
|
|
for rule: Dictionary in rules:
|
|
|
|
|
if matches_rule(rule, event):
|
|
|
|
|
matched_rules.append(rule)
|
|
|
|
|
return matched_rules
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func matches_rule(rule: Dictionary, event: Dictionary) -> bool:
|
|
|
|
|
var trigger: Dictionary = rule.get("trigger", {})
|
|
|
|
|
var rule_type: String = String(trigger.get("type", ""))
|
|
|
|
|
if rule_type != String(event.get("type", "")):
|
|
|
|
|
return false
|
|
|
|
|
var source: Node2D = event.get("source") as Node2D
|
|
|
|
|
var target: Node2D = event.get("target") as Node2D
|
|
|
|
|
var source_id: int = int(trigger.get("source", -1))
|
|
|
|
|
var target_id: int = int(trigger.get("target", -1))
|
|
|
|
|
match rule_type:
|
|
|
|
|
"arrived_at_waypoint":
|
|
|
|
|
if not ids_match(source_id, source):
|
|
|
|
|
return false
|
|
|
|
|
var params: Dictionary = trigger.get("params", {})
|
|
|
|
|
var waypoint: Vector2 = params.get("waypoint_pos", Vector2.INF)
|
|
|
|
|
var position: Vector2 = event.get("position", Vector2.INF)
|
|
|
|
|
return position.distance_to(waypoint) <= WAYPOINT_MATCH_EPSILON
|
|
|
|
|
"action_finished":
|
|
|
|
|
if not ids_match(source_id, source):
|
|
|
|
|
return false
|
|
|
|
|
var params: Dictionary = trigger.get("params", {})
|
|
|
|
|
var wanted_type: String = String(params.get("action_type", ""))
|
|
|
|
|
var action: Dictionary = event.get("action", {})
|
|
|
|
|
return wanted_type.is_empty() or ACTION_REGISTRY.canonical_type(wanted_type) == ACTION_REGISTRY.canonical_type(String(action.get("type", "")))
|
|
|
|
|
"speech_finished":
|
|
|
|
|
return ids_match(source_id, source)
|
|
|
|
|
"entered_area", "collided", "prop_picked_up", "prop_dropped", "prop_thrown":
|
|
|
|
|
return ids_match(source_id, source) and ids_match(target_id, target)
|
|
|
|
|
_:
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func ids_match(expected_id: int, node: Node2D) -> bool:
|
|
|
|
|
return expected_id == -1 or (node != null and is_instance_valid(node) and node.get_instance_id() == expected_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func physics_tick(world_children: Array[Node2D]) -> Array[Dictionary]:
|
|
|
|
|
var areas: Array[TriggerArea] = []
|
|
|
|
|
for node: Node2D in world_children:
|
|
|
|
|
if node is TriggerArea:
|
|
|
|
|
areas.append(node as TriggerArea)
|
|
|
|
|
if areas.is_empty():
|
|
|
|
|
return []
|
|
|
|
|
var movables: Array[Node2D] = []
|
|
|
|
|
for node: Node2D in world_children:
|
|
|
|
|
if node is STICKMAN_RIG_SCRIPT:
|
|
|
|
|
var rig: StickmanRig = node as StickmanRig
|
|
|
|
|
if rig.state == StickmanRig.RigState.ANIMATED:
|
|
|
|
|
movables.append(rig)
|
|
|
|
|
elif node is PropBlock and not (node as PropBlock).freeze:
|
|
|
|
|
movables.append(node)
|
|
|
|
|
var swept_bounds: Dictionary = {}
|
|
|
|
|
var live_ids: Dictionary = {}
|
|
|
|
|
for node: Node2D in movables:
|
|
|
|
|
var node_id: int = node.get_instance_id()
|
|
|
|
|
var current_bounds: Rect2 = STAGE_SELECTION.get_world_aabb(node).abs()
|
|
|
|
|
var swept: Rect2 = current_bounds
|
|
|
|
|
if _area_previous_bounds.has(node_id):
|
|
|
|
|
swept = swept.merge((_area_previous_bounds[node_id] as Rect2).abs())
|
|
|
|
|
swept_bounds[node_id] = swept
|
|
|
|
|
_area_previous_bounds[node_id] = current_bounds
|
|
|
|
|
live_ids[node_id] = true
|
|
|
|
|
for old_id: int in _area_previous_bounds.keys().duplicate():
|
|
|
|
|
if not live_ids.has(old_id):
|
|
|
|
|
_area_previous_bounds.erase(old_id)
|
|
|
|
|
var events: Array[Dictionary] = []
|
|
|
|
|
for area: TriggerArea in areas:
|
|
|
|
|
var area_rect: Rect2 = (area.global_transform * area.get_area_rect()).abs()
|
|
|
|
|
for node: Node2D in movables:
|
|
|
|
|
var key: String = "%d:%d" % [area.get_instance_id(), node.get_instance_id()]
|
|
|
|
|
var swept: Rect2 = swept_bounds.get(node.get_instance_id(), Rect2()) as Rect2
|
|
|
|
|
var inside: bool = swept.intersects(area_rect, true)
|
|
|
|
|
var was_inside: bool = bool(_area_overlap.get(key, false))
|
|
|
|
|
if inside and not was_inside:
|
|
|
|
|
_area_overlap[key] = true
|
|
|
|
|
events.append({"type": "entered_area", "source": node, "target": area, "position": movable_point(node), "action": {}})
|
|
|
|
|
elif not inside and was_inside:
|
|
|
|
|
_area_overlap[key] = false
|
|
|
|
|
return events
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func refresh_event_edge_baseline(world_children: Array[Node2D]) -> void:
|
|
|
|
|
var areas: Array[TriggerArea] = []
|
|
|
|
|
for node: Node2D in world_children:
|
|
|
|
|
if node is TriggerArea:
|
|
|
|
|
areas.append(node as TriggerArea)
|
|
|
|
|
var movables: Array[Node2D] = []
|
|
|
|
|
for node: Node2D in world_children:
|
|
|
|
|
if node is STICKMAN_RIG_SCRIPT and (node as StickmanRig).state == StickmanRig.RigState.ANIMATED:
|
|
|
|
|
movables.append(node)
|
|
|
|
|
elif node is PropBlock:
|
|
|
|
|
movables.append(node)
|
|
|
|
|
_area_overlap.clear()
|
|
|
|
|
_area_previous_bounds.clear()
|
|
|
|
|
for node: Node2D in movables:
|
|
|
|
|
var node_id: int = node.get_instance_id()
|
|
|
|
|
var bounds: Rect2 = STAGE_SELECTION.get_world_aabb(node).abs()
|
|
|
|
|
_area_previous_bounds[node_id] = bounds
|
|
|
|
|
for area: TriggerArea in areas:
|
|
|
|
|
var key: String = "%d:%d" % [area.get_instance_id(), node_id]
|
|
|
|
|
var area_rect: Rect2 = (area.global_transform * area.get_area_rect()).abs()
|
|
|
|
|
_area_overlap[key] = bounds.intersects(area_rect, true)
|
|
|
|
|
_collision_pairs.clear()
|
|
|
|
|
_suppressed_contact_pairs.clear()
|
|
|
|
|
for node: Node2D in world_children:
|
|
|
|
|
if not node is PropBlock:
|
|
|
|
|
continue
|
|
|
|
|
var prop: PropBlock = node as PropBlock
|
|
|
|
|
for other: Node in prop.get_colliding_bodies():
|
|
|
|
|
if other == null or not is_instance_valid(other):
|
|
|
|
|
continue
|
|
|
|
|
_suppressed_contact_pairs[contact_key(prop, other)] = true
|
|
|
|
|
if not other.has_meta("stickman_rig"):
|
|
|
|
|
continue
|
|
|
|
|
var rig: StickmanRig = other.get_meta("stickman_rig") as StickmanRig
|
|
|
|
|
if rig == null or not is_instance_valid(rig):
|
|
|
|
|
continue
|
|
|
|
|
var key: String = collision_key(rig, prop)
|
|
|
|
|
var pair: Dictionary = _collision_pairs.get(key, {"rig_id": rig.get_instance_id(), "prop_id": prop.get_instance_id(), "bodies": {}})
|
|
|
|
|
var body_ids: Dictionary = pair.get("bodies", {})
|
|
|
|
|
body_ids[other.get_instance_id()] = true
|
|
|
|
|
pair["bodies"] = body_ids
|
|
|
|
|
_collision_pairs[key] = pair
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func register_contact(prop: PropBlock, other: Node) -> bool:
|
|
|
|
|
var key: String = contact_key(prop, other)
|
|
|
|
|
if _suppressed_contact_pairs.has(key):
|
|
|
|
|
return false
|
|
|
|
|
_suppressed_contact_pairs[key] = true
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func register_rig_body_contact(rig: StickmanRig, prop: PropBlock, body: Node) -> Dictionary:
|
|
|
|
|
var key: String = collision_key(rig, prop)
|
|
|
|
|
var pair: Dictionary = _collision_pairs.get(key, {
|
|
|
|
|
"rig_id": rig.get_instance_id(),
|
|
|
|
|
"prop_id": prop.get_instance_id(),
|
|
|
|
|
"bodies": {},
|
|
|
|
|
})
|
|
|
|
|
var bodies: Dictionary = pair.get("bodies", {})
|
|
|
|
|
var body_id: int = body.get_instance_id()
|
|
|
|
|
if bodies.has(body_id):
|
|
|
|
|
return {"accepted": false, "first_contact": false}
|
|
|
|
|
var first_contact: bool = bodies.is_empty()
|
|
|
|
|
bodies[body_id] = true
|
|
|
|
|
pair["bodies"] = bodies
|
|
|
|
|
_collision_pairs[key] = pair
|
|
|
|
|
return {"accepted": true, "first_contact": first_contact}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func unregister_contact(prop: PropBlock, other: Node) -> void:
|
|
|
|
|
_suppressed_contact_pairs.erase(contact_key(prop, other))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func unregister_rig_body_contact(rig: StickmanRig, prop: PropBlock, body: Node) -> void:
|
|
|
|
|
var key: String = collision_key(rig, prop)
|
|
|
|
|
var pair: Dictionary = _collision_pairs.get(key, {})
|
|
|
|
|
if pair.is_empty():
|
|
|
|
|
return
|
|
|
|
|
var bodies: Dictionary = pair.get("bodies", {})
|
|
|
|
|
bodies.erase(body.get_instance_id())
|
|
|
|
|
if bodies.is_empty():
|
|
|
|
|
_collision_pairs.erase(key)
|
|
|
|
|
else:
|
|
|
|
|
pair["bodies"] = bodies
|
|
|
|
|
_collision_pairs[key] = pair
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func purge_collision_pairs_for_ids(ids: Array[int]) -> void:
|
|
|
|
|
for key: String in _collision_pairs.keys().duplicate():
|
|
|
|
|
var pair: Dictionary = _collision_pairs[key]
|
|
|
|
|
if ids.has(int(pair.get("rig_id", -1))) or ids.has(int(pair.get("prop_id", -1))):
|
|
|
|
|
_collision_pairs.erase(key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func reset_runtime_state() -> void:
|
|
|
|
|
_area_overlap.clear()
|
|
|
|
|
_area_previous_bounds.clear()
|
|
|
|
|
_collision_pairs.clear()
|
|
|
|
|
_suppressed_contact_pairs.clear()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func movable_point(node: Node2D) -> Vector2:
|
|
|
|
|
if node is STICKMAN_RIG_SCRIPT:
|
|
|
|
|
return node.global_position - StickmanLocomotion.FOOT_OFFSET
|
|
|
|
|
return node.global_position
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func movable_overlaps_area(node: Node2D, area_rect: Rect2) -> bool:
|
|
|
|
|
var bounds: Rect2 = STAGE_SELECTION.get_world_aabb(node).abs()
|
|
|
|
|
if bounds.size.is_zero_approx():
|
|
|
|
|
return area_rect.has_point(movable_point(node))
|
|
|
|
|
return area_rect.intersects(bounds, true)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_area_overlap() -> Dictionary:
|
|
|
|
|
return _area_overlap
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_area_previous_bounds() -> Dictionary:
|
|
|
|
|
return _area_previous_bounds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_collision_pairs() -> Dictionary:
|
|
|
|
|
return _collision_pairs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_suppressed_contact_pairs() -> Dictionary:
|
|
|
|
|
return _suppressed_contact_pairs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func contact_key(prop: PropBlock, other: Node) -> String:
|
|
|
|
|
return "%d:%d" % [prop.get_instance_id(), other.get_instance_id()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func collision_key(rig: StickmanRig, prop: PropBlock) -> String:
|
|
|
|
|
return "%d:%d" % [rig.get_instance_id(), prop.get_instance_id()]
|