Record original-alignment implementation and traceability checkpoint
Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
This commit is contained in:
@@ -103,6 +103,30 @@ func get_raid_ids() -> Array[StringName]:
|
||||
return _sorted_keys(_raids)
|
||||
|
||||
|
||||
## Explicit IDs in valid older saves need not have advanced Runtime's allocator.
|
||||
## Query the existing history without exposing or copying its mutable payloads.
|
||||
func has_resolution(raid_id: StringName, resolution_id: StringName) -> bool:
|
||||
return _raids.has(raid_id) and (_raids[raid_id][&"resolved_rounds"] as Dictionary).has(resolution_id)
|
||||
|
||||
|
||||
## Detached primitives only; ordinary ticks must not clone growing round history.
|
||||
## The interval is already part of each persisted raid, not a new balance value.
|
||||
func get_due_rounds(absolute_minute: int) -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
for raid_id: StringName in _sorted_keys(_raids):
|
||||
var raid: Dictionary = _raids[raid_id]
|
||||
if int(raid[&"state"]) != RaidState.ACTIVE:
|
||||
continue
|
||||
var earliest := int(raid[&"state_changed_at"]) + int(raid[&"round_minutes"])
|
||||
if earliest <= absolute_minute:
|
||||
result.append({
|
||||
&"raid_id": raid_id,
|
||||
&"earliest_minute": earliest,
|
||||
&"round_number": int(raid[&"current_round"]) + 1,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
func get_active_raid_ids() -> Array[StringName]:
|
||||
var result: Array[StringName] = []
|
||||
for raid_id: StringName in _sorted_keys(_raids):
|
||||
@@ -112,6 +136,75 @@ func get_active_raid_ids() -> Array[StringName]:
|
||||
return result
|
||||
|
||||
|
||||
## Compact, owned projection: never copy combatants or accumulated round history
|
||||
## just to display the current threats. Preparing raids also lock employees.
|
||||
func get_threat_summary() -> Dictionary:
|
||||
var counts := {&"preparing": 0, &"active": 0, &"between_waves": 0, &"total": 0}
|
||||
for raid: Dictionary in _raids.values():
|
||||
match int(raid[&"state"]):
|
||||
RaidState.PREPARING:
|
||||
counts[&"preparing"] += 1
|
||||
RaidState.ACTIVE:
|
||||
counts[&"active"] += 1
|
||||
RaidState.BETWEEN_WAVES:
|
||||
counts[&"between_waves"] += 1
|
||||
counts[&"total"] = counts[&"preparing"] + counts[&"active"] + counts[&"between_waves"]
|
||||
return counts
|
||||
|
||||
|
||||
## Owned UI projections. Deliberately omit resolved rounds and skill history.
|
||||
func get_management_rows() -> Array[Dictionary]:
|
||||
var rows: Array[Dictionary] = []
|
||||
for raid_id: StringName in _sorted_keys(_raids):
|
||||
var raid: Dictionary = _raids[raid_id]
|
||||
var missing: Array[StringName] = []
|
||||
for objective_id: StringName in raid[&"objective_ids"]:
|
||||
if not (raid[&"assets"] as Dictionary).has(objective_id):
|
||||
missing.append(objective_id)
|
||||
var defender_ids: Array[StringName] = []
|
||||
for actor_id: StringName in _sorted_keys(raid[&"combatants"]):
|
||||
if raid[&"combatants"][actor_id][&"side"] == &"DEFENDER": defender_ids.append(actor_id)
|
||||
rows.append({
|
||||
&"raid_id": raid_id, &"state": int(raid[&"state"]),
|
||||
&"source_id": raid[&"source_id"], &"current_round": int(raid[&"current_round"]),
|
||||
&"source_kind": String((raid.get(&"event_source", {}) as Dictionary).get("source_kind", "")),
|
||||
&"created_at": int(raid[&"created_at"]),
|
||||
&"current_wave": int(raid[&"current_wave_index"]) + 1,
|
||||
&"wave_count": (raid[&"waves"] as Array).size(),
|
||||
&"retreat_policy": raid[&"retreat_policy"],
|
||||
&"defender_retreat_threshold": int(raid[&"defender_retreat_threshold"]),
|
||||
&"objective_ids": (raid[&"objective_ids"] as Array).duplicate(),
|
||||
&"missing_objectives": missing,
|
||||
&"defender_ids": defender_ids, &"asset_ids": _sorted_keys(raid[&"assets"]),
|
||||
&"result": raid[&"result"], &"result_reason": raid[&"result_reason"],
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
func get_management_detail(raid_id: StringName) -> Dictionary:
|
||||
if not _raids.has(raid_id):
|
||||
return {}
|
||||
var raid: Dictionary = _raids[raid_id]
|
||||
var defenders: Array[Dictionary] = []
|
||||
for actor_id: StringName in _sorted_keys(raid[&"combatants"]):
|
||||
var actor: Dictionary = raid[&"combatants"][actor_id]
|
||||
if actor[&"side"] == &"DEFENDER":
|
||||
defenders.append({
|
||||
&"actor_id": actor_id, &"health": actor[&"health"],
|
||||
&"max_health": actor[&"max_health"], &"combat": actor[&"combat"],
|
||||
&"fatigue": actor[&"fatigue"], &"state": actor[&"state"],
|
||||
})
|
||||
var assets: Array[Dictionary] = []
|
||||
for asset_id: StringName in _sorted_keys(raid[&"assets"]):
|
||||
var asset: Dictionary = raid[&"assets"][asset_id]
|
||||
assets.append({
|
||||
&"asset_id": asset_id, &"kind": asset[&"kind"],
|
||||
&"integrity": asset[&"integrity"], &"max_integrity": asset[&"max_integrity"],
|
||||
&"defense": asset[&"defense"],
|
||||
})
|
||||
return {&"defenders": defenders, &"assets": assets}
|
||||
|
||||
|
||||
func create_raid(
|
||||
raid_id: StringName,
|
||||
absolute_minute: int,
|
||||
@@ -124,6 +217,11 @@ func create_raid(
|
||||
if _raids.has(raid_id):
|
||||
return _duplicate("Raid", raid_id)
|
||||
|
||||
var event_source_value: Variant = _value(definition, &"event_source", {})
|
||||
if not _is_valid_event_source(event_source_value):
|
||||
return _invalid("Event threat provenance is malformed.")
|
||||
var event_source: Dictionary = event_source_value.duplicate(true)
|
||||
|
||||
var entry_points := _sorted_unique_names(_value(definition, &"entry_points", []))
|
||||
var objective_ids := _sorted_unique_names(_value(definition, &"objective_ids", []))
|
||||
if entry_points.is_empty() or objective_ids.is_empty():
|
||||
@@ -146,6 +244,7 @@ func create_raid(
|
||||
var raid := {
|
||||
&"raid_id": raid_id,
|
||||
&"source_id": StringName(_value(definition, &"source_id", &"")),
|
||||
&"event_source": event_source,
|
||||
&"state": RaidState.PREPARING,
|
||||
&"created_at": absolute_minute,
|
||||
&"started_at": -1,
|
||||
@@ -1372,6 +1471,7 @@ func _raid_to_snapshot(raid: Dictionary) -> Dictionary:
|
||||
var snapshot := {
|
||||
&"raid_id": String(raid[&"raid_id"]),
|
||||
&"source_id": String(raid[&"source_id"]),
|
||||
&"event_source": _canonical_snapshot_value(raid.get(&"event_source", {})),
|
||||
&"state": RaidState.keys()[int(raid[&"state"])],
|
||||
&"created_at": raid[&"created_at"],
|
||||
&"started_at": raid[&"started_at"],
|
||||
@@ -1432,6 +1532,9 @@ func _raid_to_snapshot(raid: Dictionary) -> Dictionary:
|
||||
|
||||
|
||||
func _raid_from_snapshot(raw: Dictionary) -> Dictionary:
|
||||
var event_source_value: Variant = _value(raw, &"event_source", {})
|
||||
if not _is_valid_event_source(event_source_value):
|
||||
return {&"ok": false, &"error": "Event threat provenance snapshot is malformed."}
|
||||
for collection_key: StringName in [&"waves", &"combatants", &"assets", &"queued_skills", &"applied_skill_ids", &"resolved_rounds"]:
|
||||
if not _value(raw, collection_key, null) is Array:
|
||||
return {&"ok": false, &"error": "Malformed Raid collection: %s" % collection_key}
|
||||
@@ -1442,6 +1545,7 @@ func _raid_from_snapshot(raw: Dictionary) -> Dictionary:
|
||||
var raid := {
|
||||
&"raid_id": raid_id,
|
||||
&"source_id": StringName(_value(raw, &"source_id", &"")),
|
||||
&"event_source": event_source_value.duplicate(true),
|
||||
&"state": state_index,
|
||||
&"created_at": int(_value(raw, &"created_at", -1)),
|
||||
&"started_at": int(_value(raw, &"started_at", -1)),
|
||||
@@ -1563,6 +1667,8 @@ func _raid_from_snapshot(raw: Dictionary) -> Dictionary:
|
||||
|
||||
func _validate_raid(raid: Dictionary) -> Dictionary:
|
||||
var errors: Array[String] = []
|
||||
if not _is_valid_event_source(raid.get(&"event_source", {})):
|
||||
errors.append("Event threat provenance is invalid.")
|
||||
var raid_id := StringName(raid.get(&"raid_id", &""))
|
||||
var state := int(raid.get(&"state", -1))
|
||||
var created_at := int(raid.get(&"created_at", -1))
|
||||
@@ -1749,7 +1855,10 @@ func _validate_raid(raid: Dictionary) -> Dictionary:
|
||||
var listed_objective: bool = objective_ids is Array and objective_ids.has(asset_id)
|
||||
if listed_objective != (kind == AssetKind.OBJECTIVE):
|
||||
errors.append("Defense objective registration mismatch: %s" % asset_id)
|
||||
if state != RaidState.PREPARING and assets is Dictionary and objective_ids is Array:
|
||||
# Preparation may be cancelled before all objective assets are registered.
|
||||
# A raid that actually started must retain those assets even after cancellation.
|
||||
var cancelled_before_start := state == RaidState.CANCELLED and started_at == -1
|
||||
if state != RaidState.PREPARING and not cancelled_before_start and assets is Dictionary and objective_ids is Array:
|
||||
for objective_id: StringName in objective_ids:
|
||||
if not assets.has(objective_id):
|
||||
errors.append("Started Raid is missing objective asset: %s" % objective_id)
|
||||
@@ -2127,3 +2236,28 @@ func _not_found(kind: String, value_id: StringName) -> CommandResult:
|
||||
last_error,
|
||||
{&"id": value_id}
|
||||
)
|
||||
|
||||
|
||||
## Optional provenance is absent in legacy/manual raids. Configured event raids
|
||||
## retain it so a replay cannot silently replace an already-running incident.
|
||||
func _is_valid_event_source(raw: Variant) -> bool:
|
||||
if not raw is Dictionary:
|
||||
return false
|
||||
if raw.is_empty():
|
||||
return true
|
||||
var keys := ["event_instance_id", "event_action_id", "template_id", "template_digest", "source_kind"]
|
||||
if raw.size() != keys.size():
|
||||
return false
|
||||
for key: String in keys:
|
||||
var value: Variant = _value(raw, StringName(key), null)
|
||||
if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME] or String(value).is_empty():
|
||||
return false
|
||||
if String(_value(raw, &"source_kind", "")) not in ["THIEF", "BANKRUPTCY_RIOT", "BANK_DELINQUENCY", "AUTHORITY_ESCALATION"]:
|
||||
return false
|
||||
var digest := String(_value(raw, &"template_digest", ""))
|
||||
if digest.length() != 64:
|
||||
return false
|
||||
for character: String in digest:
|
||||
if character not in "0123456789abcdef":
|
||||
return false
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user