Add world indicators and parameters to enhance universe simulation

- new universe creation function.
This commit is contained in:
Kirill
2026-09-28 22:19:13 +03:00
parent e79f3fa362
commit 15ee7833ed
14 changed files with 522 additions and 27 deletions
+19 -1
View File
@@ -7,6 +7,7 @@ const SAVE_PATH = "user://settings.cfg"
const FORMAT_VERSION: int = 2 const FORMAT_VERSION: int = 2
const SimulationTime = preload("res://world/owniverse/simulation_time.gd") const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
const STATE_VERSION: String = SimulationTime.STATE_VERSION const STATE_VERSION: String = SimulationTime.STATE_VERSION
var last_error: String = "" var last_error: String = ""
var blocked_saves: Dictionary = {} var blocked_saves: Dictionary = {}
@@ -229,8 +230,25 @@ static func _validate_payload(data: Variant, speed_limit: int, stages: Array) ->
"planet_remainders", "blast_remainders", "destruction_remainders", "civilization_remainders", "planet_remainders", "blast_remainders", "destruction_remainders", "civilization_remainders",
"civilization_stats", "civilization_homes", "ethics", "stats", "civilization_stats", "civilization_homes", "ethics", "stats",
] ]
if saved.size() != required.size(): # 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 field not in required and field not in ["world_parameters", "trends"]:
return "unsupported state structure" return "unsupported state structure"
if saved.has("world_parameters"):
if not _number_map(saved.world_parameters) or saved.world_parameters.size() != WorldParameters.defaults().size():
return "invalid state.world_parameters"
for key in WorldParameters.defaults():
if not saved.world_parameters.has(key) or saved.world_parameters[key] < 1.0:
return "invalid world parameter " + key
if key in ["Bur", "Exp", "Agg"] and saved.world_parameters[key] > 19.0:
return "invalid world scale " + key
if saved.has("trends"):
if not _number_map(saved.trends) or saved.trends.size() != WorldParameters.TRENDS.size():
return "invalid state.trends"
for key in WorldParameters.TRENDS:
if not saved.trends.has(key) or saved.trends[key] >= 1.0:
return "invalid trend " + key
for field in required: for field in required:
if not saved.has(field): if not saved.has(field):
return "missing state." + field return "missing state." + field
+55 -3
View File
@@ -72,7 +72,7 @@ GI не хранит накопленные счётчики первичной
- `density = 1.23456e-7 * sqrt(B + M) / max(1, V)^(1/6)`. - `density = 1.23456e-7 * sqrt(B + M) / max(1, V)^(1/6)`.
- `blast_factor = clamp(free_S / V, 0, 1)`; при нулевом объёме `1`. - `blast_factor = clamp(free_S / V, 0, 1)`; при нулевом объёме `1`.
Система сил Unity и планетные бонусы в GI пока не перенесены; здесь используются нейтральные множители 1. Металличность для производства фиксируется до расходования ресурсов на звёзды. Среда бласта фиксируется для поколения до добавления его продуктов. Планетные бонусы сил пока не перенесены. Параметры мира Bur/Exp/Agg имеют собственные сохраняемые шкалы и эффективные множители (см. раздел индикаторов ниже). Металличность для производства фиксируется до расходования ресурсов на звёзды. Среда бласта фиксируется для поколения до добавления его продуктов.
## Разрушение и бласты ## Разрушение и бласты
@@ -102,7 +102,7 @@ GI не хранит накопленные счётчики первичной
Прогресс и деградация округляются вниз с сохранением остатков. Если оба накопленных исхода конкурируют за последнюю планету, распределяются только существующие объекты, а неиспользованный остаток переносится на следующую попытку. Деградация распределяется по весам `fall`; знаковые ошибки распределения также сохраняются. Прогресс и деградация округляются вниз с сохранением остатков. Если оба накопленных исхода конкурируют за последнюю планету, распределяются только существующие объекты, а неиспользованный остаток переносится на следующую попытку. Деградация распределяется по весам `fall`; знаковые ошибки распределения также сохраняются.
Для всех шести стадий базовые доли за попытку — `progress=0.00025`, `degradation=0.0025`. Интервалы HP/EON1/EON2/EON3/EON4/EON5: 5 млн, 10 млн, 10 млн, 1 млн, 1 млн, 500 тыс. лет. Используются постепенные окна GI шириной `span / 40`. Случайные множители Unity не применяются; агрессия, порядок и просвещение пока имеют нейтральное влияние. Для всех шести стадий базовые доли за попытку — `progress=0.00025`, `degradation=0.0025`. Интервалы HP/EON1/EON2/EON3/EON4/EON5: 5 млн, 10 млн, 10 млн, 1 млн, 1 млн, 500 тыс. лет. Используются постепенные окна GI шириной `span / 40`. Случайные множители Unity не применяются. Агрессия, порядок и просвещение изменяют вероятности по формулам из раздела индикаторов ниже.
HP деградирует в RP. У эонов возможен откат к предыдущим стадиям по таблице. RP не развивается сам. C0.0 и C0.1 также развиваются: период 2000 лет, прогресс 0.0175, деградация 0.025. Переходы C0.0 → C0.1 → C0.2, откат C0.0 → EON5 и C0.1 → C0.0. Развитие C0.2 и выше подключено через этические правила. Конфликт civ_bonus устранён повторным импортом: бонусы структур совпадают с Unity. Для существующих этических цивилизаций пользователь выбрал равномерное распределение между шестью этиками с детерминированным целым остатком. HP деградирует в RP. У эонов возможен откат к предыдущим стадиям по таблице. RP не развивается сам. C0.0 и C0.1 также развиваются: период 2000 лет, прогресс 0.0175, деградация 0.025. Переходы C0.0 → C0.1 → C0.2, откат C0.0 → EON5 и C0.1 → C0.0. Развитие C0.2 и выше подключено через этические правила. Конфликт civ_bonus устранён повторным импортом: бонусы структур совпадают с Unity. Для существующих этических цивилизаций пользователь выбрал равномерное распределение между шестью этиками с детерминированным целым остатком.
@@ -144,4 +144,56 @@ HP деградирует в RP. У эонов возможен откат к п
Размещение выбирает структуры в порядке убывания `civ_bonus`, начиная с соответствующего стадии `claim`. Прямое размещение ограничено числом структур; остальные разрешённые структуры при наличии дают общий бонус, как в Unity. Период новых партий равен `span / средневзвешенный civ_bonus`, для неразмещённой части множитель равен 1. Уже назначенные сроки партий не переписываются при изменении бонуса. Размещение выбирает структуры в порядке убывания `civ_bonus`, начиная с соответствующего стадии `claim`. Прямое размещение ограничено числом структур; остальные разрешённые структуры при наличии дают общий бонус, как в Unity. Период новых партий равен `span / средневзвешенный civ_bonus`, для неразмещённой части множитель равен 1. Уже назначенные сроки партий не переписываются при изменении бонуса.
`life_version = 3` добавляет часы существующим C0.2+ с момента загрузки; старые часы HP, эонов и начальных цивилизаций сохраняются. Ct не получает часы. Миграция выполняется однократно. Глобальные силы и их модификаторы не входят в этот перенос: используются нейтральные множители 1. Ct не получает часы. Инициализация отсутствующих партий жизни выполняется при загрузке и перед продвижением симуляции. Параметры мира и тенденции описаны ниже; интерфейс применения сил в этот перенос не входит.
## Параметры мира, тенденции и индикаторы
Источник формул — Unity `OwniverseInstance`, `Life`, `LifeActions`, `Trends` и классы индикаторов. Расчёты находятся в `world_parameters.gd`; показ — в `ui/indicators/world_indicators.gd`. Шкалы обновляются каждые 0,1 реальной секунды и сразу при создании/загрузке вселенной. Подписи используют текущую локаль и шаблоны.
### Энергия и плотность
Энергия сохраняет экономику GI: запас расходуется на H/S. Заполнение индикатора — `clamp(energy / 1e14, 0, 1)`. Это не коэффициент производства Unity; дополнительное замедление производства при уменьшении запаса не вводится. В описании выводятся остаток и процент.
Плотность берётся из `stellar_environment`. Положение указателя ограничено диапазоном 0…1:
- `density ≤ 1e-5`: шкала разрыва, `(log10(max(density, 1e-7)) + 7) / 2`;
- `1e-5 < density < 0.9`: обычная шкала, `ln(density × 1e5) / ln(9e4)`;
- `density ≥ 0.9`: шкала сжатия, `(density − 0.9) × 10`.
В Unity в первой формуле было `−7`; это ошибка, не позволяющая указателю двигаться. Здесь исправлено на `+7`. Переключение шкалы меняет и заголовок/описание индикатора. Само наступление апокалипсиса индикатор не запускает.
### Горючесть, взрывчатость и агрессия
`world_parameters` хранит Bur, Exp и Agg в диапазоне 1…19, начальное значение — 10. Эффективный множитель `M(x) = 10^((x−10)/9)`: крайние значения 0,1 и 10, нейтральное — 1.
- Bur делит срок жизни новых звёзд на `M(Bur)`. `modify_world_parameter("Bur", delta)` также пересчитывает оставшееся время существующих звёзд с коэффициентом `старый M / новый M`, сохраняя количество и остатки округления. Производство звёзд умножается на `M`, если `M ≤ 1`, иначе на `sqrt(M)`.
- Exp умножает вероятность альтернативного исхода (с ограничением до 1), мощность `entity.blast` и производство событий группы SNGRBB. Локальный `destroy_level` от него не меняется.
- Для Agg обозначим `a = M(Agg)`. Вероятность прогресса умножается на `a`, деградации — на `(1 − (1−a)/20) / Order`, войны — на `1 − (1−a)/5`, колонизации — на `1 − (1−a)/2`. Если сумма прогресса и деградации превышает 1, они пропорционально нормируются. Вероятности войны/колонизации ограничены единицей.
- Для первичной жизни дополнительный множитель прогресса — Enlightenment. Order дополнительно уменьшает потери в войнах и побочный ущерб. Начальные Enlightenment и Order равны 1.
Изменять шкалы следует через `owniverse.modify_world_parameter(key, delta)`, чтобы горючесть корректно обновляла партии. Готовых кнопок применения сил этот метод не добавляет.
### Тенденции
Пять направлений: Antitheism, Integration, Xenocide, Reconstruction, Spirituality. Участвуют космические стадии с непустым `home`; вес первой — 1, каждой следующей — вдвое больше. Для каждой тенденции:
`rate = Σ_стадий (вес стадии × Σ_этик(доля этики × вес этики))`
`прирост = rate × прошедшие игровые годы × 1e-9`.
Доли берутся на начало интервала, после проверки вместимости. Умножение на время исправляет ошибку Unity, где аргумент period не использовался. Накопление само по себе не зависит от FPS; изменение состава цивилизаций внутри крупных шагов остаётся дискретным, как остальная симуляция.
Веса этик в порядке пяти направлений:
| Этика | Антитеизм | Интеграция | Ксеноцид | Реконструкция | Духовность |
|---|---:|---:|---:|---:|---:|
| m | 0 | 2 | 2 | 0 | 6 |
| l | 6 | 1 | 1 | 2 | 0 |
| w | 2 | 0 | 6 | 0 | 2 |
| d | 0 | 6 | 0 | 1 | 3 |
| c | 1 | 3 | 3 | 3 | 0 |
| b | 3 | 0 | 0 | 6 | 1 |
При достижении 1 учитываются все целые срабатывания, дробный остаток сохраняется. Только Integration имеет последствия в исходной Unity: равновероятно выбирается Federation (+0,1 и Agg −1), Enlightenment (+0,2 и Agg −1) или Order (+0,1). Генератор сохраняет своё состояние. До 256 событий за шаг выбор выполняется по одному; для больших количеств используется нормальная аппроксимация мультиномиального распределения с сохранением точного суммарного числа событий, чтобы высокие скорости не блокировали кадр. Она может менять последовательность исходов при другом разбиении больших интервалов.
Federation сохраняется и увеличивается как в Unity, но в исходном коде не применяется к дипломатии. Дополнительное правило для него не придумано. Остальные четыре тенденции имеют счётчики срабатываний и сброс, но не последствия — так же, как в исходной реализации. Параметры и остатки тенденций сохраняются; у существующего сохранения текущего формата без этих полей используются нейтральные значения и нулевые тенденции, без изменения ресурсов или возраста.
+53 -8
View File
@@ -5,6 +5,9 @@ const OWNIVERSE_ITEM = preload("uid://d3xjfxwe3w16v")
const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png" const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png"
const ItemDescription = preload("res://world/owniverse/item_description.gd") const ItemDescription = preload("res://world/owniverse/item_description.gd")
const WorldIndicators = preload("res://ui/indicators/world_indicators.gd")
var world_indicators: RefCounted
var indicator_elapsed: float = 0.0
var description_targets: Dictionary = {} var description_targets: Dictionary = {}
var description_samples: Dictionary = {} var description_samples: Dictionary = {}
var description_rates: Dictionary = {} var description_rates: Dictionary = {}
@@ -317,15 +320,10 @@ func _page_action_pressed(page_id: int, action_amount: float) -> void:
func _ready() -> void: func _ready() -> void:
get_tree().auto_accept_quit = false get_tree().auto_accept_quit = false
var inited_result = init_enitities() world_indicators = WorldIndicators.new(self)
locked_items = inited_result.locked_items create_new_universe()
load_data() load_data()
_init_owniverse_items() _refresh_universe_ui()
owniverse.refresh_item_amounts()
_change_speed(0)
set_datetime(owniverse.state.time.year)
set_owniverse_item_value()
_unlock_items()
g_counters.swipe.sec = 0.2 g_counters.swipe.sec = 0.2
@@ -334,9 +332,56 @@ func _ready() -> void:
page_actions[pa].page_id = pa page_actions[pa].page_id = pa
## Starts a fresh universe. Can be connected directly to a button's pressed signal.
## Saving continues through the normal autosave/close path.
func create_new_universe() -> void:
var previous: Owniverse = owniverse
owniverse = Owniverse.new()
var inited_result: Dictionary = init_enitities()
locked_items = inited_result.locked_items
owniverse.new_state = owniverse.state
for entity in previous.entities.values():
entity.free()
previous.free()
if is_instance_valid(g_tween):
g_tween.kill()
g_active_items.clear()
description_targets.clear()
description_samples.clear()
description_rates.clear()
description_elapsed = 0.0
for label in g_item_descriptions:
label.text = ""
for item in owniverse_items.values():
item.get_parent().remove_child(item)
item.queue_free()
_init_owniverse_items()
for page in range(5, item_pages.size()):
item_pages[page].visible = false
for actions in page_actions.values():
actions.set_actions("", 0.0)
pending_page = RESOURCE_PAGE
_restore_page.call_deferred()
_refresh_universe_ui()
_on_topmenu_button_pressed("World")
func _refresh_universe_ui() -> void:
owniverse.refresh_item_amounts()
_change_speed(0)
set_datetime(owniverse.state.time.year)
set_owniverse_item_value()
_unlock_items()
if world_indicators != null: world_indicators.update(owniverse)
func _process(delta: float) -> void: func _process(delta: float) -> void:
owniverse.evolute(delta) owniverse.evolute(delta)
indicator_elapsed += delta
if indicator_elapsed >= 0.1:
indicator_elapsed = 0.0
world_indicators.update(owniverse)
_update_descriptions(delta) _update_descriptions(delta)
set_owniverse_item_value() set_owniverse_item_value()
set_datetime(owniverse.state.time.year) set_datetime(owniverse.state.time.year)
+8
View File
@@ -1,5 +1,13 @@
# Проверки поэтапного переноса из Owniverse.U # Проверки поэтапного переноса из Owniverse.U
## Параметры мира и индикаторы
```sh
godot --headless --path Owniverse.GI --script res://tests/test_world_indicators.gd
```
Проверяются сроки жизни существующих и новых звёзд при смене горючести, вероятности и мощность взрывов, влияние агрессии/порядка/просвещения на жизнь, накопление тенденций по времени и этикам, несколько событий за шаг, продолжение после сериализации, валидация сохранения, границы шкал и описания на двух языках. Сцена использует фикстуру без доступа к сохранению игрока.
## Прямой расчёт завершившихся сверхновых ## Прямой расчёт завершившихся сверхновых
```sh ```sh
+171
View File
@@ -0,0 +1,171 @@
extends SceneTree
const Parameters = preload("res://world/owniverse/world_parameters.gd")
const Descriptions = preload("res://world/owniverse/item_description.gd")
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks += 1
if not condition:
failures += 1
push_error(message)
func fixture():
var world = load("res://world/owniverse/owniverse.gd").new()
for id in root.get_node("Global").OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
world.entities[id] = entity
world.state.amount[id] = 0.0
world.state.stats.total_amount[id] = 0.0
if entity.group_as != "":
world.state.amount[entity.group_as] = 0.0
world.state.stats.total_amount[entity.group_as] = 0.0
return world
func dispose(world) -> void:
for entity in world.entities.values(): entity.free()
world.free()
func run() -> void:
var global: Node = root.get_node("Global")
var world = fixture()
world.state.amount.RD = 400.0
world._append_batch(world.state, "RD", 400.0, 0.0, 0.0)
var end: float = world.state.batches.RD[0].end_year
world.modify_world_parameter("Bur", 9.0)
check(is_equal_approx(world.state.batches.RD[0].end_year, end / 10.0), "Burnability accelerates existing cohorts")
var count: float = world.state.batches.RD[0].amount
world.modify_world_parameter("Bur", -9.0)
check(is_equal_approx(world.state.batches.RD[0].end_year, end) and world.state.batches.RD[0].amount == count, "Changing burnability back preserves population and lifetime")
world.modify_world_parameter("Bur", 9.0)
world._append_batch(world.state, "YD", 10.0, 0.0, 0.0)
check(is_equal_approx(world.state.batches.YD[0].start_year, world.entities.YD.lifespan / 10.0), "New stars use the same burnability")
world.state.time = {"datetime": end / 10.0 - 0.1, "year": end / 10.0 - 0.1}
world._withdraw_batches(world.state, "RD", world.state.time.year)
world.modify_world_parameter("Bur", -18.0)
check(world.state.batches.RD[0].end_year >= world.state.time.year, "Slowing a partially evolved cohort keeps its remaining window in the future")
world.modify_world_parameter("Exp", 9.0)
check(world._evolution_products(world.state, world.entities.SN, 2.0).blast == 180.0, "Explosion power includes world explosiveness")
var products: Dictionary = world._evolution_products(world.state, world.entities.HRG, 1000.0)
check(products.get("GRB", 0.0) == 100.0 and products.get("SN", 0.0) == 900.0, "Explosiveness raises rare-event probability without duplicating stars")
world.entities.HRG.output_or.chance = 50.0
products = world._evolution_products(world.state, world.entities.HRG, 100.0)
check(products.get("GRB", 0.0) == 100.0 and products.get("SN", 0.0) == 0.0, "Rare-event probability is bounded at one")
dispose(world)
world = fixture()
var ordinary: Dictionary = world._life_products(world.state, world.entities.HP, 1000000.0)
world.state.life_remainders.clear()
world.modify_world_parameter("Agg", 9.0)
var aggressive: Dictionary = world._life_products(world.state, world.entities.HP, 1000000.0)
check(aggressive.EON1 > ordinary.EON1, "Aggression accelerates primitive evolution")
world.state.life_remainders.clear()
world.state.world_parameters.Order = 10.0
var orderly: Dictionary = world._life_products(world.state, world.entities.HP, 1000000.0)
check(orderly.RP < aggressive.RP, "Order reduces degradation")
world.state.life_remainders.clear()
world.state.world_parameters.Enlightenment = 10.0
var enlightened: Dictionary = world._life_products(world.state, world.entities.HP, 1000000.0)
check(enlightened.EON1 > orderly.EON1, "Enlightenment increases primitive progress")
dispose(world)
world = fixture()
var hotter = fixture()
for simulation in [world, hotter]:
simulation.state.amount.RD = 1000000.0
simulation._append_batch(simulation.state, "RD", 1000000.0, 0.0, 0.0)
hotter.modify_world_parameter("Bur", 9.0)
world.evolute(1000.0)
hotter.evolute(1000.0)
check(hotter.state.amount.S > world.state.amount.S, "Burnability changes actual space production")
check(hotter.state.energy + hotter.state.amount.S == Parameters.INITIAL_ENERGY, "Enhanced production still spends the GI energy budget")
dispose(world)
dispose(hotter)
world = fixture()
world.state.amount["C0.2"] = 1000000.0
world.state.ethics["C0.2"] = {"m": 1000000.0}
var baseline: Dictionary = world.state.duplicate(true)
var advanced: Dictionary = baseline.duplicate(true)
advanced.world_parameters.Agg = 19.0
var next_stage: String = global.CivilizationRules["C0.2"].next
var outcomes: Array = []
for target in [baseline, advanced]:
var expired: Dictionary = {"C0.2": [{"amount": 1000000.0, "from": 0.0, "to": 1.0}]}
var budget: Dictionary = world.CivilizationState.stellar_budget(world, target, global.CivilizationRules)
world.CivilizationEvolution.prepare(world, target, expired, 1.0, budget, global.CivilizationRules)
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0.0))
check(outcomes[1] > outcomes[0], "Aggression changes actual ethical civilization transitions")
dispose(world)
# Trends use ethical proportions and stage weights, not population magnitude.
world = fixture()
var rules: Dictionary = {"C2.0": {"home": "DwStar"}, "C2.1": {"home": "DwStar"}}
world.state.ethics = {"C2.0": {"d": 100.0}, "C2.1": {"d": 100.0}}
var source: Dictionary = world.state.duplicate(true)
var single: Dictionary = source.duplicate(true)
var split: Dictionary = source.duplicate(true)
Parameters.advance(source, single, rules, 1.0e6)
for i in 10: Parameters.advance(source, split, rules, 1.0e5)
check(is_equal_approx(single.trends.Integration, 0.018), "Trend integrates elapsed years and doubled stage weights")
for key in Parameters.TRENDS:
check(is_equal_approx(single.trends[key], split.trends[key]), "Trend is independent of frame partition: " + key)
source.ethics["C2.0"].d = 1000000.0
var larger: Dictionary = world.state.duplicate(true)
Parameters.advance(source, larger, rules, 1.0e6)
check(is_equal_approx(larger.trends.Integration, single.trends.Integration), "Population proportions determine trend influence")
world.state.trends.Integration = 0.99
Parameters.advance(source, world.state, rules, 1.0e8)
check(world.state.civilization_stats["trend:Integration"] == 2.0, "Long steps process every crossed threshold")
check(is_equal_approx(world.state.trends.Integration, 0.79), "Threshold crossing preserves the fractional remainder")
check(world.state.world_parameters.Federation > 1.0 or world.state.world_parameters.Enlightenment > 1.0 or world.state.world_parameters.Order > 1.0, "Integration events change the world")
var restored: Dictionary = bytes_to_var(var_to_bytes(world.state))
Parameters.advance(source, world.state, rules, 1.0e8)
Parameters.advance(source, restored, rules, 1.0e8)
check(world.state == restored, "Trend events resume identically after serialization")
Parameters.advance(source, world.state, rules, 1.0e12)
check(world.state.trends.Integration >= 0.0 and world.state.trends.Integration < 1.0, "Very large intervals finish with a valid trend fraction")
check(world.state.world_parameters.Agg >= 1.0, "Repeated integration events cannot reduce aggression below the scale")
var payload: Dictionary = {"format_version": 2, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
var manager: Node = root.get_node("SaveManager")
check(manager.validate_data(payload) == "", "New parameters and trends pass save validation")
var old: Dictionary = payload.duplicate(true)
old.state.erase("world_parameters")
old.state.erase("trends")
check(manager.validate_data(old) == "", "Existing current-format saves remain readable")
payload.state.world_parameters.Agg = 20.0
check(manager.validate_data(payload) != "", "Out-of-range scales are rejected")
dispose(world)
for sample in [[1e-7, 0.0], [1e-6, 0.5], [1e-5, 1.0], [0.9, 0.0], [0.95, 0.5], [1.0, 1.0]]:
check(is_equal_approx(Parameters.density_display(sample[0]).position, sample[1]), "Density boundary " + str(sample[0]))
# The fixture overrides saving/loading; the player's save is never accessed.
var scene = load("res://world/world.tscn").instantiate()
var stage = scene.get_node("Stage")
stage.owniverse.free()
stage.set_script(load("res://tests/fixtures/ui_stage.gd"))
root.add_child(scene)
await process_frame
await process_frame
stage.owniverse.state.energy = Parameters.INITIAL_ENERGY / 4.0
stage.owniverse.state.world_parameters.Bur = 19.0
stage.owniverse.state.trends.Integration = 0.75
stage.world_indicators.update(stage.owniverse)
for bar in stage.world_indicators.energy_bars:
check(bar.scale.x == 0.25, "Energy fill is synchronized on both pages")
var pointer: Control = stage.world_indicators.pointers.Bur
check(is_equal_approx(pointer.position.x + pointer.size.x / 2.0, 304.0), "Burnability pointer reaches the correct scale endpoint")
check(stage.world_indicators.trend_bars[1].scale.y == 0.75, "Trend bar displays its stored level")
for locale in ["en", "ru"]:
global.Locale = locale
for id in ["Energy", "Density", "Burnability", "Explosiveness", "Aggression", "Trends"]:
var description: String = Descriptions.render(stage.owniverse, global, id, "Indicator")
check(not description.contains("—") and not description.contains("%value"), "Resolved indicator description: " + locale + "/" + id)
dispose(stage.owniverse)
scene.free()
print("World indicators: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://dmldg06dx3utl
+48
View File
@@ -0,0 +1,48 @@
extends RefCounted
const Parameters = preload("res://world/owniverse/world_parameters.gd")
var energy_bars: Array = []
var density_indicators: Array = []
var pointers: Dictionary = {}
var trend_bars: Array = []
func _init(stage: Node) -> void:
var pages: Node = stage.get_node("World/PageScroller/HBoxContainer")
for path in ["ResourcePage/ResourceHeader", "StructurePage/StructureHeader"]:
energy_bars.append(pages.get_node(path + "/EnergyIndicator/Indicator"))
density_indicators.append(pages.get_node(path + "/DensityIndicator"))
pointers.Bur = pages.get_node("StarPage/StarHeader/BurnabilityIndicator/BurnabilityPointer")
pointers.Exp = pages.get_node("StarPage/StarHeader/ExplosivenessIndicator/ExplosivenessPointer")
pointers.Agg = pages.get_node("LifePage/LifeHeader/AggressionIndicator/AggressionPointer")
var trends: Node = pages.get_node("LifePage/LifeHeader/TrendIndicator/HBoxContainer")
for i in 5:
var container: String = "CenterContainer" + (str(i + 1) if i > 0 else "")
trend_bars.append(trends.get_node(container + "/Trend" + str(i + 1)))
# Decorative children must not swallow clicks intended for indicator buttons.
for header_path in ["ResourcePage/ResourceHeader", "StructurePage/StructureHeader", "StarPage/StarHeader", "LifePage/LifeHeader"]:
for child in pages.get_node(header_path).get_children():
if child is TextureButton: _ignore_mouse_on_children(child)
func _ignore_mouse_on_children(node: Node) -> void:
for child in node.get_children():
if child is Control: child.mouse_filter = Control.MOUSE_FILTER_IGNORE
_ignore_mouse_on_children(child)
func update(world) -> void:
var source: Dictionary = world.state
var energy: float = clampf(source.energy / Parameters.INITIAL_ENERGY, 0.0, 1.0)
for bar in energy_bars: bar.scale.x = energy
var density: Dictionary = Parameters.density_display(world.stellar_environment(source).density)
for indicator in density_indicators:
for mode in ["Density", "DensityRip", "DensityCrunch"]:
indicator.get_node(mode).visible = mode == density.mode
var pointer: Control = indicator.get_node("Pointer")
pointer.position.x = 16.0 + 288.0 * density.position - pointer.size.x / 2.0
pointer.self_modulate = Color.BLACK if density.mode == "DensityCrunch" else Color.WHITE
for key in pointers:
var pointer: Control = pointers[key]
pointer.position.x = 16.0 * clampf(Parameters.value(source, key), 1.0, 19.0) - pointer.size.x / 2.0
for i in trend_bars.size():
var bar: Control = trend_bars[i]
bar.pivot_offset = Vector2(0.0, bar.size.y)
bar.scale.y = clampf(source.get("trends", {}).get(Parameters.TRENDS[i], 0.0), 0.0, 1.0)
+1
View File
@@ -0,0 +1 @@
uid://brwcqoffc2n1e
+10 -7
View File
@@ -1,6 +1,7 @@
extends RefCounted extends RefCounted
const State = preload("res://world/owniverse/civilization_state.gd") const State = preload("res://world/owniverse/civilization_state.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
const ETHICS = State.ETHICS const ETHICS = State.ETHICS
const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"} const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"}
@@ -106,6 +107,8 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
var plans: Dictionary = {} var plans: Dictionary = {}
var colonies: Array = [] var colonies: Array = []
var collateral: Dictionary = {} var collateral: Dictionary = {}
var modifiers: Dictionary = WorldParameters.life_modifiers(target)
var order: float = WorldParameters.value(target, "Order")
if not target.has("civilization_stats"): target.civilization_stats = {} if not target.has("civilization_stats"): target.civilization_stats = {}
var ids: Array = expired.keys() var ids: Array = expired.keys()
ids.sort() ids.sort()
@@ -124,14 +127,14 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
var settlers: Dictionary = {} var settlers: Dictionary = {}
for ethic in triggered: for ethic in triggered:
var row: Dictionary = rules[id].ethics[ethic] var row: Dictionary = rules[id].ethics[ethic]
var p: float = maxf(0.0, float(row.progress)) var p: float = maxf(0.0, float(row.progress) * modifiers.progress)
var d: float = maxf(0.0, float(row.degradation)) var d: float = maxf(0.0, float(row.degradation) * modifiers.degradation)
var scale: float = maxf(1.0, p + d) var scale: float = maxf(1.0, p + d)
var pro: float = rate(target, id + ":" + ethic + ":pro", triggered[ethic], p / scale) var pro: float = rate(target, id + ":" + ethic + ":pro", triggered[ethic], p / scale)
var deg: float = rate(target, id + ":" + ethic + ":deg", triggered[ethic], d / scale) var deg: float = rate(target, id + ":" + ethic + ":deg", triggered[ethic], d / scale)
var bounded: Dictionary = world._allocate_whole({"pro": pro, "deg": deg}, triggered[ethic]) var bounded: Dictionary = world._allocate_whole({"pro": pro, "deg": deg}, triggered[ethic])
if rules[id].fights: warriors[ethic] = rate(target, id + ":" + ethic + ":war", triggered[ethic], float(row.war)) if rules[id].fights: warriors[ethic] = rate(target, id + ":" + ethic + ":war", triggered[ethic], minf(1.0, float(row.war) * modifiers.war))
if rules[id].colonizes: settlers[ethic] = rate(target, id + ":" + ethic + ":col", triggered[ethic], float(row.colonization)) if rules[id].colonizes: settlers[ethic] = rate(target, id + ":" + ethic + ":col", triggered[ethic], minf(1.0, float(row.colonization) * modifiers.colonization))
successes[ethic] = bounded.pro successes[ethic] = bounded.pro
failures[ethic] = bounded.deg failures[ethic] = bounded.deg
triggered[ethic] -= bounded.pro + bounded.deg triggered[ethic] -= bounded.pro + bounded.deg
@@ -146,16 +149,16 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
var win_expected: float = outcomes.get("pro25", 0.0) * 0.25 + outcomes.get("pro50", 0.0) * 0.5 + outcomes.get("pro75", 0.0) * 0.75 var win_expected: float = outcomes.get("pro25", 0.0) * 0.25 + outcomes.get("pro50", 0.0) * 0.5 + outcomes.get("pro75", 0.0) * 0.75
var loss_expected: float = outcomes.get("deg", 0.0) * 2.0 + outcomes.get("pro25", 0.0) + outcomes.get("pro50", 0.0) * 1.5 + outcomes.get("pro75", 0.0) * 2.0 var loss_expected: float = outcomes.get("deg", 0.0) * 2.0 + outcomes.get("pro25", 0.0) + outcomes.get("pro50", 0.0) * 1.5 + outcomes.get("pro75", 0.0) * 2.0
var winners: Dictionary = take(world, warriors, rate(target, id + ":wins", win_expected, 1.0)) var winners: Dictionary = take(world, warriors, rate(target, id + ":wins", win_expected, 1.0))
var attackers_lost: Dictionary = take(world, warriors, outcomes.get("deg", 0.0)) var attackers_lost: Dictionary = take(world, warriors, rate(target, id + ":attackers_lost", outcomes.get("deg", 0.0) / order, 1.0))
add(triggered, progress(world, target, rules, budget, plan, winners)) add(triggered, progress(world, target, rules, budget, plan, winners))
fall(target, plan, rules, attackers_lost) fall(target, plan, rules, attackers_lost)
var losses: float = minf(rate(target, id + ":loss", loss_expected, 1.0), total(triggered) + total(remaining[id])) var losses: float = minf(rate(target, id + ":loss", loss_expected / order, 1.0), total(triggered) + total(remaining[id]))
var sides: Dictionary = world._allocate_whole({"due": total(triggered), "later": total(remaining[id])}, losses) var sides: Dictionary = world._allocate_whole({"due": total(triggered), "later": total(remaining[id])}, losses)
var victims: Dictionary = take(world, triggered, sides.due) var victims: Dictionary = take(world, triggered, sides.due)
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year)) add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
fall(target, plan, rules, victims) fall(target, plan, rules, victims)
add(triggered, warriors) add(triggered, warriors)
var damage: float = outcomes.get("pro50", 0.0) * 10.0 + outcomes.get("pro75", 0.0) * 30.0 var damage: float = rate(target, id + ":collateral", (outcomes.get("pro50", 0.0) * 10.0 + outcomes.get("pro75", 0.0) * 30.0) / order, 1.0)
collateral[rules[id].previous] = collateral.get(rules[id].previous, 0.0) + damage collateral[rules[id].previous] = collateral.get(rules[id].previous, 0.0) + damage
target.civilization_stats.wars = target.civilization_stats.get("wars", 0.0) + wars target.civilization_stats.wars = target.civilization_stats.get("wars", 0.0) + wars
emit(plan, id, triggered) emit(plan, id, triggered)
+14
View File
@@ -3,6 +3,7 @@ extends RefCounted
const GREEN = "#40cc40" const GREEN = "#40cc40"
const RED = "#ff6060" const RED = "#ff6060"
const GREY = "#888888" const GREY = "#888888"
const Parameters = preload("res://world/owniverse/world_parameters.gd")
static func number(value: float, digits: int = 0) -> String: static func number(value: float, digits: int = 0) -> String:
if not is_finite(value): if not is_finite(value):
@@ -82,6 +83,19 @@ static func variables(world, global: Node, id: String, rate: float = 0.0) -> Dic
static func render(world, global: Node, id: String, category: String = "Entity", rate: float = 0.0) -> String: static func render(world, global: Node, id: String, category: String = "Entity", rate: float = 0.0) -> String:
var values: Dictionary = variables(world, global, id, rate) var values: Dictionary = variables(world, global, id, rate)
if category == "Indicator":
if id == "Density":
id = Parameters.density_display(world.stellar_environment(world.state).density).mode
var scales: Dictionary = {"Burnability": "Bur", "Explosiveness": "Exp", "Aggression": "Agg"}
if scales.has(id):
var level: float = Parameters.value(world.state, scales[id])
values.amount = number(level, 1 if level != floorf(level) else 0)
values.scale = global.render_template(global.get_template("Scale" + str(clampi(roundi(level), 1, 19)), "Indicator", "Scale"), {})
if id == "Trends":
for trend in Parameters.TRENDS:
values[trend.to_lower()] = number(world.state.get("trends", {}).get(trend, 0.0) * 100.0, 2) + "%"
if id == "Energy":
values["энергия"] = number(world.state.energy) + " (" + number(clampf(world.state.energy / Parameters.INITIAL_ENERGY, 0.0, 1.0) * 100.0, 2) + "%)"
var title: String = global.render_template(global.get_template(id, category, "Name"), values) var title: String = global.render_template(global.get_template(id, category, "Name"), values)
var body: String = global.render_template(global.get_template(id, category, "Description"), values) var body: String = global.render_template(global.get_template(id, category, "Description"), values)
return "[b]" + (title if title != "" else id) + "[/b]\n" + body return "[b]" + (title if title != "" else id) + "[/b]\n" + body
+37 -7
View File
@@ -3,6 +3,7 @@ class_name Owniverse
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd") const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd") const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
const SimulationTime = preload("res://world/owniverse/simulation_time.gd") const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
#var state: State = State.new() #var state: State = State.new()
@@ -30,7 +31,9 @@ var state: Dictionary = {
}, },
'speed': 1, 'speed': 1,
'speed_in_year': 1, 'speed_in_year': 1,
'energy': 1E14, 'energy': WorldParameters.INITIAL_ENERGY,
'world_parameters': WorldParameters.defaults(),
'trends': WorldParameters.trend_defaults(),
'batches': {}, 'batches': {},
'batch_origins': {}, 'batch_origins': {},
'binds': {}, 'binds': {},
@@ -110,6 +113,7 @@ func _evolute_to_datetime(datetime: float) -> void:
_evolve_batches(next, to_year) _evolve_batches(next, to_year)
_refresh_groups(next) _refresh_groups(next)
_clamp_auto_resources(next) _clamp_auto_resources(next)
WorldParameters.advance(previous, next, Global.CivilizationRules, to_year - from_year)
next.time = {"datetime": datetime, "year": to_year} next.time = {"datetime": datetime, "year": to_year}
# Publish only after every phase has finished; previous stays untouched. # Publish only after every phase has finished; previous stays untouched.
@@ -161,6 +165,25 @@ func _add_time_delta(delta: float) -> Dictionary:
return {"datetime": new_datetime, "year": SimulationTime.processed_year(new_datetime)} return {"datetime": new_datetime, "year": SimulationTime.processed_year(new_datetime)}
## Public entry point for forces: update a world scale and preserve stellar ages.
func modify_world_parameter(key: String, change: float) -> void:
if key not in ["Bur", "Exp", "Agg"] or not is_finite(change): return
var old_multiplier: float = WorldParameters.effective(state, key)
if not state.has("world_parameters"): state.world_parameters = WorldParameters.defaults()
state.world_parameters[key] = clampf(WorldParameters.value(state, key) + change, 1.0, 19.0)
if key == "Bur":
var ratio: float = old_multiplier / WorldParameters.effective(state, key)
var now: float = state.time.year
for id in state.batches:
if not entities.has(id) or entities[id].group_as != "Star": continue
for batch: Dictionary in state.batches[id]:
# Only future survival time changes. Birth dates and the fractional
# withdrawal carry remain intact; already elapsed time is not replayed.
for field in ["start_year", "end_year", "processed_until"]:
if batch.has(field): batch[field] = now + maxf(0.0, batch[field] - now) * ratio
new_state = state
## Rebuild after changing group_as or replacing definitions without changing their count. ## Rebuild after changing group_as or replacing definitions without changing their count.
## Adding/removing definitions is detected automatically during initialization. ## Adding/removing definitions is detected automatically during initialization.
func rebuild_group_index() -> void: func rebuild_group_index() -> void:
@@ -355,6 +378,8 @@ func _append_batch(target: Dictionary, id: String, amount: float,
target.batch_origins = {} target.batch_origins = {}
var batches: Array = target.batches[id] var batches: Array = target.batches[id]
var lifespan: float = CivilizationState.effective_span(self, target, id, Global.CivilizationRules) var lifespan: float = CivilizationState.effective_span(self, target, id, Global.CivilizationRules)
if entities[id].group_as == "Star":
lifespan /= WorldParameters.effective(target, "Bur")
var batch_duration: float = lifespan / Global.batch_count if lifespan > 10.0 else lifespan var batch_duration: float = lifespan / Global.batch_count if lifespan > 10.0 else lifespan
if batch_duration <= 0.0: if batch_duration <= 0.0:
return NAN return NAN
@@ -475,11 +500,17 @@ func _add_output_terms(outputs: Dictionary, terms: String, count: float) -> void
func _life_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary: func _life_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary:
var rule: Dictionary = entity.life_rule var rule: Dictionary = entity.life_rule
var pending: Dictionary = {} var pending: Dictionary = {}
var modifiers: Dictionary = WorldParameters.life_modifiers(target)
var probabilities: Dictionary = {
"progress": float(rule.progress) * modifiers.progress * WorldParameters.value(target, "Enlightenment"),
"degradation": float(rule.degradation) * modifiers.degradation,
}
var probability_sum: float = maxf(1.0, probabilities.progress + probabilities.degradation)
if not target.has("life_remainders"): if not target.has("life_remainders"):
target.life_remainders = {} target.life_remainders = {}
for channel in ["progress", "degradation"]: for channel in ["progress", "degradation"]:
var key: String = entity.id + ":" + channel var key: String = entity.id + ":" + channel
var expected: float = count * float(rule[channel]) + target.life_remainders.get(key, 0.0) var expected: float = count * probabilities[channel] / probability_sum + target.life_remainders.get(key, 0.0)
target.life_remainders[key] = expected target.life_remainders[key] = expected
pending[channel] = floorf(snappedf(expected, 1.0e-12)) pending[channel] = floorf(snappedf(expected, 1.0e-12))
var assigned: Dictionary = _allocate_whole(pending, count) var assigned: Dictionary = _allocate_whole(pending, count)
@@ -530,7 +561,7 @@ func _evolution_products(target: Dictionary, entity: OwniverseEntity, count: flo
outputs[id] = count * maxf(0.0, entity.output_and[id]) outputs[id] = count * maxf(0.0, entity.output_and[id])
if not entity.output_or.is_empty(): if not entity.output_or.is_empty():
# The parser stores the percentage after '%', e.g. 0.1 means 0.1 percent. # The parser stores the percentage after '%', e.g. 0.1 means 0.1 percent.
var chance: float = clampf(entity.output_or.get("chance", 0.0) / 100.0, 0.0, 1.0) var chance: float = clampf(entity.output_or.get("chance", 0.0) / 100.0 * WorldParameters.effective(target, "Exp"), 0.0, 1.0)
if not target.has("evolution_remainders"): if not target.has("evolution_remainders"):
target.evolution_remainders = {} target.evolution_remainders = {}
var expected: float = count * chance + target.evolution_remainders.get(entity.id, 0.0) var expected: float = count * chance + target.evolution_remainders.get(entity.id, 0.0)
@@ -542,12 +573,11 @@ func _evolution_products(target: Dictionary, entity: OwniverseEntity, count: flo
# blast:N. Zero disables the event; never add both representations. # blast:N. Zero disables the event; never add both representations.
outputs.erase("blast") outputs.erase("blast")
if entity.blast > 0: if entity.blast > 0:
outputs["blast"] = count * entity.blast outputs["blast"] = count * entity.blast * WorldParameters.effective(target, "Exp")
return outputs return outputs
# Derived from current concrete stock, including bound objects, never UI aliases. # Derived from current concrete stock, including bound objects, never UI aliases.
# GI has no Unity force parameters; stellar effects use their neutral multiplier 1.
func stellar_environment(source: Dictionary) -> Dictionary: func stellar_environment(source: Dictionary) -> Dictionary:
var quantities: Dictionary = _physical_amounts(source) var quantities: Dictionary = _physical_amounts(source)
# SMBH is a self-group in GI; its quantity stores mass, not object count. # SMBH is a self-group in GI; its quantity stores mass, not object count.
@@ -618,7 +648,7 @@ func _produce_newborn_lifetime(target: Dictionary, id: String, count: float,
object_years -= maxf(0.0, added) * _expired_years(to_year, batch.start_year, batch.end_year) object_years -= maxf(0.0, added) * _expired_years(to_year, batch.start_year, batch.end_year)
object_years = maxf(0.0, object_years) object_years = maxf(0.0, object_years)
for resource in products: for resource in products:
var output: float = object_years * products[resource] var output: float = object_years * products[resource] * WorldParameters.production(target, entities[id])
if resource in ["H", "S"]: if resource in ["H", "S"]:
output = minf(output, target.energy) output = minf(output, target.energy)
var actual: float = _credit(target, resource, output) var actual: float = _credit(target, resource, output)
@@ -1001,7 +1031,7 @@ func _produce_for_period(previous: Dictionary, target: Dictionary, quantities: D
var auto_share: float = clampf(entity.auto, 0.0, 1.0) var auto_share: float = clampf(entity.auto, 0.0, 1.0)
var products: Dictionary = _yearly_products(entity) var products: Dictionary = _yearly_products(entity)
for product in products: for product in products:
var amount: float = object_years * products[product] var amount: float = object_years * products[product] * WorldParameters.production(previous, entity)
basic[product] = basic.get(product, 0.0) + amount * (1.0 - auto_share) basic[product] = basic.get(product, 0.0) + amount * (1.0 - auto_share)
automatic[product] = automatic.get(product, 0.0) + amount * auto_share automatic[product] = automatic.get(product, 0.0) + amount * auto_share
# Basic and automatic output each retain their unproduced remainder. # Basic and automatic output each retain their unproduced remainder.
+102
View File
@@ -0,0 +1,102 @@
extends RefCounted
const INITIAL_ENERGY = 1.0e14
const TRENDS = ["Antitheism", "Integration", "Xenocide", "Reconstruction", "Spirituality"]
const WEIGHTS = {
"m": [0, 2, 2, 0, 6], "l": [6, 1, 1, 2, 0],
"w": [2, 0, 6, 0, 2], "d": [0, 6, 0, 1, 3],
"c": [1, 3, 3, 3, 0], "b": [3, 0, 0, 6, 1],
}
static func defaults() -> Dictionary:
return {"Bur": 10.0, "Exp": 10.0, "Agg": 10.0,
"Federation": 1.0, "Enlightenment": 1.0, "Order": 1.0}
static func trend_defaults() -> Dictionary:
return {"Antitheism": 0.0, "Integration": 0.0, "Xenocide": 0.0,
"Reconstruction": 0.0, "Spirituality": 0.0}
static func value(source: Dictionary, key: String) -> float:
return source.get("world_parameters", {}).get(key, defaults().get(key, 1.0))
static func effective(source: Dictionary, key: String) -> float:
return pow(10.0, (clampf(value(source, key), 1.0, 19.0) - 10.0) / 9.0)
static func life_modifiers(source: Dictionary) -> Dictionary:
var aggression: float = effective(source, "Agg")
return {"progress": aggression,
"degradation": (1.0 - (1.0 - aggression) / 20.0) / value(source, "Order"),
"war": 1.0 - (1.0 - aggression) / 5.0,
"colonization": 1.0 - (1.0 - aggression) / 2.0}
static func production(source: Dictionary, entity) -> float:
if entity.event_group == "SNGRBB":
return effective(source, "Exp")
if entity.group_as == "Star":
var burn: float = effective(source, "Bur")
return sqrt(burn) if burn > 1.0 else burn
return 1.0
static func density_display(density: float) -> Dictionary:
if density <= 1.0e-5:
# Unity had log10(density) - 7 here; +7 maps 1e-7..1e-5 to 0..1.
return {"mode": "DensityRip", "position": clampf((log(maxf(density, 1e-7)) / log(10.0) + 7.0) / 2.0, 0.0, 1.0)}
if density >= 0.9:
return {"mode": "DensityCrunch", "position": clampf((density - 0.9) * 10.0, 0.0, 1.0)}
return {"mode": "Density", "position": clampf(log(density * 1.0e5) / log(9.0e4), 0.0, 1.0)}
## Integrate the population present at the start of the simulation interval.
## Thresholds use whole event counts; no per-year loop at high speeds.
static func advance(previous: Dictionary, target: Dictionary, rules: Dictionary, years: float) -> void:
if years <= 0.0: return
if not target.has("world_parameters"): target.world_parameters = defaults()
if not target.has("trends"): target.trends = trend_defaults()
var rates: Array = [0.0, 0.0, 0.0, 0.0, 0.0]
var stages: Array = []
for id in rules:
if str(rules[id].home) != "": stages.append(id)
stages.sort()
var weight: float = 1.0
for id in stages:
var population: Dictionary = previous.get("ethics", {}).get(id, {})
var count: float = 0.0
for n in population.values(): count += n
if count > 0.0:
for ethic in population:
for i in TRENDS.size():
rates[i] += population[ethic] / count * WEIGHTS[ethic][i] * weight
weight *= 2.0
for i in TRENDS.size():
var name: String = TRENDS[i]
var accumulated: float = target.trends[name] + rates[i] * years * 1.0e-9
var events: float = floorf(snappedf(accumulated, 1.0e-12))
target.trends[name] = maxf(0.0, accumulated - events)
if events <= 0.0: continue
var key: String = "trend:" + name
target.civilization_stats[key] = target.civilization_stats.get(key, 0.0) + events
if name == "Integration": _integration(target, events)
static func _integration(target: Dictionary, events: float) -> void:
# Equal-probability outcomes in Unity. Use a saved RNG and bounded aggregate
# sampling for large steps, so thousands of triggers do not stall a frame.
var rng := RandomNumberGenerator.new()
rng.seed = 76123
if target.civilization_stats.has("trend_rng"):
rng.state = int(target.civilization_stats.trend_rng)
var counts: Array = [0.0, 0.0, 0.0]
if events <= 256.0:
for i in int(events): counts[rng.randi_range(0, 2)] += 1.0
else:
counts[0] = clampf(roundf(rng.randfn(events / 3.0, sqrt(events * 2.0 / 9.0))), 0.0, events)
var rest: float = events - counts[0]
counts[1] = clampf(roundf(rng.randfn(rest / 2.0, sqrt(rest / 4.0))), 0.0, rest)
counts[2] = rest - counts[1]
target.civilization_stats.trend_rng = rng.state
var parameters: Dictionary = target.world_parameters
parameters.Agg = maxf(1.0, parameters.Agg - counts[0] - counts[1])
parameters.Federation += counts[0] * 0.1
parameters.Enlightenment += counts[1] * 0.2
parameters.Order += counts[2] * 0.1
for i in 3:
var key: String = ["Federation", "Enlightenment", "Order"][i]
target.civilization_stats[key] = target.civilization_stats.get(key, 0.0) + counts[i]
+1
View File
@@ -0,0 +1 @@
uid://brsauq1r3xn87
+1
View File
@@ -1110,6 +1110,7 @@ autostart = true
[connection signal="pressed" from="Stage/World/PageScroller/HBoxContainer/LifePage/LifeHeader/AggressionIndicator" to="Stage" method="_on_indicator_pressed" binds= ["Aggression", 4]] [connection signal="pressed" from="Stage/World/PageScroller/HBoxContainer/LifePage/LifeHeader/AggressionIndicator" to="Stage" method="_on_indicator_pressed" binds= ["Aggression", 4]]
[connection signal="pressed" from="Stage/World/PageScroller/HBoxContainer/LifePage/LifeHeader/TrendIndicator" to="Stage" method="_on_indicator_pressed" binds= ["Trends", 4]] [connection signal="pressed" from="Stage/World/PageScroller/HBoxContainer/LifePage/LifeHeader/TrendIndicator" to="Stage" method="_on_indicator_pressed" binds= ["Trends", 4]]
[connection signal="pressed" from="Stage/MainMenu/TopMenu/BackButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["World"]] [connection signal="pressed" from="Stage/MainMenu/TopMenu/BackButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["World"]]
[connection signal="pressed" from="Stage/MainMenu/CreateOwniverse" to="Stage" method="create_new_universe"]
[connection signal="pressed" from="Stage/MainMenu/Achievements" to="Stage" method="_on_topmenu_button_pressed" binds= ["Achievements"]] [connection signal="pressed" from="Stage/MainMenu/Achievements" to="Stage" method="_on_topmenu_button_pressed" binds= ["Achievements"]]
[connection signal="pressed" from="Stage/MainMenu/Credits" to="Stage" method="_on_topmenu_button_pressed" binds= ["Credits"]] [connection signal="pressed" from="Stage/MainMenu/Credits" to="Stage" method="_on_topmenu_button_pressed" binds= ["Credits"]]
[connection signal="pressed" from="Stage/Report/TopMenu/BackButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["World"]] [connection signal="pressed" from="Stage/Report/TopMenu/BackButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["World"]]