Implement apocalypse mechanics: add apocalypse queue, controller, and integrate into production logic

This commit is contained in:
Kirill
2026-10-18 00:05:53 +03:00
parent f53181b9ad
commit 3047ef5a85
12 changed files with 393 additions and 7 deletions
+169
View File
@@ -0,0 +1,169 @@
extends "res://world/owniverse/simulation_module.gd"
class_name OwniverseApocalypseController
const E = preload("res://world/owniverse/math/exact.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
const TICK_SECONDS: float = 0.1
const END_WAIT_TICKS: int = 20
var _schedule: Array = []
func defaults() -> Dictionary:
return {"phase": 0, "type": 0, "warning": 0, "stage": 1, "tick": 0, "end_ticks": 0}
func check_density(target: Dictionary) -> void:
var apocalypse: Dictionary = target.get("apocalypse", defaults())
if E.gt(apocalypse.get("phase", 0), 0): return
var density: Variant = world.environment.stellar_environment(target).density
var parameters: Dictionary = WorldParameters.extra_params()
var warning: int = 0
if E.ge(density, parameters.bigcrunch3): warning = 3
elif E.ge(density, parameters.bigcrunch2): warning = 2
elif E.ge(density, parameters.bigcrunch1): warning = 1
elif E.le(density, parameters.bigrip3): warning = -3
elif E.le(density, parameters.bigrip2): warning = -2
elif E.le(density, parameters.bigrip1): warning = -1
apocalypse.warning = warning
target.apocalypse = apocalypse
if E.eq(warning, 3):
start(target, 3)
elif E.eq(warning, -3):
start(target, -3)
func start(target: Dictionary, event_type: int) -> void:
var apocalypse: Dictionary = target.get("apocalypse", defaults()).duplicate(true)
if E.gt(apocalypse.phase, 0): return
apocalypse.phase = 1
apocalypse.type = event_type
apocalypse.stage = 1
apocalypse.tick = 0
apocalypse.end_ticks = 0
target.apocalypse = apocalypse
target.speed = 1
target.speed_in_year = 1
_schedule = _make_schedule("SMBH" if event_type > 0 else "S")
func process_tick(target: Dictionary) -> Dictionary:
var result: Dictionary = {"screen": -1, "finished": false}
var apocalypse: Dictionary = target.get("apocalypse", defaults())
if E.ne(apocalypse.get("phase", 0), 1): return result
if _schedule.is_empty(): _schedule = _make_schedule("SMBH" if E.gt(apocalypse.type, 0) else "S")
apocalypse.tick = E.add(apocalypse.tick, 1)
var stage_rows: Array = []
for row: Dictionary in _schedule:
if E.eq(row.stage, apocalypse.stage): stage_rows.append(row)
if stage_rows.is_empty():
var next_stage: Variant = INF
for row: Dictionary in _schedule:
if E.gt(row.stage, apocalypse.stage): next_stage = E.minimum(next_stage, row.stage)
if E.finite(next_stage):
apocalypse.stage = E.integer(next_stage)
apocalypse.tick = 0
else:
_process_final_energy(target, apocalypse)
if E.eq(apocalypse.end_ticks, END_WAIT_TICKS):
apocalypse.phase = 2
result.finished = true
target.apocalypse = apocalypse
return result
var stage_complete: bool = true
for row: Dictionary in stage_rows:
if E.gt(row.tick, apocalypse.tick):
stage_complete = false
continue
var id: String = row.id
if not world.catalog.entities.has(id): continue
if E.gt(world.ledger.physical_amount(target, id), 0):
stage_complete = false
result.screen = int(row.screen)
_destroy_fraction(target, id)
if stage_complete:
var next_stage: Variant = INF
for row: Dictionary in _schedule:
if E.gt(row.stage, apocalypse.stage): next_stage = E.minimum(next_stage, row.stage)
if E.finite(next_stage):
apocalypse.stage = E.integer(next_stage)
apocalypse.tick = 0
else:
_process_final_energy(target, apocalypse)
if E.eq(apocalypse.end_ticks, END_WAIT_TICKS):
apocalypse.phase = 2
result.finished = true
target.apocalypse = apocalypse
world.ledger.refresh_groups(target)
world.ledger.clamp_auto_resources(target)
world.presentation.refresh(target, world.catalog)
return result
func _make_schedule(skipped_id: String) -> Array:
var rows: Array = Global.ApocalypseQueue.values()
rows.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return int(a.Num) < int(b.Num))
var result: Array = []
var stage: int = -1
var tick: int = 0
for source: Dictionary in rows:
if source.Shortcut == skipped_id: continue
var row_stage: int = int(source.Stage)
if row_stage != stage:
stage = row_stage
tick = 0
tick += int(source.Tick)
result.append({"id": str(source.Shortcut), "stage": row_stage, "screen": int(source.Screen), "tick": tick})
return result
func _destroy_fraction(target: Dictionary, id: String) -> void:
var entity: OwniverseEntity = world.catalog.entities[id]
var physical: Variant = world.ledger.physical_amount(target, id)
if E.le(physical, 0): return
var amount: Variant = E.round_value(E.divide(physical, 2)) if E.gt(physical, 5) else (1 if E.ge(physical, 1) else physical)
amount = E.minimum(physical, E.maximum(0, amount))
if E.le(amount, 0): return
var fraction: Variant = E.clamp_value(E.divide(amount, physical), 0, 1)
if E.gt(entity.destroy_level, 0): world.stellar.destroy_planets(target, id, fraction, entity.destroy_level, "apocalypse")
var products: Dictionary = world.stellar.evolution_products(target, entity, amount, {"explosiveness": WorldParameters.effective(target, "Exp")})
var normalized: Dictionary = {}
for output in products:
normalized[output] = E.divide(products[output], amount)
var incoming: Dictionary = world.ledger.transfer_evolution(target, {id: fraction}, {id: normalized})
world.batches.scale_batches(target, id, E.subtract(1, fraction))
var terminal: String = "SMBH" if E.gt(target.apocalypse.type, 0) else "S"
var terminal_yield: Variant = 0
var new_owners: Dictionary = {}
if normalized.is_empty():
terminal_yield = E.multiply(amount, E.add(1, E.maximum(0, entity.mass))) if E.contains(["Star", "BH"], entity.group_as) else amount
for output in normalized:
if E.eq(output, "blast"):
var environment: Dictionary = world.environment.stellar_environment(target)
world.stellar.apply_blast(target, E.multiply(normalized[output], amount), environment)
continue
if not world.catalog.entities.has(output): continue
var output_amount: Variant = E.maximum(0, products[output])
if E.le(output_amount, 0): continue
output_amount = world.ledger.credit(target, output, output_amount)
var output_entity: OwniverseEntity = world.catalog.entities[output]
if E.gt(output_entity.lifespan, 0) or not output_entity.life_rule.is_empty():
world.batches.append_batch(target, output, output_amount, target.time.datetime, target.time.datetime)
new_owners[output] = E.add(new_owners.get(output, 0), output_amount)
world.ledger.accept_transfers(target, incoming)
world.ledger.bind_new_owners(target, new_owners)
if E.gt(terminal_yield, 0): world.ledger.credit(target, terminal, terminal_yield)
world.ledger.round_object_allocations(target)
func _process_final_energy(target: Dictionary, apocalypse: Dictionary) -> void:
var energy: Variant = E.maximum(0, target.energy)
if E.gt(energy, 0):
var amount: Variant = E.round_value(E.divide(energy, 2)) if E.gt(energy, 5) else (1 if E.ge(energy, 1) else energy)
amount = E.minimum(energy, amount)
target.energy = E.maximum(0, E.subtract(energy, amount))
world.ledger.credit(target, "SMBH" if E.gt(apocalypse.type, 0) else "S", amount)
apocalypse.end_ticks = 0
else:
apocalypse.end_ticks = int(apocalypse.end_ticks) + 1
@@ -0,0 +1 @@
uid://u5hs757qtn8c
+1
View File
@@ -14,6 +14,7 @@ func yearly_products(entity: OwniverseEntity) -> Dictionary:
func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Dictionary, distribution: Dictionary, from_year: Variant, to_year: Variant) -> void:
if E.gt(target.get("apocalypse", {}).get("phase", 0), 0): return
var basic: Dictionary = {}
var automatic: Dictionary = {}
for id in quantities:
+1
View File
@@ -72,6 +72,7 @@ func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefC
var profile_trends: int = world.profile_start()
WorldParameters.advance(previous, next, Global.CivilizationRules, E.subtract(to_year, from_year))
world.profile_end("trends", profile_trends)
world.apocalypse.check_density(next)
next.time = clock if not clock.is_empty() else {"datetime": datetime, "year": to_year}
# Publish only after every phase has finished; previous stays untouched.
+7 -1
View File
@@ -112,6 +112,7 @@ func plan_smbh(desired_amount: Variant) -> Dictionary:
func produce_smbh(desired_amount: Variant) -> Variant:
if E.gt(world.state.get("apocalypse", {}).get("phase", 0), 0): return 0
var plan: Dictionary = plan_smbh(desired_amount)
var consumed_mass: Variant = plan.amount
var taken: Dictionary = plan.taken
@@ -137,10 +138,12 @@ func produce_smbh(desired_amount: Variant) -> Variant:
world.ledger.clamp_auto_resources(next)
world.state = next
world.presentation.refresh(world.state, world.catalog)
world.apocalypse.check_density(world.state)
return consumed_mass
func produce(item_id: String, desired_amount: Variant) -> Variant:
if E.gt(world.state.get("apocalypse", {}).get("phase", 0), 0): return 0
if E.eq(item_id, "SMBH"):
return produce_smbh(desired_amount)
# Use special production for S and H
@@ -171,10 +174,12 @@ func produce(item_id: String, desired_amount: Variant) -> Variant:
world.stellar.create_planets(world.state, item_id, max_producible, world.state.time.datetime, world.state.time.datetime, metallicity)
world.ledger.refresh_groups(world.state)
world.presentation.refresh(world.state, world.catalog)
world.apocalypse.check_density(world.state)
return max_producible
func produce_hs(entity_id: String, value: Variant) -> Variant:
if E.gt(world.state.get("apocalypse", {}).get("phase", 0), 0): return 0
if E.le(value, 0):
return 0
@@ -186,7 +191,8 @@ func produce_hs(entity_id: String, value: Variant) -> Variant:
world.state.energy = E.subtract(world.state.energy, to_produce)
world.ledger.process_entity(entity_id, to_produce)
world.presentation.refresh(world.state, world.catalog)
world.apocalypse.check_density(world.state)
return to_produce
+11 -2
View File
@@ -12,6 +12,7 @@ var ledger = preload("res://world/owniverse/object_ledger.gd").new(self)
var production = preload("res://world/owniverse/manual_production.gd").new(self)
var automatic = preload("res://world/owniverse/automatic_production.gd").new(self)
var stellar = preload("res://world/owniverse/stellar_rules.gd").new(self)
var apocalypse = preload("res://world/owniverse/apocalypse_controller.gd").new(self)
var life = preload("res://world/owniverse/life_evolution.gd").new(self)
var environment = preload("res://world/owniverse/universe_environment.gd").new(self)
var evolution = preload("res://world/owniverse/evolution_coordinator.gd").new(self)
@@ -109,6 +110,7 @@ var state: Dictionary = {
'civilization_stats': {},
'civilization_homes': {},
'ethics': {},
'apocalypse': {'phase': 0, 'type': 0, 'warning': 0, 'stage': 1, 'tick': 0, 'end_ticks': 0},
'stats': {'total_amount': {}},
}
@@ -132,6 +134,10 @@ func display_clock() -> Dictionary:
## Keep the requested clock advancing while a transaction spans render frames.
func evolute_frame(delta: float, tree: SceneTree) -> void:
if not is_finite(delta) or delta < 0.0 or not is_finite(delta * pow(10.0, speed - 1)): return
if E.ge(state.get("apocalypse", {}).get("phase", 0), 2):
frame_clock = state.time.duplicate()
frame_elapsed_seconds = 0.0
return
if frame_clock.is_empty(): frame_clock = state.time.duplicate()
frame_clock = SimulationTime.advance(frame_clock, delta, speed)
frame_elapsed_seconds += delta
@@ -152,6 +158,7 @@ func evolute_frame(delta: float, tree: SceneTree) -> void:
evolution.run_frame(frame_active_clock, frame_budget)
func request_production(id: String, amount: Variant, resource: bool = false) -> bool:
if E.gt(state.get("apocalypse", {}).get("phase", 0), 0): return false
if frame_busy or production_queue_busy:
_queued_productions.append({"id": id, "amount": amount, "resource": resource})
return true
@@ -218,10 +225,12 @@ var speed: int :
get:
return _requested_speed if _requested_speed != 0 else state.speed
set(value):
var requested_speed: int = clampi(value, 1, Global.SPEED_LIMIT)
if E.eq(state.get("apocalypse", {}).get("phase", 0), 1): requested_speed = 1
if frame_busy:
_requested_speed = clampi(value, 1, Global.SPEED_LIMIT)
_requested_speed = requested_speed
return
state.speed = clampi(value, 1, Global.SPEED_LIMIT)
state.speed = requested_speed
state.speed_in_year = E.power(10, E.subtract(state.speed, 1))
func evolute(delta: Variant) -> void: