Add core simulation modules for owniverse mechanics
- Introduced `life_evolution.gd` to manage life evolution processes. - Implemented `manual_production.gd` for handling production mechanics, including resource management and production steps. - Created `object_batches.gd` to manage batches of objects, including scaling and appending batches. - Developed `object_ledger.gd` for tracking physical amounts, available resources, and binding ownerships. - Added `simulation_module.gd` as a base class for simulation modules, providing a weak reference to the world. - Established `stellar_rules.gd` for managing stellar evolution and destruction mechanics. - Introduced `universe_environment.gd` to calculate environmental parameters based on the current state of the universe.
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
|
||||
Метод `members(id)` возвращает копию списка. После изменения group_as нужно вызвать `rebuild_group_index()`; добавление или удаление определения автоматически обнаруживается по изменению размера словаря. Полная перезагрузка определений выполняется созданием нового каталога.
|
||||
|
||||
Owniverse сохраняет совместимые свойства entities/entity_groups/entity_event_groups и методы-обёртки. Они используют те же данные каталога, без дублирования. Stage загружает каталог, затем отдельно инициализирует количество, статистику и UI. Эволюция цивилизаций обращается непосредственно к QuantityMath.
|
||||
Stage загружает каталог, затем отдельно инициализирует количество, статистику и UI. Доступ к определениям идёт через `world.catalog.entities`, к группам — через `world.catalog.members(id)`. Совместимые свойства и методы-переадресации из Owniverse удалены. Эволюция цивилизаций обращается непосредственно к QuantityMath.
|
||||
|
||||
Формулы, порядок обхода определений, формат сохранения и правила симуляции не изменены. Проверки модулей: `tests/test_quantity_catalog.gd`; интеграционные проверки перечислены в `tests/README.md`.
|
||||
|
||||
@@ -23,4 +23,49 @@ Owniverse сохраняет совместимые свойства entities/en
|
||||
- `unlocks(state, catalog, locked)` проверяет условия по историческим количествам и их отображаемым суммам. Возвращает remaining (ещё закрытые сущности) и visible (открываемые сущности и их count_as). Переданный список не изменяется. Уже открытые иконки повторно не блокируются.
|
||||
- `refresh` управляет снимком pending_amounts и флагом dirty, учитывает отложенное обновление и принудительную публикацию. Сам модуль не запускает таймеры и не меняет частоту обновления.
|
||||
|
||||
Owniverse делегирует существующие get_item_amounts/refresh_item_amounts и свойства обновления экземпляру presentation. Stage применяет результат разблокировки к контролам и хранит список закрытых иконок. Снимки и флаги принадлежат отдельному миру и не включаются в сохранение.
|
||||
Stage обращается непосредственно к `world.presentation`, применяет результат разблокировки к контролам и хранит список закрытых иконок. Снимки и флаги принадлежат отдельному миру и не включаются в сохранение.
|
||||
|
||||
## Партии, учёт, производство и эволюция
|
||||
|
||||
| Модуль / свойство мира | Ответственность |
|
||||
| --- | --- |
|
||||
| `object_batches.gd` / `batches` | Окна рождения и эволюции, частичное выбытие, остатки округления, масштабирование партий, интеграл времени жизни, прямое завершение короткоживущих партий |
|
||||
| `object_ledger.gd` / `ledger` | Свободные и связанные количества, списание и зачисление, статистика выпуска, целочисленность, резервирование и передача привязок |
|
||||
| `manual_production.gd` / `production` | Предпросмотр доступного количества, транзакция оплаты и привязок, ручное производство, объединение чёрных дыр в SMBH |
|
||||
| `automatic_production.gd` / `automatic` | Выпуск за период и жизнь новорождённых объектов, разделение ресурсов на обычные и автоматические, энергия, квоты звёзд и бурых карликов |
|
||||
| `stellar_rules.gd` / `stellar` | Продукты звёздной эволюции, создание планет, выбор звёздных преемников, два уровня разрушения и глобальные бласты |
|
||||
| `life_evolution.gd` / `life` | Прогресс, деградация и повторная попытка планетарной жизни |
|
||||
| `civilization_evolution.gd`, `civilization_state.gd` | Существующие правила цивилизаций: этики, войны, колонизация, лимиты и места обитания |
|
||||
| `universe_environment.gd` / `environment` | Плотность, металличность, доля попаданий бластов, изменение параметров мира с сохранением возраста звёзд |
|
||||
| `evolution_coordinator.gd` / `evolution` | Порядок фаз шага, последовательные поколения и публикация нового состояния |
|
||||
|
||||
В `owniverse.gd` остались создание состояния и модулей, скорость и проверка входного delta перед запуском эволюции. Дублирующее поле new_state и закомментированная старая реализация удалены. Единственный источник текущего состояния — `world.state`.
|
||||
|
||||
### Порядок шага
|
||||
|
||||
1. Определить обработанную десятую года. При продвижении времени восстановить недостающие партии жизни и проверить вместимость.
|
||||
2. Скопировать состояние, посчитать физические количества и производство за период.
|
||||
3. Обработать поколения до конца интервала. Для каждого поколения: локальные разрушения → выбытие партий всех типов → план переходов → резервирование передачи привязок → зачисление преемников и их партий → производство новорождённых → применение привязок и округление → завершение переходов цивилизаций → глобальные бласты → проверка вместимости.
|
||||
4. Обновить групповые количества, ограничить автоматические ресурсы и продвинуть параметры мира.
|
||||
5. Опубликовать новое состояние и обновить представление количеств.
|
||||
|
||||
Резервирование привязок использует замороженную таблицу, чтобы одновременные переходы не зависели от порядка обработки владельцев. Коммит передач происходит после зачисления преемников. Оплата производства остаётся атомарной: при нехватке ресурсов исходное состояние не меняется.
|
||||
|
||||
### Зависимости и жизненный цикл
|
||||
|
||||
Модули симуляции получают свой Owniverse через `simulation_module.gd`. Они работают с переданным target либо с актуальным `world.state`; ссылка на старое состояние не кэшируется. Обращение к другим подсистемам явное, например `world.ledger.credit(...)` или `world.batches.append_batch(...)`. Внутренние вызовы идут напрямую.
|
||||
|
||||
Ссылка модуля на мир слабая, поэтому цикл владения не возникает. Owniverse владеет модулями, каждый новый мир получает собственные экземпляры. Использовать модуль после освобождения его мира нельзя. В сохранение попадает только состояние; модули, каталог и UI туда не сериализуются. Определения OwniverseEntity по-прежнему освобождает владелец сцены.
|
||||
|
||||
### Примеры нового API
|
||||
|
||||
```gdscript
|
||||
var available = world.production.calculate_max_producible(world.catalog.entities["YD"])
|
||||
world.production.produce("YD", 10.0)
|
||||
var physical = world.ledger.physical_amounts(world.state)
|
||||
var displayed = world.presentation.amounts(world.state, world.catalog)
|
||||
world.environment.modify_world_parameter("Bur", 1.0)
|
||||
world.evolute(delta)
|
||||
```
|
||||
|
||||
Проверки проверяют прежние формулы через новый API; сравнение ускоренного и обычного путей выполняется через `stellar.direct_completions`. Дополнительный тест `test_module_lifecycle.gd` проверяет изоляцию миров и освобождение модулей.
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
func yearly_products(entity: OwniverseEntity) -> Dictionary:
|
||||
var products: Dictionary = {}
|
||||
if not entity.life_product.is_empty() and entity.lifespan > 0.0:
|
||||
for resource in entity.life_product:
|
||||
products[resource] = maxf(0.0, entity.life_product[resource]) / entity.lifespan
|
||||
elif entity.product_per_year_type != "":
|
||||
products[entity.product_per_year_type] = maxf(0.0, entity.product_per_year_amount)
|
||||
return products
|
||||
|
||||
|
||||
func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Dictionary,
|
||||
distribution: Dictionary, from_year: float, to_year: float) -> void:
|
||||
var basic: Dictionary = {}
|
||||
var automatic: Dictionary = {}
|
||||
for id in quantities:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
var object_years: float = world.batches.object_years(previous, id, quantities[id], from_year, to_year)
|
||||
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] * 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.
|
||||
if not target.has("resource_remainders"):
|
||||
target.resource_remainders = {"basic": {}, "automatic": {}}
|
||||
var pending: Dictionary = target.resource_remainders
|
||||
var requests: Dictionary = {}
|
||||
for id in basic:
|
||||
pending.basic[id] = pending.basic.get(id, 0.0) + basic[id]
|
||||
pending.automatic[id] = pending.automatic.get(id, 0.0) + automatic[id]
|
||||
var totals: Dictionary = {}
|
||||
for channel in ["basic", "automatic"]:
|
||||
for id in pending[channel]:
|
||||
totals[id] = maxf(0.0, pending.basic.get(id, 0.0) + pending.automatic.get(id, 0.0))
|
||||
for id in totals:
|
||||
if id == "S" or id == "H":
|
||||
requests[id] = floorf(totals[id])
|
||||
var funded: Dictionary = QuantityMath.allocate_whole(requests, floorf(maxf(0.0, previous.energy)))
|
||||
automatic = {}
|
||||
target.energy = previous.energy
|
||||
for id in totals:
|
||||
var amount: float = floorf(totals[id])
|
||||
if id == "S" or id == "H":
|
||||
amount = funded.get(id, 0.0)
|
||||
target.energy -= amount
|
||||
# Split the actual whole output, retaining the allocation error for next time.
|
||||
var auto_ratio: float = clampf(pending.automatic.get(id, 0.0) / totals[id], 0.0, 1.0) if totals[id] > 0.0 else 0.0
|
||||
var auto_amount: float = floorf(amount * auto_ratio)
|
||||
pending.automatic[id] = pending.automatic.get(id, 0.0) - auto_amount
|
||||
pending.basic[id] = pending.basic.get(id, 0.0) - (amount - auto_amount)
|
||||
automatic[id] = auto_amount
|
||||
amount = world.ledger.credit(target, id, amount)
|
||||
world.batches.append_batch(target, id, amount, from_year, to_year)
|
||||
|
||||
# Keep unused automatic resources across cycles; they are part of free amounts.
|
||||
var budget: Dictionary = previous.get("auto_resources", {}).duplicate(true)
|
||||
for id in automatic:
|
||||
budget[id] = budget.get(id, 0.0) + automatic[id]
|
||||
for id in budget:
|
||||
budget[id] = minf(maxf(0.0, budget[id]), world.ledger.available(target, id))
|
||||
target.auto_resources = budget
|
||||
produce_brown_dwarfs(target, automatic.get("H", 0.0), from_year, to_year)
|
||||
budget = ordinary_star_budget(target)
|
||||
|
||||
# One common multiplier preserves the initial count ratios, even under scarcity.
|
||||
var candidates: Dictionary = {}
|
||||
for id in distribution:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
if entity.group_as != "Star":
|
||||
continue
|
||||
var has_cost: bool = false
|
||||
for resource in entity.cost:
|
||||
if entity.cost[resource] > 0.0:
|
||||
has_cost = true
|
||||
if has_cost:
|
||||
candidates[id] = distribution[id]
|
||||
candidates = QuantityMath.normalize_distribution(candidates)
|
||||
if candidates.is_empty():
|
||||
return
|
||||
var costs: Dictionary = {}
|
||||
var needs: Dictionary = {}
|
||||
for id in candidates:
|
||||
if auto_capacity(target, world.catalog.entities[id], budget) <= 0.0:
|
||||
continue
|
||||
for resource in world.catalog.entities[id].cost:
|
||||
var cost: float = maxf(0.0, world.catalog.entities[id].cost[resource]) * candidates[id]
|
||||
costs[resource] = costs.get(resource, 0.0) + cost
|
||||
needs[resource] = needs.get(resource, 0.0) + cost
|
||||
for resource in world.catalog.entities[id].bind:
|
||||
needs[resource] = needs.get(resource, 0.0) + maxf(0.0, world.catalog.entities[id].bind[resource]) * candidates[id]
|
||||
var scale: float = INF
|
||||
for resource in costs:
|
||||
if costs[resource] > 0.0:
|
||||
scale = minf(scale, budget.get(resource, 0.0) / costs[resource])
|
||||
for resource in needs:
|
||||
if needs[resource] > 0.0:
|
||||
scale = minf(scale, world.ledger.available(target, resource) / needs[resource])
|
||||
var concrete_needs: Dictionary = world.ledger.expand_requirements(target, needs)
|
||||
for resource in concrete_needs:
|
||||
if concrete_needs[resource] > 0.0:
|
||||
scale = minf(scale, world.ledger.available(target, resource) / concrete_needs[resource])
|
||||
if not is_finite(scale) or scale <= 0.0:
|
||||
return
|
||||
# Rare types are served first; signed count deficits compensate later cycles.
|
||||
if not target.has("auto_star_deficits"):
|
||||
target.auto_star_deficits = {}
|
||||
var deficits: Dictionary = target.auto_star_deficits
|
||||
var order: Array = candidates.keys()
|
||||
order.sort_custom(func(a: String, b: String) -> bool:
|
||||
return candidates[a] < candidates[b] if candidates[a] != candidates[b] else a < b)
|
||||
var created: Dictionary = {}
|
||||
var metallicity: float = world.environment.stellar_environment(target).metallicity
|
||||
var total_created: float = 0.0
|
||||
for id in order:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
var step: float = world.production.production_step(entity)
|
||||
if not is_finite(step):
|
||||
continue
|
||||
var quota: float = maxf(0.0, candidates[id] * scale + deficits.get(id, 0.0))
|
||||
var desired: float = floorf(quota / step) * step
|
||||
var actual: float = minf(desired, auto_capacity(target, entity, budget))
|
||||
if actual <= 0.0:
|
||||
continue
|
||||
if not world.production.try_production(target, entity, actual, true):
|
||||
continue
|
||||
budget = ordinary_star_budget(target)
|
||||
created[id] = actual
|
||||
total_created += actual
|
||||
for id in candidates:
|
||||
# Credit only actual production, so retained resources aren't counted twice.
|
||||
deficits[id] = deficits.get(id, 0.0) + candidates[id] * total_created - created.get(id, 0.0)
|
||||
for id in created:
|
||||
world.ledger.credit(target, id, created[id])
|
||||
world.batches.append_batch(target, id, created[id], from_year, to_year)
|
||||
world.stellar.create_planets(target, id, created[id], from_year, to_year, metallicity)
|
||||
|
||||
|
||||
func produce_brown_dwarfs(target: Dictionary, new_hydrogen: float, from_year: float, to_year: float) -> void:
|
||||
if not world.catalog.entities.has("HBD"):
|
||||
return
|
||||
var entity: OwniverseEntity = world.catalog.entities.HBD
|
||||
var cost: float = entity.cost.get("H", 0.0)
|
||||
if cost <= 0.0:
|
||||
return
|
||||
var quota: float = target.auto_star_deficits.get("HBD", 0.0) + maxf(0.0, new_hydrogen) * 0.001 / cost
|
||||
var fuel: float = maxf(0.0, target.auto_star_deficits.get("HBD_fuel", 0.0))
|
||||
var available: float = minf(target.auto_resources.get("H", 0.0), world.ledger.available(target, "H"))
|
||||
quota = minf(quota, (available + fuel) / cost)
|
||||
var expected: float = maxf(0.0, snappedf(quota, 1e-9))
|
||||
var amount: float = floorf(expected)
|
||||
# Do not retry a fractional chance on frames with no new automatic fuel.
|
||||
if new_hydrogen > 0.0 and expected > amount:
|
||||
var random_state: float = target.auto_star_deficits.get("HBD_random", 1234567.0)
|
||||
random_state = fmod(random_state * 48271.0, 2147483647.0)
|
||||
target.auto_star_deficits.HBD_random = random_state
|
||||
if random_state / 2147483647.0 < expected - amount:
|
||||
amount += 1.0
|
||||
amount = minf(amount, floorf(snappedf((available + fuel) / cost, 1e-9)))
|
||||
var charge: float = ceilf(maxf(0.0, snappedf(amount * cost - fuel, 1e-9)))
|
||||
if amount > 0.0 and charge <= available:
|
||||
world.ledger.take_available(target, "H", charge)
|
||||
target.auto_resources.H = maxf(0.0, target.auto_resources.get("H", 0.0) - charge)
|
||||
target.auto_star_deficits.HBD_fuel = maxf(0.0, snappedf(fuel + charge - amount * cost, 1e-9))
|
||||
quota -= amount
|
||||
world.ledger.credit(target, "HBD", amount)
|
||||
world.batches.append_batch(target, "HBD", amount, from_year, to_year)
|
||||
target.auto_star_deficits.HBD = quota
|
||||
|
||||
|
||||
func ordinary_star_budget(target: Dictionary) -> Dictionary:
|
||||
var budget: Dictionary = target.auto_resources.duplicate()
|
||||
if world.catalog.entities.has("HBD"):
|
||||
var reserved: float = maxf(0.0, target.auto_star_deficits.get("HBD", 0.0) * world.catalog.entities.HBD.cost.get("H", 0.0) - target.auto_star_deficits.get("HBD_fuel", 0.0))
|
||||
budget.H = maxf(0.0, budget.get("H", 0.0) - reserved)
|
||||
return budget
|
||||
|
||||
|
||||
func auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Dictionary) -> float:
|
||||
var capacity: float = INF
|
||||
var needs: Dictionary = {}
|
||||
for resource in entity.cost:
|
||||
var cost: float = maxf(0.0, entity.cost[resource])
|
||||
needs[resource] = cost
|
||||
if cost > 0.0:
|
||||
capacity = minf(capacity, floorf(budget.get(resource, 0.0) / cost))
|
||||
for resource in entity.bind:
|
||||
needs[resource] = needs.get(resource, 0.0) + maxf(0.0, entity.bind[resource])
|
||||
for resource in needs:
|
||||
if needs[resource] > 0.0:
|
||||
capacity = minf(capacity, floorf(world.ledger.available(source, resource) / needs[resource]))
|
||||
var concrete: Dictionary = world.ledger.expand_requirements(source, needs)
|
||||
for resource in concrete:
|
||||
if concrete[resource] > 0.0:
|
||||
capacity = minf(capacity, floorf(world.ledger.available(source, resource) / concrete[resource]))
|
||||
var step: float = world.production.production_step(entity)
|
||||
return floorf(capacity / step) * step if is_finite(capacity) and is_finite(step) else 0.0
|
||||
|
||||
|
||||
func produce_newborn_lifetime(target: Dictionary, id: String, count: float,
|
||||
born_from: float, born_to: float, to_year: float, before_batches: Array,
|
||||
remaining_object_years: float = NAN) -> void:
|
||||
var products: Dictionary = yearly_products(world.catalog.entities[id])
|
||||
if products.is_empty() or count <= 0.0:
|
||||
return
|
||||
var object_years: float = remaining_object_years if is_finite(remaining_object_years) else count * maxf(0.0, to_year - (born_from + born_to) * 0.5)
|
||||
var previous_amounts: Dictionary = {}
|
||||
for old: Dictionary in before_batches:
|
||||
var start: float = old.get("created_from", -INF)
|
||||
if not previous_amounts.has(start):
|
||||
previous_amounts[start] = old.amount
|
||||
for batch: Dictionary in target.batches.get(id, []):
|
||||
var added: float = batch.amount - previous_amounts.get(batch.get("created_from", INF), 0.0)
|
||||
object_years -= maxf(0.0, added) * world.batches.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] * WorldParameters.production(target, world.catalog.entities[id])
|
||||
if resource in ["H", "S"]:
|
||||
output = minf(output, target.energy)
|
||||
var actual: float = world.ledger.credit(target, resource, output)
|
||||
if resource in ["H", "S"]:
|
||||
target.energy = maxf(0.0, target.energy - actual)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cty3i0pb8ihwf
|
||||
@@ -72,7 +72,7 @@ static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop
|
||||
var removed: Dictionary = take(world, pop, count)
|
||||
var actual: float = total(removed)
|
||||
if actual > 0.0:
|
||||
world._scale_batches(target, plan.id, (old_total - actual) / old_total)
|
||||
world.batches.scale_batches(target, plan.id, (old_total - actual) / old_total)
|
||||
plan.amount += actual
|
||||
plan.to = maxf(plan.to, year)
|
||||
return removed
|
||||
@@ -164,7 +164,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
||||
if not incoming.has(destination): incoming[destination] = {}
|
||||
add(incoming[destination], pop)
|
||||
products[destination] = total(pop)
|
||||
if State.is_space_civilization(id, rules) and world.entities[destination].group_as == "Planet":
|
||||
if State.is_space_civilization(id, rules) and world.catalog.entities[destination].group_as == "Planet":
|
||||
planetary[destination] = planetary.get(destination, 0.0) + total(pop)
|
||||
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": maxf(plan.from, plan.to), "products": products}]
|
||||
return {"remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies}
|
||||
@@ -175,10 +175,10 @@ static func finish(world, target: Dictionary, transaction: Dictionary, year: flo
|
||||
if not target.ethics.has(id): target.ethics[id] = {}
|
||||
add(target.ethics[id], transaction.incoming[id])
|
||||
# Space civilizations that fall back to planetary stages need a stellar host.
|
||||
var physical: Dictionary = world._physical_amounts(target)
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
var hosts: Dictionary = {}
|
||||
for id in physical:
|
||||
if world.entities[id].event_group == "DwStar" and physical[id] > 0.0: hosts[id] = physical[id]
|
||||
if world.catalog.entities[id].event_group == "DwStar" and physical[id] > 0.0: hosts[id] = physical[id]
|
||||
for id in transaction.planetary:
|
||||
var shares: Dictionary = split(target, "return:" + id, minf(transaction.planetary[id], target.amount.get(id, 0.0)), hosts)
|
||||
for owner in shares:
|
||||
@@ -194,7 +194,7 @@ static func finish(world, target: Dictionary, transaction: Dictionary, year: flo
|
||||
target.civilization_stats.worship = worship
|
||||
|
||||
static func colonize(world, target: Dictionary, colonies: Array, year: float, rules: Dictionary) -> void:
|
||||
var physical: Dictionary = world._physical_amounts(target)
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
var stock: Dictionary = {}
|
||||
for id in ["GG", "AB", "RP", "HP"]: stock[id] = physical.get(id, 0.0)
|
||||
var requests: Dictionary = {}
|
||||
@@ -212,17 +212,17 @@ static func colonize(world, target: Dictionary, colonies: Array, year: float, ru
|
||||
var slots: Dictionary = {"": target.amount.get(id, 0.0)}
|
||||
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0.0)
|
||||
var taken: Dictionary = QuantityMath.allocate_whole(slots, amount)
|
||||
world._scale_batches(target, id, 1.0 - amount / stock[id])
|
||||
world.batches.scale_batches(target, id, 1.0 - amount / stock[id])
|
||||
stock[id] -= amount
|
||||
for owner in taken:
|
||||
if taken[owner] <= 0.0: continue
|
||||
if owner == "": target.amount[id] -= taken[owner]
|
||||
else: target.binds[owner][id] -= taken[owner]
|
||||
var created: float = world._credit(target, colony.stage, taken[owner])
|
||||
var created: float = world.ledger.credit(target, colony.stage, taken[owner])
|
||||
if owner != "":
|
||||
target.amount[colony.stage] -= created
|
||||
target.binds[owner][colony.stage] = target.binds[owner].get(colony.stage, 0.0) + created
|
||||
world._append_batch(target, colony.stage, amount, year, year)
|
||||
world.batches.append_batch(target, colony.stage, amount, year, year)
|
||||
if not target.ethics.has(colony.stage): target.ethics[colony.stage] = {}
|
||||
add(target.ethics[colony.stage], population)
|
||||
target.civilization_stats.colonies = target.civilization_stats.get("colonies", 0.0) + count
|
||||
|
||||
@@ -12,7 +12,7 @@ static func is_space_civilization(id: String, rules: Dictionary) -> bool:
|
||||
static func reconcile(world, target: Dictionary, rules: Dictionary) -> void:
|
||||
if not target.has("ethics"):
|
||||
target.ethics = {}
|
||||
var physical: Dictionary = world._physical_amounts(target)
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
for id in rules:
|
||||
var count: float = physical.get(id, 0.0)
|
||||
var current: Dictionary = target.ethics.get(id, {})
|
||||
@@ -40,10 +40,10 @@ static func detach_space_civilizations(target: Dictionary, rules: Dictionary) ->
|
||||
# A shared budget per stellar family, including stars bound into structures.
|
||||
# Reservations affect capacity, not the physical stock or its production.
|
||||
static func stellar_budget(world, target: Dictionary, rules: Dictionary) -> Dictionary:
|
||||
var physical: Dictionary = world._physical_amounts(target)
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
var capacity: Dictionary = {}
|
||||
for id in physical:
|
||||
var entity = world.entities[id]
|
||||
var entity = world.catalog.entities[id]
|
||||
for home in ["DwStar", "GiStar", "WD", "NS", "MGT", "BH"]:
|
||||
if entity.event_group == home or entity.count_as == home or id == home:
|
||||
capacity[home] = capacity.get(home, 0.0) + physical[id]
|
||||
@@ -129,10 +129,10 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
||||
if excess <= 0.0:
|
||||
continue
|
||||
var previous: String = stages[index - 1] if index > 0 else "C1.9"
|
||||
world._scale_batches(target, id, supported / count)
|
||||
world.batches.scale_batches(target, id, supported / count)
|
||||
target.amount[id] = supported
|
||||
world._credit(target, previous, excess)
|
||||
world._append_batch(target, previous, excess, year, year)
|
||||
world.ledger.credit(target, previous, excess)
|
||||
world.batches.append_batch(target, previous, excess, year, year)
|
||||
var removed: Dictionary = QuantityMath.allocate_whole(target.ethics[id], excess)
|
||||
if not target.ethics.has(previous):
|
||||
target.ethics[previous] = {}
|
||||
@@ -141,9 +141,9 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
||||
target.ethics[previous][ethic] = target.ethics[previous].get(ethic, 0.0) + removed[ethic]
|
||||
if previous == "C1.9":
|
||||
var stars: Dictionary = {}
|
||||
var physical: Dictionary = world._physical_amounts(target)
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
for star in physical:
|
||||
if world.entities[star].event_group == "DwStar" and physical[star] > 0.0:
|
||||
if world.catalog.entities[star].event_group == "DwStar" and physical[star] > 0.0:
|
||||
stars[star] = physical[star]
|
||||
var weights: Dictionary = QuantityMath.normalize_distribution(stars)
|
||||
# Worlds are aggregates: a star may host several inhabited planets.
|
||||
@@ -164,16 +164,16 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
||||
# Unity's structure chain: direct Claim resets the required structure count;
|
||||
# intervening stages require ten times more units of the preceding structure.
|
||||
static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dictionary:
|
||||
var physical: Dictionary = world._physical_amounts(target)
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
var structures: Array = []
|
||||
var capacity: Dictionary = {}
|
||||
for id in world.entities:
|
||||
if world.entities[id].claim != "":
|
||||
for id in world.catalog.entities:
|
||||
if world.catalog.entities[id].claim != "":
|
||||
structures.append(id)
|
||||
capacity[id] = physical.get(id, 0.0)
|
||||
structures.sort_custom(func(a, b):
|
||||
var left: float = world.entities[a].civ_bonus
|
||||
var right: float = world.entities[b].civ_bonus
|
||||
var left: float = world.catalog.entities[a].civ_bonus
|
||||
var right: float = world.catalog.entities[b].civ_bonus
|
||||
return left > right if left != right else str(a) < str(b))
|
||||
var stages: Array = []
|
||||
for id in rules:
|
||||
@@ -186,7 +186,7 @@ static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dict
|
||||
for id in stages:
|
||||
var direct: String = ""
|
||||
for structure in structures:
|
||||
if world.entities[structure].claim == id: direct = structure
|
||||
if world.catalog.entities[structure].claim == id: direct = structure
|
||||
if direct != "":
|
||||
anchor = direct
|
||||
units = 1.0
|
||||
@@ -216,7 +216,7 @@ static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
|
||||
if structure == budget.anchors[id]: allowed = true
|
||||
if not allowed or budget.capacity[structure] <= 0.0: continue
|
||||
var amount: float = left
|
||||
if world.entities[structure].claim == id:
|
||||
if world.catalog.entities[structure].claim == id:
|
||||
amount = minf(amount, maxf(0.0, budget.capacity[structure] - used.get(structure, 0.0)))
|
||||
used[structure] = used.get(structure, 0.0) + amount
|
||||
if amount > 0.0: homes[structure] = amount
|
||||
@@ -225,7 +225,7 @@ static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
|
||||
target.civilization_homes[id] = homes
|
||||
|
||||
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> float:
|
||||
if not rules.has(id): return world.entities[id].lifespan
|
||||
if not rules.has(id): return world.catalog.entities[id].lifespan
|
||||
var base: float = rules[id].span
|
||||
if not is_space_civilization(id, rules): return base
|
||||
assign_homes(world, target, rules)
|
||||
@@ -233,5 +233,5 @@ static func effective_span(world, target: Dictionary, id: String, rules: Diction
|
||||
if population <= 0.0: return base
|
||||
var weighted: float = population
|
||||
for home in target.civilization_homes.get(id, {}):
|
||||
weighted += target.civilization_homes[id][home] * (maxf(0.0, world.entities[home].civ_bonus) - 1.0)
|
||||
weighted += target.civilization_homes[id][home] * (maxf(0.0, world.catalog.entities[home].civ_bonus) - 1.0)
|
||||
return base * population / weighted if weighted > 0.0 else base
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
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")
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
func initialize_life_batches() -> void:
|
||||
CivilizationState.detach_space_civilizations(world.state, Global.CivilizationRules)
|
||||
CivilizationState.enforce_capacity(world, world.state, Global.CivilizationRules, world.state.time.datetime)
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
for id in world.catalog.entities:
|
||||
if world.catalog.entities[id].life_rule.is_empty():
|
||||
continue
|
||||
var tracked: float = 0.0
|
||||
for batch: Dictionary in world.state.batches.get(id, []):
|
||||
tracked += batch.amount
|
||||
world.batches.append_batch(world.state, id, maxf(0.0, physical.get(id, 0.0) - tracked), world.state.time.datetime, world.state.time.datetime)
|
||||
|
||||
|
||||
func evolute_to_datetime(datetime: float) -> void:
|
||||
# Reconcile only when simulation advances.
|
||||
# Fractional frame updates cannot change capacity or populations.
|
||||
var processed_year: float = SimulationTime.processed_year(datetime)
|
||||
if processed_year > world.state.time.year:
|
||||
initialize_life_batches()
|
||||
var previous: Dictionary = world.state
|
||||
var next: Dictionary = previous.duplicate_deep()
|
||||
next.version = SimulationTime.STATE_VERSION
|
||||
next.speed = clampi(int(previous.speed), 1, Global.SPEED_LIMIT)
|
||||
next.speed_in_year = pow(10.0, next.speed - 1)
|
||||
var from_year: float = previous.time.year
|
||||
var to_year: float = processed_year
|
||||
if to_year > from_year:
|
||||
var quantities: Dictionary = world.ledger.physical_amounts(previous)
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
|
||||
next.distribution = distribution
|
||||
|
||||
world.automatic.produce_for_period(previous, next, quantities, distribution, from_year, to_year)
|
||||
evolve_batches(next, to_year)
|
||||
world.ledger.refresh_groups(next)
|
||||
world.ledger.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.
|
||||
world.state = next
|
||||
if to_year > from_year:
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
|
||||
|
||||
func evolve_batches(target: Dictionary, to_year: float) -> void:
|
||||
# A long cycle may contain several generations; preserve actual birth intervals.
|
||||
var completed: Dictionary = {}
|
||||
while evolve_batch_generation(target, to_year, completed):
|
||||
pass
|
||||
|
||||
|
||||
func evolve_batch_generation(target: Dictionary, to_year: float, completed: Dictionary = {}) -> bool:
|
||||
# Apply direct completions at the original next-generation boundary, so
|
||||
# binding transfers, the blast environment and capacity checks keep order.
|
||||
var completed_now: Dictionary = completed.duplicate(true)
|
||||
completed.clear()
|
||||
var quantities: Dictionary = world.ledger.physical_amounts(target)
|
||||
var expired: Dictionary = {}
|
||||
var preview: Dictionary = {"batches": {}}
|
||||
for id in target.batches:
|
||||
if world.catalog.entities.has(id) and world.catalog.entities[id].destroy_level > 0:
|
||||
preview.batches[id] = target.batches[id].duplicate(true)
|
||||
for id in target.batches.keys():
|
||||
if not world.catalog.entities.has(id) or world.catalog.entities[id].destroy_level <= 0:
|
||||
continue
|
||||
var dying: float = 0.0
|
||||
for part: Dictionary in completed_now.get(id, []):
|
||||
dying += part.amount
|
||||
for part: Dictionary in world.batches.withdraw_batches(preview, id, to_year):
|
||||
dying += part.amount
|
||||
if quantities.get(id, 0.0) > 0.0:
|
||||
world.stellar.destroy_planets(target, id, dying / quantities[id], world.catalog.entities[id].destroy_level)
|
||||
quantities = world.ledger.physical_amounts(target)
|
||||
# Withdraw all types before creating successors: iteration order cannot affect them.
|
||||
for id in target.batches.keys():
|
||||
if world.catalog.entities.has(id):
|
||||
expired[id] = completed_now.get(id, []) + world.batches.withdraw_batches(target, id, to_year)
|
||||
var has_expired: bool = false
|
||||
for parts: Array in expired.values():
|
||||
if not parts.is_empty():
|
||||
has_expired = true
|
||||
break
|
||||
if not has_expired:
|
||||
return false
|
||||
var fractions: Dictionary = {}
|
||||
var outputs: Dictionary = {}
|
||||
var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules)
|
||||
var civilization_transaction: Dictionary = CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules)
|
||||
for id in expired:
|
||||
var total: float = 0.0
|
||||
for part: Dictionary in expired[id]:
|
||||
total += part.amount
|
||||
fractions[id] = clampf(total / quantities[id], 0.0, 1.0) if quantities.get(id, 0.0) > 0.0 else 0.0
|
||||
var combined: Dictionary = {}
|
||||
for part: Dictionary in expired[id]:
|
||||
if not part.has("products"):
|
||||
part.products = world.stellar.evolution_products(target, world.catalog.entities[id], part.amount)
|
||||
for successor in part.products.keys():
|
||||
if Global.CivilizationRules.has(id):
|
||||
continue
|
||||
var requested: float = part.products[successor]
|
||||
var admitted: float = CivilizationState.admit_transition(star_budget, Global.CivilizationRules, id, successor, requested)
|
||||
part.products[successor] = admitted
|
||||
if admitted < requested:
|
||||
part.products[id] = part.products.get(id, 0.0) + requested - admitted
|
||||
for successor in part.products:
|
||||
combined[successor] = combined.get(successor, 0.0) + part.products[successor]
|
||||
outputs[id] = {}
|
||||
if total > 0.0:
|
||||
for successor in combined:
|
||||
outputs[id][successor] = combined[successor] / total
|
||||
|
||||
var incoming: Dictionary = world.ledger.transfer_evolution(target, fractions, outputs)
|
||||
|
||||
var new_owners: Dictionary = {}
|
||||
var blast_power: float = 0.0
|
||||
var environment: Dictionary = world.environment.stellar_environment(target)
|
||||
for id in expired:
|
||||
for part: Dictionary in expired[id]:
|
||||
for successor in part.products:
|
||||
var amount: float = part.products[successor]
|
||||
if successor == "blast":
|
||||
blast_power += amount
|
||||
continue
|
||||
if not world.catalog.entities.has(successor):
|
||||
continue
|
||||
if successor == id and (not world.catalog.entities[id].life_rule.is_empty() or Global.CivilizationRules.has(id)):
|
||||
target.amount[id] = target.amount.get(id, 0.0) + amount
|
||||
else:
|
||||
amount = world.ledger.credit(target, successor, amount)
|
||||
var before_batches: Array = target.batches.get(successor, []).duplicate(true)
|
||||
var cutoff: float = to_year if world.stellar.uses_short_lived_fast_path(successor) else -INF
|
||||
var integral: float = world.batches.append_batch(target, successor, amount, part.from, part.to, completed, cutoff)
|
||||
world.automatic.produce_newborn_lifetime(target, successor, amount, part.from, part.to, to_year, before_batches, integral)
|
||||
new_owners[successor] = new_owners.get(successor, 0.0) + amount
|
||||
world.ledger.accept_transfers(target, incoming)
|
||||
world.ledger.bind_new_owners(target, new_owners)
|
||||
world.ledger.round_object_allocations(target)
|
||||
CivilizationEvolution.finish(world, target, civilization_transaction, to_year, Global.CivilizationRules)
|
||||
world.stellar.apply_blast(target, blast_power, environment)
|
||||
CivilizationState.enforce_capacity(world, target, Global.CivilizationRules, to_year)
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://bok6yra83r83c
|
||||
@@ -18,8 +18,8 @@ static func colored(text: String, color: String) -> String:
|
||||
|
||||
static func matches(world, id: String) -> Array:
|
||||
var result: Array = []
|
||||
for key in world.entities:
|
||||
var entity = world.entities[key]
|
||||
for key in world.catalog.entities:
|
||||
var entity = world.catalog.entities[key]
|
||||
if key == id or entity.count_as == id or entity.group_as == id:
|
||||
if not world.catalog.is_group(key):
|
||||
result.append(key)
|
||||
@@ -28,7 +28,7 @@ static func matches(world, id: String) -> Array:
|
||||
static func quantities(world, id: String) -> Dictionary:
|
||||
var free: float = 0.0
|
||||
var total: float = 0.0
|
||||
var physical: Dictionary = world._physical_amounts(world.state)
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
for key in matches(world, id):
|
||||
free += world.state.amount.get(key, 0.0)
|
||||
total += physical.get(key, 0.0)
|
||||
@@ -38,15 +38,15 @@ static func quantities(world, id: String) -> Dictionary:
|
||||
return {"free": free, "total": total}
|
||||
|
||||
static func costs(world, global: Node, id: String) -> String:
|
||||
if not world.entities.has(id):
|
||||
if not world.catalog.entities.has(id):
|
||||
return ""
|
||||
var lines: PackedStringArray = []
|
||||
for requirements in [world.entities[id].cost, world.entities[id].bind]:
|
||||
for requirements in [world.catalog.entities[id].cost, world.catalog.entities[id].bind]:
|
||||
for resource in requirements:
|
||||
var required: float = requirements[resource]
|
||||
if required < 0.0:
|
||||
continue
|
||||
var available: float = floorf(world._available(world.state, resource))
|
||||
var available: float = floorf(world.ledger.available(world.state, resource))
|
||||
var name: String = str(global.get_template(resource, "Entity", "Name").get("text", resource))
|
||||
lines.append(colored(name + ": " + number(required) + " (" + number(available) + ")", GREEN if available >= required else RED))
|
||||
return "\n".join(lines)
|
||||
@@ -55,25 +55,25 @@ static func variables(world, global: Node, id: String, rate: float = 0.0) -> Dic
|
||||
var counts: Dictionary = quantities(world, id)
|
||||
var show_total: bool = false
|
||||
for key in matches(world, id):
|
||||
show_total = show_total or world.entities[key].group_as == "Planet"
|
||||
var amount: String = number(counts.total if show_total else counts.free) if world.entities.has(id) else "—"
|
||||
show_total = show_total or world.catalog.entities[key].group_as == "Planet"
|
||||
var amount: String = number(counts.total if show_total else counts.free) if world.catalog.entities.has(id) else "—"
|
||||
if not show_total and counts.total > counts.free:
|
||||
amount += " " + colored("(" + number(counts.total) + ")", GREY)
|
||||
if rate != 0.0:
|
||||
amount += " | " + colored(("−" if rate < 0.0 else "+") + number(absf(rate), 1), RED if rate < 0.0 else GREEN)
|
||||
var physical: Dictionary = world._physical_amounts(world.state)
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
var voids: float = 0.0
|
||||
for key in physical:
|
||||
voids += physical[key] * world.entities[key].voids
|
||||
voids += physical[key] * world.catalog.entities[key].voids
|
||||
var he: float = world.state.amount.get("He", 0.0)
|
||||
var h: float = world.state.amount.get("H", 0.0)
|
||||
var mixture: String = "—"
|
||||
if he + h > 0.0:
|
||||
mixture = "He: " + number(100.0 * he / (he + h), 2) + "% / H: " + number(100.0 * h / (he + h), 2) + "%"
|
||||
var environment: Dictionary = world.stellar_environment(world.state)
|
||||
var environment: Dictionary = world.environment.stellar_environment(world.state)
|
||||
return {
|
||||
"amount": amount, "colored costs": costs(world, global, id),
|
||||
"lifespan": number(world.entities[id].lifespan) if world.entities.has(id) else "—",
|
||||
"lifespan": number(world.catalog.entities[id].lifespan) if world.catalog.entities.has(id) else "—",
|
||||
"voids": number(voids), "he / h": "\n" + mixture,
|
||||
"metallicity": number(environment.metallicity * 100.0, 2) + "%",
|
||||
"энергия": number(world.state.energy), "плотность": number(environment.density, 6),
|
||||
@@ -85,7 +85,7 @@ static func render(world, global: Node, id: String, category: String = "Entity",
|
||||
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
|
||||
id = Parameters.density_display(world.environment.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])
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
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 * 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 = QuantityMath.allocate_whole(pending, count)
|
||||
for channel in assigned:
|
||||
var key: String = entity.id + ":" + channel
|
||||
target.life_remainders[key] = maxf(0.0, target.life_remainders[key] - assigned[channel])
|
||||
var result: Dictionary = {entity.id: count - assigned.progress - assigned.degradation}
|
||||
result[rule.next] = assigned.progress
|
||||
var weights: Dictionary = {}
|
||||
var weight_sum: float = 0.0
|
||||
for term in str(rule.fall).split("^"):
|
||||
var pair: PackedStringArray = term.split(":")
|
||||
weights[pair[0]] = float(pair[1])
|
||||
weight_sum += float(pair[1])
|
||||
var requests: Dictionary = {}
|
||||
for id in weights:
|
||||
var key: String = entity.id + ":fall:" + id
|
||||
var expected: float = assigned.degradation * weights[id] / weight_sum + target.life_remainders.get(key, 0.0)
|
||||
target.life_remainders[key] = expected
|
||||
requests[id] = maxf(0.0, floorf(snappedf(expected, 1.0e-12)))
|
||||
var falls: Dictionary = QuantityMath.allocate_whole(requests, assigned.degradation)
|
||||
var distributed: float = 0.0
|
||||
for id in falls:
|
||||
distributed += falls[id]
|
||||
result[id] = result.get(id, 0.0) + falls[id]
|
||||
var key: String = entity.id + ":fall:" + id
|
||||
target.life_remainders[key] -= falls[id]
|
||||
# Fill sub-unit fall quotas deterministically, retaining signed allocation errors.
|
||||
while distributed < assigned.degradation:
|
||||
var selected: String = ""
|
||||
var largest: float = -INF
|
||||
for id in weights:
|
||||
var rest: float = target.life_remainders[entity.id + ":fall:" + id]
|
||||
if rest > largest:
|
||||
largest = rest
|
||||
selected = id
|
||||
result[selected] = result.get(selected, 0.0) + 1.0
|
||||
target.life_remainders[entity.id + ":fall:" + selected] -= 1.0
|
||||
distributed += 1.0
|
||||
return result
|
||||
@@ -0,0 +1 @@
|
||||
uid://chr76vnysheee
|
||||
@@ -0,0 +1,202 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
func try_production(target: Dictionary, entity: OwniverseEntity, amount: float,
|
||||
automatic: bool = false) -> bool:
|
||||
if amount <= 0.0 or amount != floorf(amount):
|
||||
return false
|
||||
var trial: Dictionary = target.duplicate()
|
||||
trial.amount = target.amount.duplicate()
|
||||
trial.binds = target.binds.duplicate(true)
|
||||
trial.auto_resources = target.auto_resources.duplicate()
|
||||
trial.batches = target.batches.duplicate()
|
||||
var copied_batches: Dictionary = {}
|
||||
for resource in entity.cost:
|
||||
var cost: float = roundf(maxf(0.0, entity.cost[resource]) * amount)
|
||||
if automatic and trial.auto_resources.get(resource, 0.0) < cost:
|
||||
return false
|
||||
var before: Dictionary = world.ledger.physical_amounts(trial)
|
||||
var taken: Dictionary = world.ledger.take_available(trial, resource, cost)
|
||||
var total: float = 0.0
|
||||
for quantity in taken.values():
|
||||
total += quantity
|
||||
if total != cost:
|
||||
return false
|
||||
if automatic:
|
||||
trial.auto_resources[resource] = maxf(0.0, trial.auto_resources.get(resource, 0.0) - cost)
|
||||
for id in taken:
|
||||
# Isolate only batches actually touched by this transaction.
|
||||
if trial.batches.has(id) and not copied_batches.has(id):
|
||||
trial.batches[id] = trial.batches[id].duplicate(true)
|
||||
copied_batches[id] = true
|
||||
var factor: float = 1.0 - taken[id] / before[id] if before.get(id, 0.0) > 0.0 else 0.0
|
||||
world.batches.scale_batches(trial, id, factor)
|
||||
if not world.ledger.bind_new_owners(trial, {entity.id: amount}, true):
|
||||
return false
|
||||
world.ledger.refresh_groups(trial)
|
||||
world.ledger.clamp_auto_resources(trial)
|
||||
for field in ["amount", "binds", "batches", "auto_resources"]:
|
||||
if trial.has(field):
|
||||
target[field] = trial[field]
|
||||
return true
|
||||
|
||||
|
||||
func smbh_sources() -> Dictionary:
|
||||
var masses: Dictionary = {}
|
||||
for id in world.catalog.entities:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
if entity.count_as == "BH" and entity.mass > 0.0:
|
||||
masses[id] = floorf(maxf(0.0, world.state.amount.get(id, 0.0))) * entity.mass
|
||||
return masses
|
||||
|
||||
|
||||
func smbh_available() -> float:
|
||||
var mass: float = 0.0
|
||||
for value in smbh_sources().values():
|
||||
mass += value
|
||||
return floorf(mass)
|
||||
|
||||
|
||||
func plan_smbh(desired_amount: float) -> Dictionary:
|
||||
var empty: Dictionary = {"amount": 0.0, "taken": {}}
|
||||
if desired_amount <= 0.0 or not is_finite(desired_amount):
|
||||
return empty
|
||||
var total_mass: float = smbh_available()
|
||||
if total_mass <= 0.0:
|
||||
return empty
|
||||
var requested: float = minf(floorf(desired_amount), total_mass)
|
||||
if requested <= 0.0:
|
||||
return empty
|
||||
var masses: Dictionary = smbh_sources()
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(masses)
|
||||
var taken: Dictionary = {}
|
||||
var order: Array = []
|
||||
var consumed_mass: float = 0.0
|
||||
for id in distribution:
|
||||
var expected: float = requested * distribution[id] / world.catalog.entities[id].mass
|
||||
taken[id] = minf(floorf(expected), floorf(world.state.amount[id]))
|
||||
consumed_mass += taken[id] * world.catalog.entities[id].mass
|
||||
order.append({"id": id, "remainder": expected - taken[id]})
|
||||
order.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a.remainder > b.remainder if a.remainder != b.remainder else str(a.id) < str(b.id))
|
||||
# Consume whole BH only; all excess mass becomes SMBH, never disappears.
|
||||
for candidate: Dictionary in order:
|
||||
if consumed_mass >= requested:
|
||||
break
|
||||
var id: String = candidate.id
|
||||
if taken[id] < floorf(world.state.amount[id]):
|
||||
taken[id] += 1.0
|
||||
consumed_mass += world.catalog.entities[id].mass
|
||||
if consumed_mass <= 0.0 or consumed_mass != floorf(consumed_mass):
|
||||
return empty
|
||||
return {"amount": consumed_mass, "taken": taken}
|
||||
|
||||
|
||||
func produce_smbh(desired_amount: float) -> float:
|
||||
var plan: Dictionary = plan_smbh(desired_amount)
|
||||
var consumed_mass: float = plan.amount
|
||||
var taken: Dictionary = plan.taken
|
||||
if consumed_mass <= 0.0:
|
||||
return 0.0
|
||||
var next: Dictionary = world.state.duplicate(true)
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
for id in taken:
|
||||
if taken[id] <= 0.0:
|
||||
continue
|
||||
next.amount[id] -= taken[id]
|
||||
var fraction: float = taken[id] / physical[id]
|
||||
world.batches.scale_batches(next, id, 1.0 - fraction)
|
||||
# Release the consumed owners' share of any resources they held.
|
||||
for resource in next.binds.get(id, {}):
|
||||
var released: float = next.binds[id][resource] * fraction
|
||||
next.binds[id][resource] -= released
|
||||
next.amount[resource] = next.amount.get(resource, 0.0) + released
|
||||
world.ledger.credit(next, "SMBH", consumed_mass)
|
||||
world.batches.append_batch(next, "SMBH", consumed_mass, world.state.time.datetime, world.state.time.datetime)
|
||||
world.ledger.round_object_allocations(next)
|
||||
world.ledger.refresh_groups(next)
|
||||
world.ledger.clamp_auto_resources(next)
|
||||
world.state = next
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
return consumed_mass
|
||||
|
||||
|
||||
func produce(item_id: String, desired_amount: float) -> float:
|
||||
if item_id == "SMBH":
|
||||
return produce_smbh(desired_amount)
|
||||
# Use special production for S and H
|
||||
if item_id == "S" or item_id == "H":
|
||||
return produce_hs(item_id, desired_amount)
|
||||
|
||||
var entity: OwniverseEntity = world.catalog.entities[item_id]
|
||||
|
||||
if desired_amount <= 0:
|
||||
return 0.0
|
||||
|
||||
var max_producible = floorf(calculate_max_producible(entity, desired_amount))
|
||||
var budget: Dictionary = CivilizationState.stellar_budget(world, world.state, Global.CivilizationRules)
|
||||
max_producible = CivilizationState.admit_transition(budget, Global.CivilizationRules, "", item_id, max_producible)
|
||||
|
||||
if max_producible <= 0:
|
||||
return 0.0
|
||||
|
||||
var metallicity: float = world.environment.stellar_environment(world.state).metallicity
|
||||
if not try_production(world.state, entity, max_producible):
|
||||
return 0.0
|
||||
|
||||
# Add produced entities
|
||||
world.ledger.process_entity(item_id, max_producible)
|
||||
world.batches.add_entity_to_batch(item_id, max_producible, world.state.time.datetime)
|
||||
world.stellar.create_planets(world.state, item_id, max_producible, world.state.time.datetime, world.state.time.datetime, metallicity)
|
||||
world.ledger.refresh_groups(world.state)
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
return max_producible
|
||||
|
||||
|
||||
func produce_hs(entity_id: String, value: float) -> float:
|
||||
if value <= 0:
|
||||
return 0.0
|
||||
|
||||
var to_produce: float = floorf(value)
|
||||
to_produce = minf(to_produce, floorf(world.state.energy))
|
||||
|
||||
if to_produce <= 0:
|
||||
return 0.0
|
||||
|
||||
world.state.energy -= to_produce
|
||||
world.ledger.process_entity(entity_id, to_produce)
|
||||
|
||||
return to_produce
|
||||
|
||||
|
||||
func production_step(entity: OwniverseEntity) -> float:
|
||||
return QuantityMath.production_step(entity.cost.values() + entity.bind.values())
|
||||
|
||||
|
||||
func calculate_max_producible(entity: OwniverseEntity, desired_amount: float = 1.0E150) -> float:
|
||||
if entity.id == "SMBH":
|
||||
return plan_smbh(desired_amount).amount
|
||||
var requirements: Dictionary = {}
|
||||
for resource in entity.cost:
|
||||
requirements[resource] = maxf(0.0, entity.cost[resource])
|
||||
for resource in entity.bind:
|
||||
requirements[resource] = requirements.get(resource, 0.0) + maxf(0.0, entity.bind[resource])
|
||||
var has_requirements: bool = false
|
||||
for quantity in requirements.values():
|
||||
if quantity > 0.0:
|
||||
has_requirements = true
|
||||
break
|
||||
# No recipe means unavailable for production, not unlimited free objects.
|
||||
if not has_requirements:
|
||||
return 0.0
|
||||
var available: float = maxf(0.0, desired_amount)
|
||||
for resource in requirements:
|
||||
if requirements[resource] > 0.0:
|
||||
available = minf(available, floorf(world.ledger.available(world.state, resource) / requirements[resource]))
|
||||
var concrete: Dictionary = world.ledger.expand_requirements(world.state, requirements)
|
||||
for resource in concrete:
|
||||
if concrete[resource] > 0.0:
|
||||
available = minf(available, floorf(world.ledger.available(world.state, resource) / concrete[resource]))
|
||||
var step: float = production_step(entity)
|
||||
return floorf(available / step) * step if is_finite(step) else 0.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://dbxnbd0g6hrab
|
||||
@@ -0,0 +1,181 @@
|
||||
extends "res://world/owniverse/simulation_module.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")
|
||||
|
||||
func scale_batches(target: Dictionary, id: String, factor: float) -> void:
|
||||
var batches: Array = target.batches.get(id, [])
|
||||
var total: float = 0.0
|
||||
var assigned: float = 0.0
|
||||
var shares: Array = []
|
||||
for batch: Dictionary in batches:
|
||||
var expected: float = batch.amount * maxf(0.0, factor)
|
||||
total += expected
|
||||
batch.amount = floorf(expected)
|
||||
assigned += batch.amount
|
||||
shares.append({"batch": batch, "rest": expected - batch.amount})
|
||||
shares.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a.rest > b.rest)
|
||||
for share: Dictionary in shares:
|
||||
if assigned < roundf(total):
|
||||
share.batch.amount += 1.0
|
||||
assigned += 1.0
|
||||
share.batch.withdraw_remainder = minf(share.batch.get("withdraw_remainder", 0.0), share.batch.amount)
|
||||
|
||||
|
||||
func append_batch(target: Dictionary, id: String, amount: float,
|
||||
created_from: float, created_to: float, completed: Dictionary = {},
|
||||
completed_before: float = -INF) -> float:
|
||||
if amount <= 0.0 or not world.catalog.entities.has(id) or world.catalog.entities[id].lifespan <= 0.0:
|
||||
return NAN
|
||||
if created_to < created_from:
|
||||
return NAN
|
||||
if not target.batches.has(id):
|
||||
target.batches[id] = []
|
||||
if not target.has("batch_origins"):
|
||||
target.batch_origins = {}
|
||||
var batches: Array = target.batches[id]
|
||||
var lifespan: float = CivilizationState.effective_span(world, target, id, Global.CivilizationRules)
|
||||
if world.catalog.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
|
||||
if not target.batch_origins.has(id):
|
||||
target.batch_origins[id] = batches[0].get("created_from", created_from) if not batches.is_empty() else created_from
|
||||
var origin: float = target.batch_origins[id]
|
||||
var cursor: float = created_from
|
||||
var duration: float = created_to - created_from
|
||||
var remaining_amount: float = amount
|
||||
var completed_amount: float = 0.0
|
||||
var completed_from: float = INF
|
||||
var completed_to: float = -INF
|
||||
# Subtract virtual windows in their original order to preserve floating-point
|
||||
# rounding of lifetime production, rather than subtracting one large sum.
|
||||
var remaining_object_years: float = amount * maxf(0.0, completed_before - (created_from + created_to) * 0.5) if is_finite(completed_before) else NAN
|
||||
# Compute the index once. Recomputing floor from a rounded boundary can
|
||||
# select the preceding window forever (e.g. fractional origins at large ages).
|
||||
var window_index: float = floorf((cursor - origin) / batch_duration)
|
||||
if origin + (window_index + 1.0) * batch_duration <= cursor:
|
||||
window_index += 1.0
|
||||
var batches_by_start: Dictionary = {}
|
||||
for existing: Dictionary in batches:
|
||||
var start: float = existing.get("created_from", -INF)
|
||||
if not batches_by_start.has(start):
|
||||
batches_by_start[start] = []
|
||||
batches_by_start[start].append(existing)
|
||||
while true:
|
||||
var window_start: float = origin + window_index * batch_duration
|
||||
var window_end: float = origin + (window_index + 1.0) * batch_duration
|
||||
if window_end <= cursor:
|
||||
push_error("Batch window exceeds floating-point time precision: " + id)
|
||||
return remaining_object_years
|
||||
var part_end: float = minf(created_to, window_end)
|
||||
var part_amount: float = remaining_amount
|
||||
if duration > 0.0 and part_end < created_to:
|
||||
part_amount = floorf(amount * (part_end - created_from) / duration) - (amount - remaining_amount)
|
||||
if window_end + lifespan <= completed_before:
|
||||
# Retain the original integer allocation and lifetime integral, but
|
||||
# do not allocate thousands of already-dead batch dictionaries.
|
||||
completed_amount += part_amount
|
||||
completed_from = minf(completed_from, window_start + lifespan)
|
||||
completed_to = maxf(completed_to, window_end + lifespan)
|
||||
remaining_object_years -= part_amount * expired_years(completed_before, window_start + lifespan, window_end + lifespan)
|
||||
else:
|
||||
var batch: Dictionary = {}
|
||||
for existing: Dictionary in batches_by_start.get(window_start, []):
|
||||
if existing.get("created_from", -INF) == window_start and existing.start_year == window_start + lifespan and existing.end_year == window_end + lifespan:
|
||||
batch = existing
|
||||
break
|
||||
if batch.is_empty():
|
||||
batch = {
|
||||
"created_from": window_start,
|
||||
"created_to": window_end,
|
||||
"start_year": window_start + lifespan,
|
||||
"end_year": window_end + lifespan,
|
||||
"amount": 0.0,
|
||||
}
|
||||
batches.append(batch)
|
||||
if not batches_by_start.has(window_start):
|
||||
batches_by_start[window_start] = []
|
||||
batches_by_start[window_start].append(batch)
|
||||
batch.amount += part_amount
|
||||
remaining_amount -= part_amount
|
||||
if duration == 0.0 or part_end >= created_to:
|
||||
break
|
||||
cursor = window_end
|
||||
window_index += 1.0
|
||||
batches.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a.created_from < b.created_from)
|
||||
if completed_amount > 0.0:
|
||||
if not completed.has(id): completed[id] = []
|
||||
completed[id].append({"amount": completed_amount, "from": completed_from, "to": completed_to})
|
||||
return remaining_object_years
|
||||
|
||||
|
||||
func withdraw_batches(target: Dictionary, id: String, to_year: float) -> Array:
|
||||
var expired: Array = []
|
||||
var remaining: Array = []
|
||||
for batch: Dictionary in target.batches.get(id, []):
|
||||
var start: float = maxf(batch.start_year, batch.get("processed_until", batch.start_year))
|
||||
var end: float = batch.end_year
|
||||
var amount: float = batch.amount
|
||||
if amount <= 0.0:
|
||||
continue
|
||||
if to_year < start or (to_year == start and end > start):
|
||||
remaining.append(batch)
|
||||
continue
|
||||
var consumed: float = amount
|
||||
var until: float = minf(to_year, end)
|
||||
if to_year < end:
|
||||
var carry: float = batch.get("withdraw_remainder", 0.0)
|
||||
var expected: float = carry + (amount - carry) * (to_year - start) / (end - start)
|
||||
consumed = floorf(expected)
|
||||
batch.withdraw_remainder = expected - consumed
|
||||
batch.amount = maxf(0.0, amount - consumed)
|
||||
batch.processed_until = to_year
|
||||
remaining.append(batch)
|
||||
if consumed > 0.0:
|
||||
expired.append({"amount": consumed, "from": start, "to": until})
|
||||
target.batches[id] = remaining
|
||||
return expired
|
||||
|
||||
|
||||
func object_years(source: Dictionary, id: String, quantity: float,
|
||||
from_year: float, to_year: float) -> float:
|
||||
var result: float = quantity * SimulationTime.elapsed_years(from_year, to_year)
|
||||
for batch: Dictionary in source.batches.get(id, []):
|
||||
var start: float = maxf(batch.start_year, batch.get("processed_until", batch.start_year))
|
||||
var end: float = batch.end_year
|
||||
result -= batch.amount * (expired_years(to_year, start, end) - expired_years(from_year, start, end))
|
||||
return maxf(0.0, result)
|
||||
|
||||
|
||||
func expired_years(year: float, start: float, end: float) -> float:
|
||||
if year <= start:
|
||||
return 0.0
|
||||
if end <= start:
|
||||
return year - start
|
||||
if year >= end:
|
||||
return year - end + (end - start) * 0.5
|
||||
return (year - start) * (year - start) / (2.0 * (end - start))
|
||||
|
||||
|
||||
func consume_from_batches(entity: OwniverseEntity, current_year: float) -> float:
|
||||
var total: float = 0.0
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
for part: Dictionary in withdraw_batches(world.state, entity.id, current_year):
|
||||
total += part.amount
|
||||
var fraction: float = clampf(total / physical[entity.id], 0.0, 1.0) if physical.get(entity.id, 0.0) > 0.0 else 0.0
|
||||
world.state.amount[entity.id] = world.state.amount.get(entity.id, 0.0) * (1.0 - fraction)
|
||||
for owner_id in world.state.binds:
|
||||
if world.state.binds[owner_id].has(entity.id):
|
||||
world.state.binds[owner_id][entity.id] *= 1.0 - fraction
|
||||
world.ledger.round_object_allocations(world.state)
|
||||
world.ledger.refresh_groups(world.state)
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
return total
|
||||
|
||||
|
||||
func add_entity_to_batch(entity_id: String, amount: float, current_year: float) -> void:
|
||||
append_batch(world.state, entity_id, amount, current_year, current_year)
|
||||
@@ -0,0 +1 @@
|
||||
uid://wyxpfe2amo5q
|
||||
@@ -0,0 +1,287 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
func physical_amounts(source: Dictionary) -> Dictionary:
|
||||
var amounts: Dictionary = {}
|
||||
for id in world.catalog.entities:
|
||||
if not world.catalog.is_group(id):
|
||||
amounts[id] = maxf(0.0, source.amount.get(id, 0.0))
|
||||
for bindings: Dictionary in source.binds.values():
|
||||
for id in bindings:
|
||||
if amounts.has(id):
|
||||
amounts[id] += maxf(0.0, bindings[id])
|
||||
return amounts
|
||||
|
||||
|
||||
func available(source: Dictionary, id: String) -> float:
|
||||
var amount: float = 0.0
|
||||
for member in world.catalog.members(id):
|
||||
amount += maxf(0.0, source.amount.get(member, 0.0))
|
||||
return amount
|
||||
|
||||
|
||||
func expand_requirements(source: Dictionary, requirements: Dictionary) -> Dictionary:
|
||||
var expanded: Dictionary = {}
|
||||
for id in requirements:
|
||||
var weights: Dictionary = {}
|
||||
for member in world.catalog.members(id):
|
||||
weights[member] = source.amount.get(member, 0.0)
|
||||
weights = QuantityMath.normalize_distribution(weights)
|
||||
for member in weights:
|
||||
expanded[member] = expanded.get(member, 0.0) + maxf(0.0, requirements[id]) * weights[member]
|
||||
return expanded
|
||||
|
||||
|
||||
func take_available(target: Dictionary, id: String, amount: float) -> Dictionary:
|
||||
var quantities: Dictionary = {}
|
||||
for member in world.catalog.members(id):
|
||||
quantities[member] = target.amount.get(member, 0.0)
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
|
||||
var taken: Dictionary = {}
|
||||
var total: float = minf(amount, available(target, id))
|
||||
var discrete: bool = not distribution.is_empty()
|
||||
for member in distribution:
|
||||
discrete = discrete and is_discrete(member)
|
||||
if discrete:
|
||||
total = floorf(total)
|
||||
var shares: Array = []
|
||||
var assigned: float = 0.0
|
||||
for member in distribution:
|
||||
var expected: float = total * distribution[member]
|
||||
taken[member] = floorf(expected) if discrete else expected
|
||||
assigned += taken[member]
|
||||
shares.append({"id": member, "rest": expected - taken[member]})
|
||||
if discrete:
|
||||
shares.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
|
||||
for share: Dictionary in shares:
|
||||
if assigned >= total:
|
||||
break
|
||||
taken[share.id] += 1.0
|
||||
assigned += 1.0
|
||||
for member in taken:
|
||||
target.amount[member] = maxf(0.0, target.amount.get(member, 0.0) - taken[member])
|
||||
return taken
|
||||
|
||||
|
||||
func is_discrete(_id: String) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
func credit(target: Dictionary, id: String, amount: float) -> float:
|
||||
if amount <= 0.0:
|
||||
return 0.0
|
||||
if is_discrete(id):
|
||||
if not target.has("production_remainders"):
|
||||
target.production_remainders = {}
|
||||
var expected: float = amount + target.production_remainders.get(id, 0.0)
|
||||
amount = floorf(expected)
|
||||
target.production_remainders[id] = expected - amount
|
||||
target.amount[id] = target.amount.get(id, 0.0) + amount
|
||||
target.stats.total_amount[id] = target.stats.total_amount.get(id, 0.0) + amount
|
||||
if world.catalog.entities.has(id) and world.catalog.entities[id].group_as != "" and world.catalog.entities[id].group_as != id:
|
||||
var group: String = world.catalog.entities[id].group_as
|
||||
target.stats.total_amount[group] = target.stats.total_amount.get(group, 0.0) + amount
|
||||
return amount
|
||||
|
||||
|
||||
func round_object_allocations(target: Dictionary) -> void:
|
||||
var physical: Dictionary = physical_amounts(target)
|
||||
for id in physical:
|
||||
if not is_discrete(id):
|
||||
continue
|
||||
var slots: Array = [ {"owner": "", "value": target.amount.get(id, 0.0)}]
|
||||
for owner_id in target.binds:
|
||||
if target.binds[owner_id].has(id):
|
||||
slots.append({"owner": owner_id, "value": target.binds[owner_id][id]})
|
||||
var assigned: float = 0.0
|
||||
for slot: Dictionary in slots:
|
||||
slot.whole = floorf(slot.value)
|
||||
slot.rest = slot.value - slot.whole
|
||||
assigned += slot.whole
|
||||
slots.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a.rest > b.rest if a.rest != b.rest else str(a.owner) < str(b.owner))
|
||||
var left: float = roundf(physical[id]) - assigned
|
||||
for slot: Dictionary in slots:
|
||||
if left > 0.0:
|
||||
slot.whole += 1.0
|
||||
left -= 1.0
|
||||
if slot.owner == "":
|
||||
target.amount[id] = slot.whole
|
||||
else:
|
||||
target.binds[slot.owner][id] = slot.whole
|
||||
|
||||
|
||||
func refresh_groups(target: Dictionary) -> void:
|
||||
var groups: Dictionary = {}
|
||||
for id in world.catalog.entities:
|
||||
var group: String = world.catalog.entities[id].group_as
|
||||
if group != "":
|
||||
groups[group] = groups.get(group, 0.0) + target.amount.get(id, 0.0)
|
||||
for group in groups:
|
||||
target.amount[group] = groups[group]
|
||||
|
||||
|
||||
func clamp_auto_resources(target: Dictionary) -> void:
|
||||
for id in target.get("auto_resources", {}):
|
||||
target.auto_resources[id] = minf(target.auto_resources[id], available(target, id))
|
||||
|
||||
|
||||
func bind_full(target: Dictionary, amounts: Dictionary) -> bool:
|
||||
# Binding only mutates free amounts and owners' holdings.
|
||||
var trial: Dictionary = {
|
||||
"amount": target.amount.duplicate(),
|
||||
"binds": target.binds.duplicate(true),
|
||||
}
|
||||
var requests: Array = []
|
||||
for owner_id in amounts:
|
||||
if not world.catalog.entities.has(owner_id):
|
||||
return false
|
||||
for id in world.catalog.entities[owner_id].bind:
|
||||
var quantity: float = amounts[owner_id] * maxf(0.0, world.catalog.entities[owner_id].bind[id])
|
||||
if not is_equal_approx(quantity, roundf(quantity)):
|
||||
return false
|
||||
if quantity > 0.0:
|
||||
requests.append({"owner": owner_id, "id": id, "amount": roundf(quantity), "members": world.catalog.members(id).size()})
|
||||
requests.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
if a.members != b.members:
|
||||
return a.members < b.members
|
||||
return str(a.id) < str(b.id) if a.id != b.id else str(a.owner) < str(b.owner))
|
||||
for request: Dictionary in requests:
|
||||
var taken: Dictionary = take_available(trial, request.id, request.amount)
|
||||
var total: float = 0.0
|
||||
for quantity in taken.values():
|
||||
total += quantity
|
||||
if total != request.amount:
|
||||
return false
|
||||
if not trial.binds.has(request.owner):
|
||||
trial.binds[request.owner] = {}
|
||||
for id in taken:
|
||||
trial.binds[request.owner][id] = trial.binds[request.owner].get(id, 0.0) + taken[id]
|
||||
target.amount = trial.amount
|
||||
target.binds = trial.binds
|
||||
return true
|
||||
|
||||
|
||||
func bind_new_owners(target: Dictionary, amounts: Dictionary, require_full: bool = false) -> bool:
|
||||
if require_full:
|
||||
return bind_full(target, amounts)
|
||||
var requests: Dictionary = {}
|
||||
for owner_id in amounts:
|
||||
if not world.catalog.entities.has(owner_id):
|
||||
continue
|
||||
for id in world.catalog.entities[owner_id].bind:
|
||||
var quantity: float = amounts[owner_id] * maxf(0.0, world.catalog.entities[owner_id].bind[id])
|
||||
var weights: Dictionary = {}
|
||||
for member in world.catalog.members(id):
|
||||
weights[member] = target.amount.get(member, 0.0)
|
||||
weights = QuantityMath.normalize_distribution(weights)
|
||||
for member in weights:
|
||||
if not requests.has(member):
|
||||
requests[member] = {}
|
||||
requests[member][owner_id] = requests[member].get(owner_id, 0.0) + quantity * weights[member]
|
||||
for id in requests:
|
||||
var requested: float = 0.0
|
||||
for quantity in requests[id].values():
|
||||
requested += quantity
|
||||
var available: float = available(target, id)
|
||||
var factor: float = minf(1.0, available / requested) if requested > 0.0 else 0.0
|
||||
for owner_id in requests[id]:
|
||||
var quantity: float = requests[id][owner_id] * factor
|
||||
if not target.binds.has(owner_id):
|
||||
target.binds[owner_id] = {}
|
||||
target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + quantity
|
||||
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - quantity)
|
||||
|
||||
return true
|
||||
|
||||
|
||||
func apply_costs(entity: OwniverseEntity, amount: float) -> void:
|
||||
var costs: Dictionary = {}
|
||||
for resource in entity.cost:
|
||||
costs[resource] = roundf(maxf(0.0, entity.cost[resource]) * amount)
|
||||
for resource in costs:
|
||||
var quantities: Dictionary = physical_amounts(world.state)
|
||||
var taken: Dictionary = take_available(world.state, resource, costs[resource])
|
||||
for id in taken:
|
||||
var factor: float = 1.0 - taken[id] / quantities[id] if quantities.get(id, 0.0) > 0.0 else 0.0
|
||||
world.batches.scale_batches(world.state, id, factor)
|
||||
refresh_groups(world.state)
|
||||
clamp_auto_resources(world.state)
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
|
||||
|
||||
func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> bool:
|
||||
if not bind_new_owners(world.state, {item_id: amount}, true):
|
||||
return false
|
||||
refresh_groups(world.state)
|
||||
clamp_auto_resources(world.state)
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
|
||||
return true
|
||||
|
||||
|
||||
func process_entity(entity_id: String, amount: float) -> void:
|
||||
if amount == 0:
|
||||
return
|
||||
|
||||
# Update state amount
|
||||
world.state.amount[entity_id] = world.state.amount.get(entity_id, 0.0) + amount
|
||||
world.state.stats.total_amount[entity_id] = world.state.stats.total_amount.get(entity_id, 0.0) + maxf(0.0, amount)
|
||||
|
||||
|
||||
# Handle group updates if entity belongs to a group
|
||||
var entity = world.catalog.entities[entity_id]
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = world.state.amount.get(entity.group_as, 0.0) + amount
|
||||
world.state.stats.total_amount[entity.group_as] = world.state.stats.total_amount.get(entity.group_as, 0.0) + maxf(0.0, amount)
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
|
||||
|
||||
## Freeze holdings before simultaneous transitions and reserve successors.
|
||||
func transfer_evolution(target: Dictionary, fractions: Dictionary, outputs: Dictionary) -> Dictionary:
|
||||
# Use a frozen binding table so simultaneous transitions cannot cascade by type order.
|
||||
var bindings: Dictionary = target.binds.duplicate(true)
|
||||
var incoming: Dictionary = {}
|
||||
for owner_id in bindings:
|
||||
var owner_survival: float = 1.0 - fractions.get(owner_id, 0.0)
|
||||
target.binds[owner_id] = {}
|
||||
for id in bindings[owner_id]:
|
||||
var held: float = bindings[owner_id][id]
|
||||
var released: float = held * (1.0 - owner_survival)
|
||||
target.amount[id] = target.amount.get(id, 0.0) + released
|
||||
var heirs: Dictionary = world.stellar.stellar_successors(outputs.get(owner_id, {})) if world.catalog.entities.has(id) and world.catalog.entities[id].group_as == "Planet" else {}
|
||||
var fraction: float = fractions.get(id, 0.0)
|
||||
for heir in heirs:
|
||||
if not incoming.has(heir):
|
||||
incoming[heir] = {}
|
||||
incoming[heir][id] = incoming[heir].get(id, 0.0) + released * (1.0 - fraction) * heirs[heir]
|
||||
for child in outputs.get(id, {}):
|
||||
if world.catalog.entities.has(child) and not CivilizationState.is_space_civilization(child, Global.CivilizationRules):
|
||||
incoming[heir][child] = incoming[heir].get(child, 0.0) + released * fraction * heirs[heir] * outputs[id][child]
|
||||
var remaining: float = held * owner_survival * (1.0 - fraction)
|
||||
target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + remaining
|
||||
for successor in outputs.get(id, {}):
|
||||
if not world.catalog.entities.has(successor) or CivilizationState.is_space_civilization(successor, Global.CivilizationRules):
|
||||
continue
|
||||
var transferred: float = held * owner_survival * fraction * maxf(0.0, outputs[id][successor])
|
||||
if not incoming.has(owner_id):
|
||||
incoming[owner_id] = {}
|
||||
incoming[owner_id][successor] = incoming[owner_id].get(successor, 0.0) + transferred
|
||||
|
||||
for id in fractions:
|
||||
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) * (1.0 - fractions[id]))
|
||||
|
||||
return incoming
|
||||
|
||||
|
||||
## Commit reservations after newly evolved objects have been credited.
|
||||
func accept_transfers(target: Dictionary, incoming: Dictionary) -> void:
|
||||
for owner_id in incoming:
|
||||
if not target.binds.has(owner_id):
|
||||
target.binds[owner_id] = {}
|
||||
for id in incoming[owner_id]:
|
||||
var transferred: float = minf(incoming[owner_id][id], target.amount.get(id, 0.0))
|
||||
target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + transferred
|
||||
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - transferred)
|
||||
@@ -0,0 +1 @@
|
||||
uid://buauxqyl73vjb
|
||||
+12
-1412
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
extends RefCounted
|
||||
## Modules borrow their world; a weak reference prevents ownership cycles.
|
||||
var _world_ref: WeakRef
|
||||
var world:
|
||||
get: return _world_ref.get_ref()
|
||||
|
||||
func _init(owner_world) -> void:
|
||||
_world_ref = weakref(owner_world)
|
||||
@@ -0,0 +1 @@
|
||||
uid://orcojklh0nne
|
||||
@@ -0,0 +1,118 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
## Testable optimization switch; both paths preserve the same event order.
|
||||
var direct_completions: bool = true
|
||||
|
||||
func add_output_terms(outputs: Dictionary, terms: String, count: float) -> void:
|
||||
if count <= 0.0:
|
||||
return
|
||||
for term in terms.split("&"):
|
||||
var parts: PackedStringArray = term.strip_edges().split(":")
|
||||
var id: String = parts[0].strip_edges()
|
||||
var amount: float = float(parts[1]) if parts.size() == 2 else 1.0
|
||||
if id != "" and amount > 0.0:
|
||||
outputs[id] = outputs.get(id, 0.0) + count * amount
|
||||
|
||||
|
||||
func evolution_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary:
|
||||
if not entity.life_rule.is_empty():
|
||||
return world.life.life_products(target, entity, count)
|
||||
var outputs: Dictionary = {}
|
||||
for id in entity.output_and:
|
||||
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 * 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)
|
||||
var positive: float = minf(count, floorf(snappedf(expected, 1.0e-12)))
|
||||
target.evolution_remainders[entity.id] = maxf(0.0, expected - positive)
|
||||
add_output_terms(outputs, str(entity.output_or.get("positive", "")), positive)
|
||||
add_output_terms(outputs, str(entity.output_or.get("negative", "")), count - positive)
|
||||
# Entity.blast is authoritative even if imported output still contains
|
||||
# blast:N. Zero disables the event; never add both representations.
|
||||
outputs.erase("blast")
|
||||
if entity.blast > 0:
|
||||
outputs["blast"] = count * entity.blast * WorldParameters.effective(target, "Exp")
|
||||
return outputs
|
||||
|
||||
|
||||
func create_planets(target: Dictionary, star_id: String, count: float,
|
||||
from_year: float, to_year: float, metallicity: float) -> void:
|
||||
if count <= 0.0 or world.catalog.entities[star_id].planets.is_empty():
|
||||
return
|
||||
if not target.binds.has(star_id):
|
||||
target.binds[star_id] = {}
|
||||
for id in world.catalog.entities[star_id].planets:
|
||||
if not world.catalog.entities.has(id):
|
||||
continue
|
||||
var rule: Dictionary = world.catalog.entities[star_id].planets[id]
|
||||
var amount: float = QuantityMath.accumulate_whole(target, "planet_remainders", star_id + ":" + id,
|
||||
count * rule.amount * rule.chance * maxf(0.0, metallicity))
|
||||
amount = world.ledger.credit(target, id, amount)
|
||||
target.amount[id] -= amount
|
||||
target.binds[star_id][id] = target.binds[star_id].get(id, 0.0) + amount
|
||||
world.batches.append_batch(target, id, amount, from_year, to_year)
|
||||
|
||||
|
||||
func stellar_successors(products: Dictionary) -> Dictionary:
|
||||
var weights: Dictionary = {}
|
||||
for id in products:
|
||||
if world.catalog.entities.has(id) and (world.catalog.entities[id].group_as in ["Star", "BH"] or world.catalog.entities[id].event_group == "SNGRBB" or world.catalog.entities[id].count_as == "BD"):
|
||||
weights[id] = products[id]
|
||||
return QuantityMath.normalize_distribution(weights)
|
||||
|
||||
|
||||
func destroy_planets(target: Dictionary, owner_id: String, ratio: float, level: int, cause: String = "local") -> void:
|
||||
if ratio <= 0.0 or level not in [1, 2]:
|
||||
return
|
||||
var held: Dictionary = target.binds.get(owner_id, {})
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
var rocky: float = 0.0
|
||||
for id in held.keys():
|
||||
if not world.catalog.entities.has(id) or world.catalog.entities[id].group_as != "Planet":
|
||||
continue
|
||||
if level == 1 and not world.catalog.entities[id].blastability:
|
||||
continue
|
||||
var key: String = cause + ":" + str(level) + ":" + owner_id + ":" + id
|
||||
var destroyed: float = minf(held[id], QuantityMath.accumulate_whole(target, "destruction_remainders", key, held[id] * minf(1.0, ratio)))
|
||||
if destroyed == held[id]:
|
||||
target.destruction_remainders[key] = 0.0
|
||||
if destroyed <= 0.0:
|
||||
continue
|
||||
held[id] -= destroyed
|
||||
world.batches.scale_batches(target, id, 1.0 - destroyed / physical[id])
|
||||
if level == 1:
|
||||
rocky += destroyed
|
||||
if rocky > 0.0:
|
||||
rocky = world.ledger.credit(target, "RP", rocky)
|
||||
target.amount.RP -= rocky
|
||||
held.RP = held.get("RP", 0.0) + rocky
|
||||
|
||||
|
||||
func apply_blast(target: Dictionary, power: float, environment: Dictionary) -> void:
|
||||
var hits: float = QuantityMath.accumulate_whole(target, "blast_remainders", "hits", power * environment.blast)
|
||||
var quantities: Dictionary = world.ledger.physical_amounts(target)
|
||||
var weights: Dictionary = {}
|
||||
for id in world.catalog.entities:
|
||||
if not world.catalog.entities[id].planets.is_empty() and quantities.get(id, 0.0) > 0.0:
|
||||
weights[id] = quantities[id]
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(weights)
|
||||
# Fractional exposure is deterministic; destruction itself remains whole.
|
||||
for id in distribution:
|
||||
destroy_planets(target, id, hits * distribution[id] / quantities[id] * environment.density, 1, "blast")
|
||||
|
||||
|
||||
func uses_short_lived_fast_path(id: String) -> bool:
|
||||
if not direct_completions: return false
|
||||
# Only terminal stellar events with linear outputs and no living successors.
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
if entity.event_group != "SNGRBB" or not entity.output_or.is_empty() or not entity.life_rule.is_empty():
|
||||
return false
|
||||
for successor in entity.output_and:
|
||||
if world.catalog.entities.has(successor) and world.catalog.entities[successor].lifespan > 0.0:
|
||||
return false
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://cim2wuoas2ojv
|
||||
@@ -0,0 +1,42 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
|
||||
func stellar_environment(source: Dictionary) -> Dictionary:
|
||||
var quantities: Dictionary = world.ledger.physical_amounts(source)
|
||||
# SMBH is a self-group in GI; its quantity stores mass, not object count.
|
||||
var mass: float = source.amount.get("SMBH", 0.0)
|
||||
var volume: float = quantities.get("S", 0.0)
|
||||
for id in quantities:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
if id in ["H", "He", "Met", "SMBH"]:
|
||||
mass += quantities[id]
|
||||
else:
|
||||
mass += quantities[id] * maxf(0.0, entity.mass)
|
||||
volume += quantities[id] * maxf(0.0, entity.space)
|
||||
volume += quantities[id] * maxf(0.0, entity.voids) * 1.0e22
|
||||
var metallicity: float = 0.0
|
||||
if mass > 0.0:
|
||||
metallicity = maxf(0.0, 0.4 * log(100.0 * source.amount.get("Met", 0.0) / mass + 0.11) + 0.88)
|
||||
var base_mass: float = 65536.0 * (pow(mass, 0.1) if mass > 0.0 and mass < 1.0e10 else (10.0 if mass >= 1.0e10 else 1.0))
|
||||
return {
|
||||
"metallicity": metallicity,
|
||||
"density": 1.23456e-7 * sqrt(base_mass + mass) / pow(maxf(1.0, volume), 1.0 / 6.0),
|
||||
"blast": clampf(source.amount.get("S", 0.0) / volume, 0.0, 1.0) if volume > 0.0 else 1.0,
|
||||
}
|
||||
|
||||
|
||||
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(world.state, key)
|
||||
if not world.state.has("world_parameters"): world.state.world_parameters = WorldParameters.defaults()
|
||||
world.state.world_parameters[key] = clampf(WorldParameters.value(world.state, key) + change, 1.0, 19.0)
|
||||
if key == "Bur":
|
||||
var ratio: float = old_multiplier / WorldParameters.effective(world.state, key)
|
||||
var now: float = world.state.time.year
|
||||
for id in world.state.batches:
|
||||
if not world.catalog.entities.has(id) or world.catalog.entities[id].group_as != "Star": continue
|
||||
for batch: Dictionary in world.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
|
||||
@@ -0,0 +1 @@
|
||||
uid://cimalpe85ofel
|
||||
Reference in New Issue
Block a user