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
@@ -30,6 +30,7 @@ const CLEANING_RUNTIME_SNAPSHOT_VERSION := 12
|
||||
const WEATHER_RUNTIME_SNAPSHOT_VERSION := 11
|
||||
const FacilityCleanliness = preload("res://scripts/environment/facility_cleanliness_service.gd")
|
||||
const ManaReserveCycle = preload("res://scripts/mana/mana_reserve_cycle.gd")
|
||||
const WarpService = preload("res://scripts/transport/warp_service.gd")
|
||||
const CleaningBridge = preload("res://scripts/environment/facility_cleaning_bridge.gd")
|
||||
const CleaningPolicy = preload("res://scripts/environment/facility_cleaning_policy.gd")
|
||||
const UPKEEP_RUNTIME_SNAPSHOT_VERSION := 10
|
||||
@@ -1257,6 +1258,8 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
if not _validate_elevator_crosslinks(restored_elevators, restored_path, restored_construction,
|
||||
restored_guests, restored_staff, restored_runtime_domain_minute):
|
||||
return false
|
||||
if not WarpService.connectors_match(restored_path, restored_construction.get_facilities()):
|
||||
return false
|
||||
for facility: FacilityInstance in restored_facilities.get_instances():
|
||||
for staff_id: StringName in facility.active_staff_ids:
|
||||
if not restored_elevators.get_actor_trip(_staff_path_actor_id(staff_id)).is_empty() and not CleaningBridge.is_deployed(restored_staff.get_staff(staff_id), facility, restored_staff):
|
||||
@@ -7844,7 +7847,8 @@ func _execute_demolish_facility(command: SimulationCommand) -> CommandResult:
|
||||
demolished.command_id = command.command_id
|
||||
var transport_ok := false
|
||||
if demolished.is_success():
|
||||
transport_ok = bool(_synchronize_elevators(_runtime_event_minute()).get(&"ok", false)) and _prune_legacy_stairs_and_movement()
|
||||
transport_ok = bool(_synchronize_elevators(_runtime_event_minute()).get(&"ok", false)) \
|
||||
and bool(_synchronize_warps().get(&"ok", false)) and _prune_legacy_stairs_and_movement()
|
||||
if not transport_ok or not demolished.is_success() \
|
||||
or not _validate_runtime_path_crosslinks(path_service, guest_service, staff_service, _movement_credits, _runtime_event_minute()) \
|
||||
or facility_infrastructure_projector.has_projection(instance_id) \
|
||||
@@ -8359,6 +8363,11 @@ func _apply_completed_facility_internal(instance: Dictionary, absolute_minute: i
|
||||
var transport := _synchronize_elevators(absolute_minute)
|
||||
if not bool(transport.get(&"ok", false)):
|
||||
return {&"ok": false, &"code": &"ELEVATOR_SYNC_FAILED", &"reason": elevators.last_error, &"details": transport}
|
||||
if definition.id == WarpService.DEFINITION_ID:
|
||||
_register_facility_mana_consumer(instance)
|
||||
var warp_sync := _synchronize_warps()
|
||||
if not bool(warp_sync.get(&"ok", false)):
|
||||
return {&"ok": false, &"code": &"WARP_SYNC_FAILED", &"reason": String(warp_sync.get(&"reason", "")), &"details": warp_sync}
|
||||
if not _prune_legacy_stairs_and_movement():
|
||||
return {&"ok": false, &"code": &"LEGACY_STAIR_PRUNE_FAILED", &"reason": "Stair topology could not be updated."}
|
||||
if not _publish_domain_event(SimulationDomainEvent.new(
|
||||
@@ -8976,7 +8985,8 @@ func _advance_guest_path_travel(
|
||||
actor_id, guest.current_cell, absolute_minute
|
||||
)
|
||||
accumulated = mini(accumulated, required_credit + MOVEMENT_CREDIT_SCALE - 1)
|
||||
if accumulated < required_credit:
|
||||
var guest_warp := _warp_step_connector(path[0], path[1], [])
|
||||
if accumulated < required_credit or not _warp_fare_affordable(guest_warp, 1):
|
||||
_record_blocked_actor_wait(
|
||||
actor_id,
|
||||
guest.guest_id,
|
||||
@@ -9000,6 +9010,7 @@ func _advance_guest_path_travel(
|
||||
guest.recovery_kind = previous_recovery_kind
|
||||
_report_path_integration_failure(actor_id, guest.guest_id, &"guest", previous_cell)
|
||||
return
|
||||
_charge_warp_fare(guest_warp, [actor_id], previous_cell, guest.current_cell)
|
||||
var remaining_credit := accumulated - required_credit
|
||||
_movement_credits[actor_id] = {
|
||||
&"credit": remaining_credit,
|
||||
@@ -9102,7 +9113,9 @@ func _advance_staff_task_travel(
|
||||
actor_id, staff.current_cell, absolute_minute
|
||||
)
|
||||
accumulated = mini(accumulated, required_credit + MOVEMENT_CREDIT_SCALE - 1)
|
||||
if accumulated < required_credit:
|
||||
var staff_warp := _warp_step_connector(path[0], path[1], [&"staff"])
|
||||
var warp_riders := 2 if task.task_type == &"rescue_transport" else 1
|
||||
if accumulated < required_credit or not _warp_fare_affordable(staff_warp, warp_riders):
|
||||
_record_blocked_actor_wait(
|
||||
actor_id,
|
||||
staff.staff_id,
|
||||
@@ -9126,6 +9139,9 @@ func _advance_staff_task_travel(
|
||||
return
|
||||
patient.current_cell = staff.current_cell
|
||||
_record_expected_actor_wait(_staff_path_actor_id(patient.staff_id), patient.current_cell, absolute_minute)
|
||||
_charge_warp_fare(staff_warp, [actor_id, _staff_path_actor_id(patient.staff_id)], previous_cell, staff.current_cell)
|
||||
else:
|
||||
_charge_warp_fare(staff_warp, [actor_id], previous_cell, staff.current_cell)
|
||||
var remaining_credit := accumulated - required_credit
|
||||
_movement_credits[actor_id] = {
|
||||
&"credit": remaining_credit,
|
||||
@@ -10802,6 +10818,11 @@ func _advance_mana(absolute_minute: int) -> void:
|
||||
preload("res://scripts/mana/mana_power_projection.gd").apply(
|
||||
facility_service, mana_network_service, int(_state.get_resource(&"mana")), absolute_minute
|
||||
)
|
||||
var warp_sync := _synchronize_warps()
|
||||
if not bool(warp_sync.get(&"ok", false)):
|
||||
_latch_infrastructure_failure(&"WARP_SYNC_FAILED", absolute_minute,
|
||||
"Warp connectors could not follow the current power state.", warp_sync)
|
||||
return
|
||||
for change: Dictionary in report.get(&"state_changes", []):
|
||||
var consumer_id := StringName(change.get(&"consumer_id", ""))
|
||||
_publish_domain_event(SimulationDomainEvent.new(
|
||||
@@ -12821,7 +12842,8 @@ func advance_staff_commute(staff_id: StringName, target_cell: Vector3i, minute:
|
||||
var required_credit := _step_movement_credit(path_service, path[0], path[1], [&"staff"])
|
||||
var credit := _movement_credit_after_one_minute(actor_id, staff.current_cell, minute)
|
||||
credit = mini(credit, required_credit + MOVEMENT_CREDIT_SCALE - 1)
|
||||
if credit < required_credit:
|
||||
var commute_warp := _warp_step_connector(path[0], path[1], [&"staff"])
|
||||
if credit < required_credit or not _warp_fare_affordable(commute_warp, 1):
|
||||
_record_expected_actor_wait(actor_id, staff.current_cell, minute)
|
||||
_movement_credits[actor_id] = {&"credit": credit, &"updated_at": minute}
|
||||
return {&"ok": true, &"moved": false}
|
||||
@@ -12830,6 +12852,7 @@ func advance_staff_commute(staff_id: StringName, target_cell: Vector3i, minute:
|
||||
_report_path_integration_failure(actor_id, staff_id, &"staff", previous)
|
||||
return {&"ok": false}
|
||||
staff.current_cell = path[1]
|
||||
_charge_warp_fare(commute_warp, [actor_id], previous, staff.current_cell)
|
||||
_movement_credits[actor_id] = {&"credit": credit - required_credit, &"updated_at": minute}
|
||||
_publish_actor_event(&"STAFF_MOVED", &"staff_commute", [staff_id, actor_id], {
|
||||
&"path_actor_id": actor_id, &"from_cell": previous, &"cell": staff.current_cell,
|
||||
@@ -12913,6 +12936,42 @@ func _execute_set_elevator_operation(command: SimulationCommand) -> CommandResul
|
||||
return CommandResult.succeeded(command.command_id, {&"shaft_id": shaft_id, &"operator_open": open_value})
|
||||
|
||||
|
||||
## A07: warp connectors exist for every completed warp pair and are usable only
|
||||
## while both warps are powered and the reserve can pay one fare.
|
||||
func _synchronize_warps() -> Dictionary:
|
||||
var available := {}
|
||||
var reserve_ok := int(_state.get_resource(&"mana")) > 0
|
||||
for warp: Dictionary in WarpService.warps_from_facilities(construction_service.get_facilities()):
|
||||
var state := mana_network_service.get_consumer_state(warp[&"instance_id"])
|
||||
available[warp[&"instance_id"]] = reserve_ok and state in [ManaNetworkService.ConsumerState.ACTIVE, ManaNetworkService.ConsumerState.DEGRADED]
|
||||
return WarpService.synchronize(path_service, construction_service.get_facilities(), available,
|
||||
int(_state.get_resource(&"mana")) >= WarpService.MANA_PER_TRIP)
|
||||
|
||||
|
||||
func _warp_step_connector(from: Vector3i, to: Vector3i, access: Array[StringName]) -> StringName:
|
||||
var connector_id := path_service.get_step_connector(from, to, access)
|
||||
return connector_id if WarpService.is_warp_connector(connector_id) else &""
|
||||
|
||||
|
||||
func _warp_fare_affordable(connector_id: StringName, riders: int) -> bool:
|
||||
return connector_id.is_empty() or int(_state.get_resource(&"mana")) >= WarpService.MANA_PER_TRIP * riders
|
||||
|
||||
|
||||
## Charged once, in the same tick as the committed step; affordability was
|
||||
## checked before the step, so the deduction cannot fail here.
|
||||
func _charge_warp_fare(connector_id: StringName, actor_ids: Array, from: Vector3i, to: Vector3i) -> void:
|
||||
if connector_id.is_empty():
|
||||
return
|
||||
var fare := WarpService.MANA_PER_TRIP * actor_ids.size()
|
||||
_state.apply_resource_deltas({&"mana": -fare})
|
||||
var subjects: Array[StringName] = [connector_id]
|
||||
for actor_id: Variant in actor_ids:
|
||||
subjects.append(StringName(actor_id))
|
||||
_publish_actor_event(&"WARP_TRAVELED", &"warp", subjects, {
|
||||
&"connector_id": connector_id, &"from_cell": from, &"cell": to,
|
||||
&"riders": actor_ids.size(), &"mana": fare})
|
||||
|
||||
|
||||
func _synchronize_elevators(minute: int) -> Dictionary:
|
||||
return elevators.synchronize(ElevatorService.configurations_from_facilities(
|
||||
construction_service.get_facilities(), definition_registry), minute)
|
||||
|
||||
Reference in New Issue
Block a user