P07-01-WX: 仕上がった天候の機能の受入:自然天候・雨・嵐・世界樹と成人客のマナ霧・上部の表示と予報・保存と移行(マナ炉の産出 120 を除く。P07-01 の補助の作業。原単位への加算 0)

This commit is contained in:
Agent Control Runner
2026-10-24 05:04:48 +09:00
parent 7ac2224e47
commit c50136017b
25 changed files with 1868 additions and 28 deletions
+86 -5
View File
@@ -37,6 +37,7 @@ const CleaningBridge = preload("res://scripts/environment/facility_cleaning_brid
const CleaningPolicy = preload("res://scripts/environment/facility_cleaning_policy.gd")
const UPKEEP_RUNTIME_SNAPSHOT_VERSION := 10
const WeatherTimeline = preload("res://scripts/environment/weather_timeline.gd")
const NaturalWeather = preload("res://scripts/environment/natural_weather_policy.gd")
const STAIRS_RUNTIME_SNAPSHOT_VERSION := 9
const TRANSPORT_RUNTIME_SNAPSHOT_VERSION := 8
const SUPPLY_RUNTIME_SNAPSHOT_VERSION := 7
@@ -85,6 +86,7 @@ const RNG_STREAM_IDS: Array[StringName] = [
&"defense",
&"event",
&"loot",
&"weather",
]
const DEFENSE_COMMAND_TYPES: Array[StringName] = [
&"create_defense_raid",
@@ -463,6 +465,8 @@ func initialize(clock: SimulationKernel, state: Variant, seed := DEFAULT_WORLD_S
if not report[&"valid"]:
_clock.set_paused(true)
definition_validation_failed.emit(report)
# A13: a new game opening at 06:00 draws its first day once streams exist.
_draw_natural_weather_if_due(_clock.absolute_minute)
_publish_snapshot(_clock.absolute_minute)
return report
@@ -1291,6 +1295,15 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
return false
elif runtime_data.has(&"weather") or not restored_weather.initialize(&"clear", restored_minute):
return false
# A13 rain holds must still describe a present guest's extended plan.
var rain_holds := restored_weather.get_rain_holds()
for held_key: Variant in rain_holds:
var held_guest := restored_guests.get_guest(StringName(held_key))
if held_guest == null or held_guest.planned_exit_at != int(rain_holds[held_key]["extended_to"]):
return false
var saved_weather: Variant = runtime_data.get(&"weather", null)
var weather_stream_required: bool = saved_weather is Dictionary \
and int(saved_weather.get("version", 0)) >= WeatherTimeline.VERSION
var restored_cleanliness := FacilityCleanliness.new()
if runtime_schema_version >= CLEANING_RUNTIME_SNAPSHOT_VERSION:
if not runtime_data.get(&"facility_cleanliness") is Dictionary or not restored_cleanliness.restore_snapshot(runtime_data[&"facility_cleanliness"]):
@@ -1323,6 +1336,12 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
if has_defense_snapshot:
return false
restored_streams[&"defense"] = DeterministicRng.from_root_seed(restored_world_seed, &"defense")
# A13 migration: saves before natural weather start the weather stream from
# the world seed, as defense did (SPEC section 5.2).
if not restored_streams.has(&"weather"):
if weather_stream_required:
return false
restored_streams[&"weather"] = DeterministicRng.from_root_seed(restored_world_seed, &"weather")
if runtime_schema_version >= DEFENSE_RUNTIME_SNAPSHOT_VERSION:
for required_stream_id: StringName in RNG_STREAM_IDS:
if not restored_streams.has(required_stream_id):
@@ -2951,6 +2970,8 @@ func _operations_arrival_boundary(
&"ok": not captured.is_empty(),
&"token": captured,
&"capacity_remaining": maxi(population_cap - guest_service.guest_count(), 0) if int(_state.get_resource(&"mana")) > 0 and weather.permits_new_arrivals() else 0,
# A13: rain halves the planned new arrivals of this boundary.
&"arrival_percent": NaturalWeather.arrival_percent(weather.get_kind()),
}
if phase == OperationsCoordinator.PHASE_VALIDATE:
return {
@@ -8784,6 +8805,7 @@ func _advance_guest_vitals(absolute_minute: int) -> void:
&"is_night": minute_of_day < 360 or minute_of_day >= 1_080,
&"intimacy_day": 3,
&"intimacy_night": 7,
&"mana_fog": weather.get_kind() == &"mana_fog",
}
})
@@ -9035,7 +9057,7 @@ func _advance_guest_ai_decisions(absolute_minute: int) -> void:
GuestRuntime.State.ENTERING:
guest_service.transition_guest(guest.guest_id, GuestRuntime.State.DECIDING, absolute_minute, &"ENTRY_COMPLETED")
GuestRuntime.State.DECIDING:
if guest.planned_exit_at <= absolute_minute:
if guest.planned_exit_at <= absolute_minute and not _hold_guest_for_rain(guest, absolute_minute):
guest_service.begin_leaving(guest.guest_id, absolute_minute, &"PLANNED_EXIT")
continue
guest.wallet_gold = economy_service.get_balance(_guest_wallet_account(guest.guest_id))
@@ -10993,13 +11015,14 @@ func _reservation_references_are_valid(
## HUD view of the A06 reserve: supply, consumption and storage per interval.
func get_mana_reserve_report() -> Dictionary:
var demand := _mana_reserve_demand()
var generation := NaturalWeather.mana_yield(ManaReserveCycle.WORLD_TREE_GENERATION, weather.get_kind())
return {
&"amount": int(_state.get_resource(&"mana")),
&"capacity": ManaReserveCycle.CAPACITY,
&"generation": ManaReserveCycle.WORLD_TREE_GENERATION,
&"generation": generation,
&"interval_minutes": ManaReserveCycle.INTERVAL_MINUTES,
&"demand": demand,
&"net": ManaReserveCycle.WORLD_TREE_GENERATION - demand,
&"net": generation - demand,
&"last_flow": mana_reserve.get_last_flow(),
}
@@ -11032,8 +11055,10 @@ func _advance_mana(absolute_minute: int) -> void:
return
# A06: generation and upkeep demand settle before local distribution and the
# whole-tower gate, so the network and projection see the committed reserve.
# A13: mana fog raises the world tree yield of this boundary to x1.2 (floor).
var reserve_flow := mana_reserve.advance_to(
absolute_minute, int(_state.get_resource(&"mana")), _mana_reserve_demand()
absolute_minute, int(_state.get_resource(&"mana")), _mana_reserve_demand(),
NaturalWeather.mana_yield(ManaReserveCycle.WORLD_TREE_GENERATION, weather.get_kind())
)
if not bool(reserve_flow.get(&"ok", false)):
_latch_infrastructure_failure(
@@ -13572,10 +13597,66 @@ func get_staff_rest_report(staff_id: StringName) -> Dictionary:
func _advance_weather(absolute_minute: int) -> void:
weather.advance_to(absolute_minute)
_draw_natural_weather_if_due(absolute_minute)
if not weather.get_rain_holds().is_empty():
var present := {}
for guest: GuestRuntime in guest_service.get_guests():
present[guest.guest_id] = true
weather.prune_rain_holds(present)
## A13: one draw per day at 06:00 from the saved weather stream. The weather
## boundary runs first, so the same minute's arrivals and closes see the result.
func _draw_natural_weather_if_due(absolute_minute: int) -> void:
if not NaturalWeather.is_draw_minute(absolute_minute) or weather.get_minute() != absolute_minute:
return
var day := NaturalWeather.day_of(absolute_minute)
var stream := get_rng_stream(&"weather")
if day <= weather.get_last_draw_day() or stream == null:
return
var draw := NaturalWeather.draw(stream, day)
var applied := weather.apply_draw(draw)
var no_entities: Array[StringName] = []
_publish_actor_event(&"WEATHER_FORECAST_DRAWN", &"weather", no_entities, {
&"day": day,
&"kind": String(draw[&"kind"]),
&"roll": int(draw[&"roll"]),
&"slot": int(draw[&"slot"]),
&"start": int(draw[&"start"]),
&"end": int(draw[&"end"]),
&"applied": bool(applied.get(&"ok", false)),
&"code": String(applied.get(&"code", &"")),
})
## A13: a guest due to leave while it rains stays until the rain ends, at most
## 240 minutes after the original plan, once per guest (SPEC section 4.3).
func _hold_guest_for_rain(guest: GuestRuntime, absolute_minute: int) -> bool:
if weather.get_kind() != &"rain" or not weather.get_rain_hold(guest.guest_id).is_empty():
return false
var original := guest.planned_exit_at
var extended := original + NaturalWeather.RAIN_HOLD_MAX_MINUTES
var rain_end := weather.rain_end_minute()
if rain_end >= 0:
extended = mini(extended, rain_end)
if extended <= absolute_minute or not weather.add_rain_hold(guest.guest_id, original, extended):
return false
guest.planned_exit_at = extended
return true
func get_weather_report() -> Dictionary:
return {&"kind": weather.get_kind(), &"minute": weather.get_minute()}
var kind := weather.get_kind()
return {
&"kind": kind,
&"minute": weather.get_minute(),
&"since": weather.get_since(),
&"next_change": weather.next_change(),
&"today_draw": weather.get_today_draw(),
&"effect_text": NaturalWeather.effect_text(kind),
&"arrival_percent": NaturalWeather.arrival_percent(kind),
&"rain_holds": weather.get_rain_holds().size(),
}
## Read-only migration preview; applying dimensions remains a separate transaction.