P14-02: Add buildable Mana warp (A07)
2x2 FRONT warp at 3000G/30 materials/360 min, 15G/day and 1 Mana upkeep. Every completed warp pair is linked by one path connector, so crossing is a single step; a 3-Mana fare per rider is charged once in the step's tick. Warps close while either end is unpowered, the reserve is empty or below one fare, and blocked arrivals offer no step. Saves validate the derived links. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7e6e540837
commit
0cfaec1626
@@ -59,11 +59,13 @@ const RESCUE_FACILITY_PATHS: Array[String] = ["res://data/facilities/rescue/infi
|
||||
const INFRASTRUCTURE_FACILITY_IDS: Array[StringName] = [
|
||||
&"guest_stairs",
|
||||
&"service_stairs",
|
||||
&"mana_warp",
|
||||
]
|
||||
|
||||
const INFRASTRUCTURE_FACILITY_PATHS: Array[String] = [
|
||||
"res://data/facilities/infrastructure/guest_stairs.tres",
|
||||
"res://data/facilities/infrastructure/service_stairs.tres",
|
||||
"res://data/facilities/infrastructure/mana_warp.tres",
|
||||
]
|
||||
|
||||
## Optional post-MVP expansion definitions. They live below a subdirectory so
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
extends RefCounted
|
||||
|
||||
## A07 Mana warp: every completed warp is linked to every other warp by one
|
||||
## path connector, so crossing is a single instant step. Connectors are derived
|
||||
## from construction state; their operational state follows the reserve and the
|
||||
## warps' own power. Fares are charged by the Runtime when an actor steps.
|
||||
const PathType := preload("res://scripts/world/path_service.gd")
|
||||
const DEFINITION_ID := &"mana_warp"
|
||||
const PREFIX := "warp.connector:"
|
||||
const MANA_PER_TRIP := 3
|
||||
const PATH_COST := 400
|
||||
|
||||
|
||||
static func is_warp_connector(connector_id: StringName) -> bool:
|
||||
return String(connector_id).begins_with(PREFIX)
|
||||
|
||||
|
||||
## Completed warps sorted by instance id: [{instance_id, origin}].
|
||||
static func warps_from_facilities(facilities: Array) -> Array[Dictionary]:
|
||||
var warps: Array[Dictionary] = []
|
||||
for value: Variant in facilities:
|
||||
if not value is Dictionary:
|
||||
continue
|
||||
var facility: Dictionary = value
|
||||
if StringName(str(facility.get(&"definition_id", ""))) != DEFINITION_ID:
|
||||
continue
|
||||
var origin: Variant = facility.get(&"origin")
|
||||
if not origin is Vector3i:
|
||||
continue
|
||||
warps.append({&"instance_id": StringName(str(facility.get(&"instance_id", ""))), &"origin": origin})
|
||||
warps.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return String(a[&"instance_id"]) < String(b[&"instance_id"]))
|
||||
return warps
|
||||
|
||||
|
||||
static func expected_connectors(facilities: Array) -> Array[Dictionary]:
|
||||
var warps := warps_from_facilities(facilities)
|
||||
var result: Array[Dictionary] = []
|
||||
for i: int in warps.size():
|
||||
for j: int in range(i + 1, warps.size()):
|
||||
result.append({
|
||||
&"id": StringName("%s%s|%s" % [PREFIX, warps[i][&"instance_id"], warps[j][&"instance_id"]]),
|
||||
&"from": warps[i][&"origin"],
|
||||
&"to": warps[j][&"origin"],
|
||||
&"endpoints": [warps[i][&"instance_id"], warps[j][&"instance_id"]],
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
## Rebuilds warp connectors from construction state and applies availability.
|
||||
## available: instance_id -> bool. Atomic: on failure the path is restored. The
|
||||
## rollback snapshot is taken only when a change is actually needed.
|
||||
static func synchronize(path: PathService, facilities: Array, available: Dictionary, fare_affordable: bool) -> Dictionary:
|
||||
var expected := expected_connectors(facilities)
|
||||
var expected_ids: Dictionary = {}
|
||||
for connector: Dictionary in expected:
|
||||
expected_ids[connector[&"id"]] = true
|
||||
var before: Variant = null
|
||||
for connector_id: StringName in path.get_connector_ids_with_prefix(PREFIX):
|
||||
if expected_ids.has(connector_id):
|
||||
continue
|
||||
if before == null:
|
||||
before = path.create_snapshot()
|
||||
if not path.remove_connector(connector_id):
|
||||
path.restore_snapshot(before)
|
||||
return {&"ok": false, &"code": &"WARP_CONNECTOR_REMOVE_FAILED", &"reason": path.last_error}
|
||||
for connector: Dictionary in expected:
|
||||
var connector_id: StringName = connector[&"id"]
|
||||
var existing := path.get_connector(connector_id)
|
||||
var endpoints: Array = connector[&"endpoints"]
|
||||
var usable := fare_affordable and bool(available.get(endpoints[0], false)) and bool(available.get(endpoints[1], false))
|
||||
var state := PathType.CONNECTOR_OPERATIONAL if usable else PathType.CONNECTOR_OUTAGE
|
||||
if not existing.is_empty() and existing[&"from"] == connector[&"from"] and existing[&"to"] == connector[&"to"] \
|
||||
and existing[&"operational_state"] == state:
|
||||
continue
|
||||
if before == null:
|
||||
before = path.create_snapshot()
|
||||
if existing.is_empty() or existing[&"from"] != connector[&"from"] or existing[&"to"] != connector[&"to"]:
|
||||
if not existing.is_empty():
|
||||
path.remove_connector(connector_id)
|
||||
if not path.configure_connector(connector_id, connector[&"from"], connector[&"to"], PATH_COST, true, &"", true,
|
||||
{&"movement_type": PathType.MOVEMENT_CONNECTOR}):
|
||||
path.restore_snapshot(before)
|
||||
return {&"ok": false, &"code": &"WARP_CONNECTOR_CONFIGURE_FAILED", &"reason": path.last_error}
|
||||
if not path.set_connector_operational_state(connector_id, state):
|
||||
path.restore_snapshot(before)
|
||||
return {&"ok": false, &"code": &"WARP_CONNECTOR_STATE_FAILED", &"reason": path.last_error}
|
||||
return {&"ok": true, &"connector_count": expected.size()}
|
||||
|
||||
|
||||
## Saved paths must hold exactly the derived warp connectors.
|
||||
static func connectors_match(path: PathService, facilities: Array) -> bool:
|
||||
var expected := expected_connectors(facilities)
|
||||
var ids := path.get_connector_ids_with_prefix(PREFIX)
|
||||
if ids.size() != expected.size():
|
||||
return false
|
||||
for connector: Dictionary in expected:
|
||||
var existing := path.get_connector(connector[&"id"])
|
||||
if existing.is_empty() or existing[&"from"] != connector[&"from"] or existing[&"to"] != connector[&"to"] \
|
||||
or int(existing[&"cost"]) != PATH_COST or StringName(existing[&"access_id"]) != &"" \
|
||||
or existing[&"movement_type"] != PathType.MOVEMENT_CONNECTOR:
|
||||
return false
|
||||
return true
|
||||
@@ -89,6 +89,16 @@ static func room(canvas: CanvasItem, rect: Rect2, id: StringName, pixel: float,
|
||||
table(canvas, at + Vector2(-2, -7)*p, p)
|
||||
&"chapel":
|
||||
chapel_furnishings(canvas, rect, p)
|
||||
&"mana_warp":
|
||||
# Original code-drawn transfer circle and rising motes; existing palette.
|
||||
var ring := Vector2(mid, floor_y - 3.0 * p)
|
||||
canvas.draw_circle(ring, 9.0 * p, INK)
|
||||
canvas.draw_circle(ring, 8.0 * p, TEAL)
|
||||
canvas.draw_circle(ring, 6.0 * p, Color("c1e0d4"))
|
||||
canvas.draw_circle(ring, 4.0 * p, TEAL)
|
||||
for i in 5:
|
||||
var x := mid + (i - 2) * 4.0 * p
|
||||
canvas.draw_rect(Rect2(x, floor_y - (9 + (i % 3) * 5) * p, p, 2.0 * p), CREAM)
|
||||
&"public_bath":
|
||||
canvas.draw_rect(Rect2(rect.position.x+4*p, floor_y-7*p, w-8*p, 7*p), CREAM)
|
||||
canvas.draw_rect(Rect2(rect.position.x+6*p, floor_y-6*p, w-12*p, 4*p), Color("72b6b8"))
|
||||
|
||||
@@ -35,6 +35,7 @@ const KIND_GUEST_ELEVATOR: StringName = &"guest_elevator"
|
||||
const KIND_SERVICE_ELEVATOR: StringName = &"service_elevator"
|
||||
const KIND_GUEST_STAIRS: StringName = &"guest_stairs"
|
||||
const KIND_SERVICE_STAIRS: StringName = &"service_stairs"
|
||||
const KIND_MANA_WARP: StringName = &"mana_warp"
|
||||
const SUPPORTED_KINDS: Array[StringName] = [
|
||||
KIND_GUEST_CORRIDOR,
|
||||
KIND_SERVICE_CORRIDOR,
|
||||
@@ -43,6 +44,7 @@ const SUPPORTED_KINDS: Array[StringName] = [
|
||||
KIND_SERVICE_ELEVATOR,
|
||||
KIND_GUEST_STAIRS,
|
||||
KIND_SERVICE_STAIRS,
|
||||
KIND_MANA_WARP,
|
||||
]
|
||||
const KNOWN_KINDS: Array[StringName] = [
|
||||
KIND_GUEST_CORRIDOR,
|
||||
@@ -52,6 +54,7 @@ const KNOWN_KINDS: Array[StringName] = [
|
||||
KIND_SERVICE_ELEVATOR,
|
||||
KIND_GUEST_STAIRS,
|
||||
KIND_SERVICE_STAIRS,
|
||||
KIND_MANA_WARP,
|
||||
]
|
||||
|
||||
const UNSUPPORTED_VERTICAL_KINDS: Array[StringName] = []
|
||||
@@ -692,6 +695,17 @@ static func _kind_schema(kind: StringName) -> Dictionary:
|
||||
&"guest_access", &"movement", &"schema_facility_proxy", &"vertical_connector",
|
||||
]),
|
||||
}
|
||||
KIND_MANA_WARP:
|
||||
# Walkable FRONT cells; WarpService links every warp pair (A07).
|
||||
return {
|
||||
&"layer": PathType.FRONT,
|
||||
&"footprint": Vector2i(2, 2),
|
||||
&"pass_through": true,
|
||||
&"staff_only": false,
|
||||
&"tags": _sorted_name_array([
|
||||
&"guest_access", &"movement", &"schema_facility_proxy", &"warp",
|
||||
]),
|
||||
}
|
||||
KIND_SERVICE_ELEVATOR:
|
||||
return {
|
||||
&"layer": PathType.BACK,
|
||||
|
||||
@@ -1420,6 +1420,15 @@ func has_connector(connector_id: StringName) -> bool:
|
||||
return _connectors.has(connector_id)
|
||||
|
||||
|
||||
func get_connector_ids_with_prefix(prefix: String) -> Array[StringName]:
|
||||
var ids: Array[StringName] = []
|
||||
for connector_id: Variant in _connectors:
|
||||
if String(connector_id).begins_with(prefix):
|
||||
ids.append(StringName(connector_id))
|
||||
ids.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
|
||||
return ids
|
||||
|
||||
|
||||
func get_connector(connector_id: StringName) -> Dictionary:
|
||||
if not _connectors.has(connector_id):
|
||||
return {}
|
||||
|
||||
Reference in New Issue
Block a user