From 3047ef5a85844db8f165b26ae0fc55788fa5f169 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sun, 18 Oct 2026 00:05:53 +0300 Subject: [PATCH] Implement apocalypse mechanics: add apocalypse queue, controller, and integrate into production logic --- autoload/global.gd | 2 + autoload/save_manager.gd | 10 +- data/owniverse_apocalypse_queue_loader.gd | 144 +++++++++++++++ data/owniverse_apocalypse_queue_loader.gd.uid | 1 + simulation_rules.md | 6 +- stage/stage.gd | 44 ++++- world/owniverse/apocalypse_controller.gd | 169 ++++++++++++++++++ world/owniverse/apocalypse_controller.gd.uid | 1 + world/owniverse/automatic_production.gd | 1 + world/owniverse/evolution_coordinator.gd | 1 + world/owniverse/manual_production.gd | 8 +- world/owniverse/owniverse.gd | 13 +- 12 files changed, 393 insertions(+), 7 deletions(-) create mode 100644 data/owniverse_apocalypse_queue_loader.gd create mode 100644 data/owniverse_apocalypse_queue_loader.gd.uid create mode 100644 world/owniverse/apocalypse_controller.gd create mode 100644 world/owniverse/apocalypse_controller.gd.uid diff --git a/autoload/global.gd b/autoload/global.gd index 2c2b0c8..f34243e 100644 --- a/autoload/global.gd +++ b/autoload/global.gd @@ -30,6 +30,7 @@ var Templates: Dictionary = {} var LifeRules: Dictionary = {} var CivilizationRules: Dictionary = {} var ExtraParams: Dictionary = {} +var ApocalypseQueue: Dictionary = {} var Music: Dictionary = {} var Effects: Dictionary = {} @@ -92,6 +93,7 @@ func _ready() -> void: OwniverseItems = OwniverseItemLoader.get_data() Templates = OwniverseTemplateLoader.get_data() CivilizationRules = OwniverseCivilizationRuleLoader.get_data() + ApocalypseQueue = ApocalypseQueueLoader.get_data() Music = OwniverseAudioLoader.get_music() Effects = OwniverseAudioLoader.get_effects() diff --git a/autoload/save_manager.gd b/autoload/save_manager.gd index 693723f..fe4e08b 100644 --- a/autoload/save_manager.gd +++ b/autoload/save_manager.gd @@ -250,8 +250,16 @@ static func _validate_payload(data: Variant, speed_limit: int, stages: Array, en # Optional additions to the current format. Saves predating indicators keep # their quantities and receive neutral defaults from Owniverse on load. for field in saved: - if (not E.contains(required, field) and not E.contains(["world_parameters", "trends"], field)): + if (not E.contains(required, field) and not E.contains(["world_parameters", "trends", "apocalypse"], field)): return "unsupported state structure" + if saved.has("apocalypse"): + var apocalypse: Variant = saved.apocalypse + if not apocalypse is Dictionary: return "invalid state.apocalypse" + for key in ["phase", "stage", "tick", "end_ticks"]: + if not _number(apocalypse.get(key), true): return E.add("invalid state.apocalypse.", key) + for key in ["type", "warning"]: + if not _number(apocalypse.get(key), true, false): return E.add("invalid state.apocalypse.", key) + if E.lt(apocalypse.phase, 0) or E.gt(apocalypse.phase, 2): return "invalid apocalypse phase" if saved.has("world_parameters"): if ((not _number_map(saved.world_parameters)) or E.ne(saved.world_parameters.size(), WorldParameters.defaults().size())): return "invalid state.world_parameters" diff --git a/data/owniverse_apocalypse_queue_loader.gd b/data/owniverse_apocalypse_queue_loader.gd new file mode 100644 index 0000000..8a31348 --- /dev/null +++ b/data/owniverse_apocalypse_queue_loader.gd @@ -0,0 +1,144 @@ +class_name ApocalypseQueueLoader + +static func get_data() -> Dictionary: + return CSVLoader.load(DATA) + +#region Owniverse Items + +const DATA := """ +Num|Shortcut|Stage|Screen|Tick|Desc +1|LUB|1|3|50|Вселенский пузырь +2|LGW|1|3|5|Великая стена +3|LGF|1|3|5|Галактическая нить +4|LSC|1|3|5|Сверхскопление +5|LGG|1|3|5|Группа галактик +6|GSG|2|3|5|Гигантская спиральная галактика +7|GSL|2|3|5|Большая спиральная галактика +8|GSM|2|3|5|Средняя спиральная галактика +9|GSS|2|3|5|Малая спиральная галактика +10|GSD|2|3|5|Карликовая спиральная галактика +11|GEG|2|3|5|Гигантская эллиптическая галактика +12|GEL|2|3|5|Большая эллиптическая галактика +13|GEM|2|3|5|Средняя эллиптическая галактика +14|GES|2|3|5|Малая эллиптическая галактика +15|GED|2|3|5|Карликовая эллиптическая галактика +16|SCG|2|3|5|Гигантское звёздное скопление +17|SCL|2|3|5|Большое звёздное скопление +18|SCM|2|3|5|Среднее звёздное скопление +19|SCS|2|3|5|Малое звёздное скопление +20|SCD|2|3|5|Рассеянное звёздное скопление +21|C4.4|3|4|20|Цивилизация 4.4 +22|C4.3|3|4|0|Цивилизация 4.3 +23|C4.2|3|4|0|Цивилизация 4.2 +24|C4.1|3|4|0|Цивилизация 4.1 +25|C4.0|3|4|0|Цивилизация 4.0 +26|C4|3|4|0|Цивилизация типа 4 +27|C3.9|4|4|5|Цивилизация 3.9 +28|C3.8|4|4|0|Цивилизация 3.8 +29|C3.7|4|4|0|Цивилизация 3.7 +30|C3.6|4|4|0|Цивилизация 3.6 +31|C3.5|4|4|0|Цивилизация 3.5 +32|C3.4|4|4|0|Цивилизация 3.4 +33|C3.3|4|4|0|Цивилизация 3.3 +34|C3.2|4|4|0|Цивилизация 3.2 +35|C3.1|4|4|0|Цивилизация 3.1 +36|C3.0|4|4|0|Цивилизация 3.0 +37|C3|4|4|0|Цивилизация типа 3 +38|C2.9|5|4|5|Цивилизация 2.9 +39|C2.8|5|4|0|Цивилизация 2.8 +40|C2.7|5|4|0|Цивилизация 2.7 +41|C2.6|5|4|0|Цивилизация 2.6 +42|C2.5|5|4|0|Цивилизация 2.5 +43|C2.4|5|4|0|Цивилизация 2.4 +44|C2.3|5|4|0|Цивилизация 2.3 +45|C2.2|5|4|0|Цивилизация 2.2 +46|C2.1|5|4|0|Цивилизация 2.1 +47|C2.0|5|4|0|Цивилизация 2.0 +48|C2|5|4|0|Цивилизация типа 2 +49|HG|6|2|5|Гипергигант +50|SG|6|2|5|Сверхгигант +51|BG|6|2|5|Голубой гигант +52|YG|6|2|5|Жёлтый гигант +53|RHG|6|2|5|Красный гипергигант +54|RSG|6|2|5|Красный сверхгигант +55|WYD|6|2|5|Бело-жёлтый карлик +56|YD|6|2|5|Жёлтый карлик +57|OD|6|2|5|Оранжевый карлик +58|RD|6|2|5|Красный карлик +59|HRG|6|2|5|Огромный красный гигант +60|LRG|6|2|0|Большой красный гигант +61|MRG|6|2|0|Средний красный гигант +62|NRG|6|2|0|Обычный Красный гигант +64|SRG|6|2|0|Малый красный гигант +63|RG|6|2|0|Красный гигант +65|HN|6|2|0|Гиперновая +66|BSN|6|2|0|Яркая сверхновая +67|SN|6|2|0|Сверхновая +68|DSN|6|2|0|Тусклая сверхновая +69|HGRB|6|2|0|Гипер гамма-всплеск +70|LGRB|6|2|0|Большой гамма-всплеск +71|GRB|6|2|0|Гамма-всплеск +72|SGRB|6|2|0|Малый гамма-всплеск +73|ASTN|6|2|5|Активная звёздная колыбель +74|MC|6|2|5|Молекулярное облако +75|STN|6|2|5|Звёздная колыбель +76|G|6|2|5|Глобула +77|N|6|2|5|Туманность +78|HBD|7|2|5|Горячий бурый карлик +79|BD|7|2|0|Бурый карлик +80|LWD|7|2|0|Большой белый карлик +81|NWD|7|2|0|Обычный белый карлик +82|SWD|7|2|0|Малый белый карлик +83|TWD|7|2|0|Крошечный белый карлик +84|WD|7|2|0|Белый карлик +85|CBD|7|2|0|Холодный бурый карлик +86|MGT|7|2|0|Магнетар +87|LNS|7|2|0|Большая нейтронная звезда +88|SNS|7|2|0|Малая нейтронная звезда +89|NS|7|2|5|Нейтронная звезда +90|BH60|7|2|5|Чёрная дыра 60 +91|BH40|7|2|0|Чёрная дыра 40 +92|BH30|7|2|0|Чёрная дыра 30 +93|BH25|7|2|0|Чёрная дыра 25 +94|BH|7|2|0|Чёрная дыра +95|C1.9|8|4|5|Цивилизация 1.9 +96|C1.8|8|4|0|Цивилизация 1.8 +97|C1.7|8|4|0|Цивилизация 1.7 +98|C1.6|8|4|0|Цивилизация 1.6 +99|C1.5|8|4|0|Цивилизация 1.5 +100|C1.4|8|4|0|Цивилизация 1.4 +101|C1.3|8|4|0|Цивилизация 1.3 +102|C1.2|8|4|0|Цивилизация 1.2 +103|C1.1|8|4|0|Цивилизация 1.1 +104|C1.0|8|4|0|Цивилизация 1.0 +105|C1|8|4|0|Цивилизация типа 1 +106|C0.9|8|4|5|Цивилизация 0.9 +107|C0.8|8|4|0|Цивилизация 0.8 +108|C0.7|8|4|0|Цивилизация 0.7 +109|C0.6|8|4|0|Цивилизация 0.6 +110|C0.5|8|4|0|Цивилизация 0.5 +111|C0.4|8|4|0|Цивилизация 0.4 +112|C0.3|8|4|0|Цивилизация 0.3 +113|C0.2|8|4|0|Цивилизация 0.2 +114|C0.1|8|4|0|Цивилизация 0.1 +115|C0.0|8|4|0|Цивилизация 0.0 +116|C0|8|4|0|Цивилизация типа 0 +117|EON5|8|4|5|Эон 5: Кайнозой +118|EON4|8|4|0|Эон 4: Мезозой +119|EON3|8|4|0|Эон 3: Палеозой +120|EON2|8|4|0|Эон 2: Протерозой +121|EON1|8|4|0|Эон 1: Архей +122|LP|9|4|5|Живая +123|HP|9|4|0|Обитаемая +124|RP|9|4|5|Каменистая +125|AB|9|4|0|Астероиды +126|GG|9|4|0|Газовый гигант +127|Met|10|1|5|Металлы +128|He|10|1|0|Гелий +129|H|11|1|5|Водород +130|SMBH|12|2|5|СМЧД +131|S|12|1|5|Пространство +""" + + +#endregion diff --git a/data/owniverse_apocalypse_queue_loader.gd.uid b/data/owniverse_apocalypse_queue_loader.gd.uid new file mode 100644 index 0000000..63dd845 --- /dev/null +++ b/data/owniverse_apocalypse_queue_loader.gd.uid @@ -0,0 +1 @@ +uid://8bh387weuy7a diff --git a/simulation_rules.md b/simulation_rules.md index a96f511..bf77554 100644 --- a/simulation_rules.md +++ b/simulation_rules.md @@ -177,9 +177,11 @@ Ct не получает часы. Инициализация отсутству - `1e-5 < density < 0.9`: обычная шкала, `ln(density × 1e5) / ln(9e4)`; - `density ≥ 0.9`: шкала сжатия, `(density − 0.9) × 10`. -В Unity в первой формуле было `−7`; это ошибка, не позволяющая указателю двигаться. Здесь исправлено на `+7`. Переключение шкалы меняет и заголовок/описание индикатора. Само наступление апокалипсиса индикатор не запускает. +В Unity в первой формуле было `−7`; это ошибка, не позволяющая указателю двигаться. Здесь исправлено на `+7`. Переключение шкалы меняет и заголовок/описание индикатора. -Общая рамка `Background/WarningBorder` получает цвета Unity `WarningUI` в формате RGBA: при плотности ≤1e-5 — `C040407F`, ≤1e-6 — `DF2020BF`, ≤1e-7 — `FF0000FF`; при плотности ≥0,9 — `40A0C07F`, ≥0,97 — `20B0DFBF`, ≥1 — `00C0FFFF`. В нормальном диапазоне рамка прозрачна. Она обновляется вместе с индикаторами, не перехватывает нажатия и снимается при восстановлении плотности. Необратимая последовательность апокалипсиса Unity сюда не входит. +Общая рамка `Background/WarningBorder` получает цвета Unity `WarningUI` в формате RGBA: при плотности ≤1e-5 — `C040407F`, ≤1e-6 — `DF2020BF`, ≤1e-7 — `FF0000FF`; при плотности ≥0,9 — `40A0C07F`, ≥0,97 — `20B0DFBF`, ≥1 — `00C0FFFF`. В нормальном диапазоне рамка прозрачна. Она обновляется вместе с индикаторами и не перехватывает нажатия. + +Пороговые состояния управляют запуском апокалипсиса: при `density ≤ bigrip3` начинается Big Rip, при `density ≥ bigcrunch3` — Big Crunch. Последовательность берётся из `Global.ApocalypseQueue`, сортируется по `Num` и использует указанные `Stage`, `Screen` и `Tick`. Каждые 0,1 секунды активные объекты выбранной страницы теряют половину количества (для остатков до 5 — по одному). На время последовательности скорость фиксируется на 1, ручное и автоматическое создание блокируется, а экран переключается по очереди. Big Rip пропускает `S`, Big Crunch — `SMBH`; остаток энергии постепенно переводится в соответствующий объект. После 20 тиков с нулевой энергией апокалипсис получает фазу завершения, симуляция останавливает время. Фаза, тип события, предупреждение и позиция в очереди сохраняются в состоянии вселенной. ### Горючесть, взрывчатость и агрессия diff --git a/stage/stage.gd b/stage/stage.gd index d7f4f35..d6baaf4 100644 --- a/stage/stage.gd +++ b/stage/stage.gd @@ -100,6 +100,12 @@ var active_actions_dirty: bool = true var active_actions_elapsed: float = 0.0 var clock_display_elapsed: float = 0.0 var interface_sounds_enabled: bool = false +var apocalypse_elapsed: float = 0.0 +var apocalypse_ui_phase: int = -1 +var apocalypse_screen: int = -1 +@onready var time_minus_button: TextureButton = $World/TopMenu/TimeMinus +@onready var time_plus_button: TextureButton = $World/TopMenu/TimePlus +@onready var apocalypse_music: OwniverseMusicPlayer = $"../MusicPlayer" var owniverse:Owniverse = Owniverse.new() var owniverse_items: Dictionary[String, OwniverseItem] = {} @@ -274,6 +280,7 @@ func _swipe_to_nearest_page() -> void: func _input(event: InputEvent) -> void: + if E.eq(owniverse.state.get("apocalypse", {}).get("phase", 0), 1): return if event is InputEventScreenDrag: #effects_player.play_effect("swipe") stage_page_scroller.scroll_horizontal = clamp( @@ -299,7 +306,7 @@ func _set_active_page_actions() -> void: #var amount = owniverse.get_available_to_produce(item_id) #var amount = owniverse.get_available_to_produce(entity) - var amount = owniverse.production.calculate_max_producible(entity) + var amount = (0 if E.gt(owniverse.state.get("apocalypse", {}).get("phase", 0), 0) else owniverse.production.calculate_max_producible(entity)) #print(item_id, " : ", amount) if item_id == "SMBH": var preview: Callable = func(requested): @@ -398,6 +405,7 @@ func _refresh_universe_ui() -> void: func _process(delta: float) -> void: owniverse.evolute_frame(delta, get_tree()) + _process_apocalypse(delta) clock_display_elapsed += delta if clock_display_elapsed >= 0.1: clock_display_elapsed = fmod(clock_display_elapsed, 0.1) @@ -431,6 +439,7 @@ func _process(delta: float) -> void: func _change_speed(delta: int) -> void: + if E.eq(owniverse.state.get("apocalypse", {}).get("phase", 0), 1) and delta != 0: return var previous_speed: int = owniverse.speed owniverse.speed = owniverse.speed + delta @@ -458,6 +467,39 @@ func _change_speed(delta: int) -> void: g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed + 1)) +func _process_apocalypse(delta: float) -> void: + var apocalypse: Dictionary = owniverse.state.get("apocalypse", {}) + var phase: int = int(apocalypse.get("phase", 0)) + if phase != apocalypse_ui_phase: + apocalypse_ui_phase = phase + if phase == 1: + apocalypse_music.play_apocalypse() + owniverse.speed = 1 + _change_speed(0) + elif phase == 2: + owniverse.speed = 1 + _change_speed(0) + time_minus_button.disabled = phase == 1 + time_plus_button.disabled = phase == 1 + active_actions_dirty = true + if phase == 1: + _set_active_page_actions() + if phase != 1: + apocalypse_elapsed = 0.0 + return + apocalypse_elapsed += delta + if owniverse.frame_busy or owniverse.production_queue_busy: + return + while apocalypse_elapsed >= OwniverseApocalypseController.TICK_SECONDS and E.eq(owniverse.state.apocalypse.phase, 1): + apocalypse_elapsed -= OwniverseApocalypseController.TICK_SECONDS + var result: Dictionary = owniverse.apocalypse.process_tick(owniverse.state) + if int(result.screen) >= 0 and int(result.screen) != apocalypse_screen: + apocalypse_screen = int(result.screen) + stage_page_scroller.scroll_horizontal = apocalypse_screen * PAGE_WIDTH + if bool(result.finished): + _change_speed(0) + + func _on_topmenu_button_pressed(screen_name: String) -> void: var current_screen: String = "" for screen_id in screens: diff --git a/world/owniverse/apocalypse_controller.gd b/world/owniverse/apocalypse_controller.gd new file mode 100644 index 0000000..017c0a6 --- /dev/null +++ b/world/owniverse/apocalypse_controller.gd @@ -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 + diff --git a/world/owniverse/apocalypse_controller.gd.uid b/world/owniverse/apocalypse_controller.gd.uid new file mode 100644 index 0000000..75ca217 --- /dev/null +++ b/world/owniverse/apocalypse_controller.gd.uid @@ -0,0 +1 @@ +uid://u5hs757qtn8c diff --git a/world/owniverse/automatic_production.gd b/world/owniverse/automatic_production.gd index 2a50065..f580c13 100644 --- a/world/owniverse/automatic_production.gd +++ b/world/owniverse/automatic_production.gd @@ -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: diff --git a/world/owniverse/evolution_coordinator.gd b/world/owniverse/evolution_coordinator.gd index 2ec1a33..9a7f50d 100644 --- a/world/owniverse/evolution_coordinator.gd +++ b/world/owniverse/evolution_coordinator.gd @@ -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. diff --git a/world/owniverse/manual_production.gd b/world/owniverse/manual_production.gd index 874e16d..a4de79e 100644 --- a/world/owniverse/manual_production.gd +++ b/world/owniverse/manual_production.gd @@ -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 diff --git a/world/owniverse/owniverse.gd b/world/owniverse/owniverse.gd index 69c9bd6..703eb78 100644 --- a/world/owniverse/owniverse.gd +++ b/world/owniverse/owniverse.gd @@ -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: