Add world indicators and parameters to enhance universe simulation
- new universe creation function.
This commit is contained in:
@@ -7,6 +7,7 @@ const SAVE_PATH = "user://settings.cfg"
|
||||
|
||||
const FORMAT_VERSION: int = 2
|
||||
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
|
||||
var last_error: String = ""
|
||||
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",
|
||||
"civilization_stats", "civilization_homes", "ethics", "stats",
|
||||
]
|
||||
if saved.size() != required.size():
|
||||
return "unsupported state structure"
|
||||
# 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"
|
||||
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:
|
||||
if not saved.has(field):
|
||||
return "missing state." + field
|
||||
|
||||
+55
-3
@@ -72,7 +72,7 @@ GI не хранит накопленные счётчики первичной
|
||||
- `density = 1.23456e-7 * sqrt(B + M) / max(1, V)^(1/6)`.
|
||||
- `blast_factor = clamp(free_S / V, 0, 1)`; при нулевом объёме `1`.
|
||||
|
||||
Система сил Unity и планетные бонусы в GI пока не перенесены; здесь используются нейтральные множители 1. Металличность для производства фиксируется до расходования ресурсов на звёзды. Среда бласта фиксируется для поколения до добавления его продуктов.
|
||||
Планетные бонусы сил пока не перенесены. Параметры мира Bur/Exp/Agg имеют собственные сохраняемые шкалы и эффективные множители (см. раздел индикаторов ниже). Металличность для производства фиксируется до расходования ресурсов на звёзды. Среда бласта фиксируется для поколения до добавления его продуктов.
|
||||
|
||||
## Разрушение и бласты
|
||||
|
||||
@@ -102,7 +102,7 @@ GI не хранит накопленные счётчики первичной
|
||||
|
||||
Прогресс и деградация округляются вниз с сохранением остатков. Если оба накопленных исхода конкурируют за последнюю планету, распределяются только существующие объекты, а неиспользованный остаток переносится на следующую попытку. Деградация распределяется по весам `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. Для существующих этических цивилизаций пользователь выбрал равномерное распределение между шестью этиками с детерминированным целым остатком.
|
||||
|
||||
@@ -144,4 +144,56 @@ HP деградирует в RP. У эонов возможен откат к п
|
||||
|
||||
Размещение выбирает структуры в порядке убывания `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
@@ -5,6 +5,9 @@ const OWNIVERSE_ITEM = preload("uid://d3xjfxwe3w16v")
|
||||
const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png"
|
||||
|
||||
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_samples: Dictionary = {}
|
||||
var description_rates: Dictionary = {}
|
||||
@@ -317,15 +320,10 @@ func _page_action_pressed(page_id: int, action_amount: float) -> void:
|
||||
|
||||
func _ready() -> void:
|
||||
get_tree().auto_accept_quit = false
|
||||
var inited_result = init_enitities()
|
||||
locked_items = inited_result.locked_items
|
||||
world_indicators = WorldIndicators.new(self)
|
||||
create_new_universe()
|
||||
load_data()
|
||||
_init_owniverse_items()
|
||||
owniverse.refresh_item_amounts()
|
||||
_change_speed(0)
|
||||
set_datetime(owniverse.state.time.year)
|
||||
set_owniverse_item_value()
|
||||
_unlock_items()
|
||||
_refresh_universe_ui()
|
||||
|
||||
g_counters.swipe.sec = 0.2
|
||||
|
||||
@@ -334,9 +332,56 @@ func _ready() -> void:
|
||||
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:
|
||||
|
||||
owniverse.evolute(delta)
|
||||
indicator_elapsed += delta
|
||||
if indicator_elapsed >= 0.1:
|
||||
indicator_elapsed = 0.0
|
||||
world_indicators.update(owniverse)
|
||||
_update_descriptions(delta)
|
||||
set_owniverse_item_value()
|
||||
set_datetime(owniverse.state.time.year)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Проверки поэтапного переноса из Owniverse.U
|
||||
|
||||
## Параметры мира и индикаторы
|
||||
|
||||
```sh
|
||||
godot --headless --path Owniverse.GI --script res://tests/test_world_indicators.gd
|
||||
```
|
||||
|
||||
Проверяются сроки жизни существующих и новых звёзд при смене горючести, вероятности и мощность взрывов, влияние агрессии/порядка/просвещения на жизнь, накопление тенденций по времени и этикам, несколько событий за шаг, продолжение после сериализации, валидация сохранения, границы шкал и описания на двух языках. Сцена использует фикстуру без доступа к сохранению игрока.
|
||||
|
||||
## Прямой расчёт завершившихся сверхновых
|
||||
|
||||
```sh
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dmldg06dx3utl
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://brwcqoffc2n1e
|
||||
@@ -1,6 +1,7 @@
|
||||
extends RefCounted
|
||||
|
||||
const State = preload("res://world/owniverse/civilization_state.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
const ETHICS = State.ETHICS
|
||||
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 colonies: Array = []
|
||||
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 = {}
|
||||
var ids: Array = expired.keys()
|
||||
ids.sort()
|
||||
@@ -124,14 +127,14 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
||||
var settlers: Dictionary = {}
|
||||
for ethic in triggered:
|
||||
var row: Dictionary = rules[id].ethics[ethic]
|
||||
var p: float = maxf(0.0, float(row.progress))
|
||||
var d: float = maxf(0.0, float(row.degradation))
|
||||
var p: float = maxf(0.0, float(row.progress) * modifiers.progress)
|
||||
var d: float = maxf(0.0, float(row.degradation) * modifiers.degradation)
|
||||
var scale: float = maxf(1.0, p + d)
|
||||
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 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].colonizes: settlers[ethic] = rate(target, id + ":" + ethic + ":col", triggered[ethic], float(row.colonization))
|
||||
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], minf(1.0, float(row.colonization) * modifiers.colonization))
|
||||
successes[ethic] = bounded.pro
|
||||
failures[ethic] = 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 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 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))
|
||||
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 victims: Dictionary = take(world, triggered, sides.due)
|
||||
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
|
||||
fall(target, plan, rules, victims)
|
||||
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
|
||||
target.civilization_stats.wars = target.civilization_stats.get("wars", 0.0) + wars
|
||||
emit(plan, id, triggered)
|
||||
|
||||
@@ -3,6 +3,7 @@ extends RefCounted
|
||||
const GREEN = "#40cc40"
|
||||
const RED = "#ff6060"
|
||||
const GREY = "#888888"
|
||||
const Parameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
|
||||
static func number(value: float, digits: int = 0) -> String:
|
||||
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:
|
||||
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 body: String = global.render_template(global.get_template(id, category, "Description"), values)
|
||||
return "[b]" + (title if title != "" else id) + "[/b]\n" + body
|
||||
|
||||
@@ -3,6 +3,7 @@ class_name Owniverse
|
||||
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
|
||||
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
|
||||
#var state: State = State.new()
|
||||
|
||||
@@ -30,7 +31,9 @@ var state: Dictionary = {
|
||||
},
|
||||
'speed': 1,
|
||||
'speed_in_year': 1,
|
||||
'energy': 1E14,
|
||||
'energy': WorldParameters.INITIAL_ENERGY,
|
||||
'world_parameters': WorldParameters.defaults(),
|
||||
'trends': WorldParameters.trend_defaults(),
|
||||
'batches': {},
|
||||
'batch_origins': {},
|
||||
'binds': {},
|
||||
@@ -110,6 +113,7 @@ func _evolute_to_datetime(datetime: float) -> void:
|
||||
_evolve_batches(next, to_year)
|
||||
_refresh_groups(next)
|
||||
_clamp_auto_resources(next)
|
||||
WorldParameters.advance(previous, next, Global.CivilizationRules, to_year - from_year)
|
||||
next.time = {"datetime": datetime, "year": to_year}
|
||||
|
||||
# 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)}
|
||||
|
||||
|
||||
## 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.
|
||||
## Adding/removing definitions is detected automatically during initialization.
|
||||
func rebuild_group_index() -> void:
|
||||
@@ -355,6 +378,8 @@ func _append_batch(target: Dictionary, id: String, amount: float,
|
||||
target.batch_origins = {}
|
||||
var batches: Array = target.batches[id]
|
||||
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
|
||||
if batch_duration <= 0.0:
|
||||
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:
|
||||
var rule: Dictionary = entity.life_rule
|
||||
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"):
|
||||
target.life_remainders = {}
|
||||
for channel in ["progress", "degradation"]:
|
||||
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
|
||||
pending[channel] = floorf(snappedf(expected, 1.0e-12))
|
||||
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])
|
||||
if not entity.output_or.is_empty():
|
||||
# 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"):
|
||||
target.evolution_remainders = {}
|
||||
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.
|
||||
outputs.erase("blast")
|
||||
if entity.blast > 0:
|
||||
outputs["blast"] = count * entity.blast
|
||||
outputs["blast"] = count * entity.blast * WorldParameters.effective(target, "Exp")
|
||||
return outputs
|
||||
|
||||
|
||||
# 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:
|
||||
var quantities: Dictionary = _physical_amounts(source)
|
||||
# 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, object_years)
|
||||
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"]:
|
||||
output = minf(output, target.energy)
|
||||
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 products: Dictionary = _yearly_products(entity)
|
||||
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)
|
||||
automatic[product] = automatic.get(product, 0.0) + amount * auto_share
|
||||
# Basic and automatic output each retain their unproduced remainder.
|
||||
|
||||
@@ -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]
|
||||
@@ -0,0 +1 @@
|
||||
uid://brsauq1r3xn87
|
||||
@@ -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/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/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/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"]]
|
||||
|
||||
Reference in New Issue
Block a user