Add exact arithmetic implementation and tests
- Introduced exact arithmetic with support for large integers and fractions in `exact.gd`, `big_integer.gd`, and `exact_value.gd`. - Implemented a comprehensive test suite in `test_exact_math.gd` to validate arithmetic operations against expected results. - Added a README in `math` directory to document the exact arithmetic system and its usage. - Ensured that all arithmetic operations maintain precision without floating-point errors, using a custom representation for large numbers. - Included methods for encoding and decoding exact values for serialization.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
- `quantity_math.gd`: нормализация положительных весов, ограниченное целочисленное распределение методом наибольших остатков, распределение с накоплением ошибки округления, накопление дробного выпуска и минимальный целый размер производственной партии.
|
||||
- `entity_catalog.gd`: загрузка определений, индексы групп и событий, списки автоматических производителей и разблокируемых сущностей.
|
||||
|
||||
Математика не обращается к дереву сцены, Global или Owniverse. Методы накопления изменяют переданный словарь остатков; остальные методы возвращают результат. Для распределения `split` количество должно быть целым, веса — неотрицательными. Равные остатки разрешаются по строковому ID. Сохранены прежние точности округления: 1e-12 для выпуска и 1e-10 для событий цивилизаций.
|
||||
Математика не обращается к дереву сцены, Global или Owniverse. Методы накопления изменяют переданный словарь остатков; остальные методы возвращают результат. Для распределения `split` количество должно быть целым, веса — неотрицательными. Равные остатки разрешаются по строковому ID. Старые поправки `snappedf` удалены: количества считаются целочисленно, дробные вычисления выполняет [модуль точной арифметики](math/README.md). Там же описаны границы точности и формат сохранения 3.
|
||||
|
||||
Каталог получает словарь исходных строк через `load_definitions(rows)`, вызываемый один раз для нового экземпляра. Конструктор OwniverseEntity по-прежнему использует настройки Global. Каталог не хранит количества, партии или состояние интерфейса. Определения пока остаются Node: освобождает их владелец мира, как до рефакторинга.
|
||||
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.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:
|
||||
if ((not entity.life_product.is_empty()) and E.gt(entity.lifespan, 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)
|
||||
products[resource] = E.divide(E.maximum(0, entity.life_product[resource]), entity.lifespan)
|
||||
elif E.ne(entity.product_per_year_type, ""):
|
||||
products[entity.product_per_year_type] = E.maximum(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:
|
||||
func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Dictionary, distribution: Dictionary, from_year: Variant, to_year: Variant) -> 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 object_years: Variant = world.batches.object_years(previous, id, quantities[id], from_year, to_year)
|
||||
var auto_share: Variant = E.clamp_value(entity.auto, 0, 1)
|
||||
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
|
||||
var amount: Variant = E.multiply(E.multiply(object_years, products[product]), WorldParameters.production(previous, entity))
|
||||
basic[product] = E.add(basic.get(product, 0), E.multiply(amount, (E.subtract(1, auto_share))))
|
||||
automatic[product] = E.add(automatic.get(product, 0), E.multiply(amount, auto_share))
|
||||
# Basic and automatic output each retain their unproduced remainder.
|
||||
if not target.has("resource_remainders"):
|
||||
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]
|
||||
pending.basic[id] = E.add(pending.basic.get(id, 0), basic[id])
|
||||
pending.automatic[id] = E.add(pending.automatic.get(id, 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))
|
||||
totals[id] = E.maximum(0, E.add(pending.basic.get(id, 0), pending.automatic.get(id, 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)))
|
||||
if (E.eq(id, "S") or E.eq(id, "H")):
|
||||
requests[id] = E.floor_value(totals[id])
|
||||
var funded: Dictionary = QuantityMath.allocate_whole(requests, E.floor_value(E.maximum(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
|
||||
var amount: Variant = E.floor_value(totals[id])
|
||||
if (E.eq(id, "S") or E.eq(id, "H")):
|
||||
amount = funded.get(id, 0)
|
||||
target.energy = E.subtract(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)
|
||||
var auto_ratio: Variant = (E.clamp_value(E.divide(pending.automatic.get(id, 0), totals[id]), 0, 1) if E.gt(totals[id], 0) else 0)
|
||||
var auto_amount: Variant = E.floor_value(E.multiply(amount, auto_ratio))
|
||||
pending.automatic[id] = E.subtract(pending.automatic.get(id, 0), auto_amount)
|
||||
pending.basic[id] = E.subtract(pending.basic.get(id, 0), (E.subtract(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)
|
||||
@@ -60,22 +60,22 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
|
||||
# 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]
|
||||
budget[id] = E.add(budget.get(id, 0), automatic[id])
|
||||
for id in budget:
|
||||
budget[id] = minf(maxf(0.0, budget[id]), world.ledger.available(target, id))
|
||||
budget[id] = E.minimum(E.maximum(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)
|
||||
produce_brown_dwarfs(target, automatic.get("H", 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":
|
||||
if E.ne(entity.group_as, "Star"):
|
||||
continue
|
||||
var has_cost: bool = false
|
||||
for resource in entity.cost:
|
||||
if entity.cost[resource] > 0.0:
|
||||
if E.gt(entity.cost[resource], 0):
|
||||
has_cost = true
|
||||
if has_cost:
|
||||
candidates[id] = distribution[id]
|
||||
@@ -85,88 +85,87 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
|
||||
var costs: Dictionary = {}
|
||||
var needs: Dictionary = {}
|
||||
for id in candidates:
|
||||
if auto_capacity(target, world.catalog.entities[id], budget) <= 0.0:
|
||||
if E.le(auto_capacity(target, world.catalog.entities[id], budget), 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
|
||||
var cost: Variant = E.multiply(E.maximum(0, world.catalog.entities[id].cost[resource]), candidates[id])
|
||||
costs[resource] = E.add(costs.get(resource, 0), cost)
|
||||
needs[resource] = E.add(needs.get(resource, 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
|
||||
needs[resource] = E.add(needs.get(resource, 0), E.multiply(E.maximum(0, world.catalog.entities[id].bind[resource]), candidates[id]))
|
||||
var scale: Variant = INF
|
||||
for resource in costs:
|
||||
if costs[resource] > 0.0:
|
||||
scale = minf(scale, budget.get(resource, 0.0) / costs[resource])
|
||||
if E.gt(costs[resource], 0):
|
||||
scale = E.minimum(scale, E.divide(budget.get(resource, 0), costs[resource]))
|
||||
for resource in needs:
|
||||
if needs[resource] > 0.0:
|
||||
scale = minf(scale, world.ledger.available(target, resource) / needs[resource])
|
||||
if E.gt(needs[resource], 0):
|
||||
scale = E.minimum(scale, E.divide(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:
|
||||
if E.gt(concrete_needs[resource], 0):
|
||||
scale = E.minimum(scale, E.divide(world.ledger.available(target, resource), concrete_needs[resource]))
|
||||
if ((not E.finite(scale)) or E.le(scale, 0)):
|
||||
return
|
||||
# Rare types are served first; signed count deficits compensate later cycles.
|
||||
if not target.has("auto_star_deficits"):
|
||||
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)
|
||||
order.sort_custom(func ( a : String , b : String ) -> bool : return (E.lt(candidates[a], candidates[b]) if E.ne(candidates[a], candidates[b]) else E.lt(a, b)))
|
||||
var created: Dictionary = {}
|
||||
var metallicity: float = world.environment.stellar_environment(target).metallicity
|
||||
var total_created: float = 0.0
|
||||
var metallicity: Variant = world.environment.stellar_environment(target).metallicity
|
||||
var total_created: Variant = 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):
|
||||
var step: Variant = world.production.production_step(entity)
|
||||
if (not E.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:
|
||||
var quota: Variant = E.maximum(0, E.add(E.multiply(candidates[id], scale), deficits.get(id, 0)))
|
||||
var desired: Variant = E.multiply(E.floor_value(E.divide(quota, step)), step)
|
||||
var actual: Variant = E.minimum(desired, auto_capacity(target, entity, budget))
|
||||
if E.le(actual, 0):
|
||||
continue
|
||||
if not world.production.try_production(target, entity, actual, true):
|
||||
if (not world.production.try_production(target, entity, actual, true)):
|
||||
continue
|
||||
budget = ordinary_star_budget(target)
|
||||
created[id] = actual
|
||||
total_created += actual
|
||||
total_created = E.add(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)
|
||||
deficits[id] = E.subtract(E.add(deficits.get(id, 0), E.multiply(candidates[id], total_created)), created.get(id, 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"):
|
||||
func produce_brown_dwarfs(target: Dictionary, new_hydrogen: Variant, from_year: Variant, to_year: Variant) -> 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:
|
||||
var cost: Variant = entity.cost.get("H", 0)
|
||||
if E.le(cost, 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)
|
||||
var quota: Variant = E.add(target.auto_star_deficits.get("HBD", 0), E.divide(E.multiply(E.maximum(0, new_hydrogen), E.of("0.001")), cost))
|
||||
var fuel: Variant = E.maximum(0, target.auto_star_deficits.get("HBD_fuel", 0))
|
||||
var available: Variant = E.minimum(target.auto_resources.get("H", 0), world.ledger.available(target, "H"))
|
||||
quota = E.minimum(quota, E.divide((E.add(available, fuel)), cost))
|
||||
var expected: Variant = E.maximum(0, quota)
|
||||
var amount: Variant = E.floor_value(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)
|
||||
if (E.gt(new_hydrogen, 0) and E.gt(expected, amount)):
|
||||
var random_state: Variant = target.auto_star_deficits.get("HBD_random", 1234567)
|
||||
random_state = E.remainder(E.multiply(random_state, 48271), E.of("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:
|
||||
if E.lt(E.divide(random_state, E.of("2147483647.0")), E.subtract(expected, amount)):
|
||||
amount = E.add(amount, 1)
|
||||
amount = E.minimum(amount, E.floor_value(E.divide((E.add(available, fuel)), cost)))
|
||||
var charge: Variant = E.ceil_value(E.maximum(0, E.subtract(E.multiply(amount, cost), fuel)))
|
||||
if (E.gt(amount, 0) and E.le(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
|
||||
target.auto_resources.H = E.maximum(0, E.subtract(target.auto_resources.get("H", 0), charge))
|
||||
target.auto_star_deficits.HBD_fuel = E.maximum(0, E.subtract(E.add(fuel, charge), E.multiply(amount, cost)))
|
||||
quota = E.subtract(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
|
||||
@@ -175,52 +174,50 @@ func produce_brown_dwarfs(target: Dictionary, new_hydrogen: float, from_year: fl
|
||||
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)
|
||||
var reserved: Variant = E.maximum(0, E.subtract(E.multiply(target.auto_star_deficits.get("HBD", 0), world.catalog.entities.HBD.cost.get("H", 0)), target.auto_star_deficits.get("HBD_fuel", 0)))
|
||||
budget.H = E.maximum(0, E.subtract(budget.get("H", 0), reserved))
|
||||
return budget
|
||||
|
||||
|
||||
func auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Dictionary) -> float:
|
||||
var capacity: float = INF
|
||||
func auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Dictionary) -> Variant:
|
||||
var capacity: Variant = INF
|
||||
var needs: Dictionary = {}
|
||||
for resource in entity.cost:
|
||||
var cost: float = maxf(0.0, entity.cost[resource])
|
||||
var cost: Variant = E.maximum(0, entity.cost[resource])
|
||||
needs[resource] = cost
|
||||
if cost > 0.0:
|
||||
capacity = minf(capacity, floorf(budget.get(resource, 0.0) / cost))
|
||||
if E.gt(cost, 0):
|
||||
capacity = E.minimum(capacity, E.floor_value(E.divide(budget.get(resource, 0), cost)))
|
||||
for resource in entity.bind:
|
||||
needs[resource] = needs.get(resource, 0.0) + maxf(0.0, entity.bind[resource])
|
||||
needs[resource] = E.add(needs.get(resource, 0), E.maximum(0, entity.bind[resource]))
|
||||
for resource in needs:
|
||||
if needs[resource] > 0.0:
|
||||
capacity = minf(capacity, floorf(world.ledger.available(source, resource) / needs[resource]))
|
||||
if E.gt(needs[resource], 0):
|
||||
capacity = E.minimum(capacity, E.floor_value(E.divide(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
|
||||
if E.gt(concrete[resource], 0):
|
||||
capacity = E.minimum(capacity, E.floor_value(E.divide(world.ledger.available(source, resource), concrete[resource])))
|
||||
var step: Variant = world.production.production_step(entity)
|
||||
return (E.multiply(E.floor_value(E.divide(capacity, step)), step) if (E.finite(capacity) and E.finite(step)) else 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:
|
||||
func produce_newborn_lifetime(target: Dictionary, id: String, count: Variant, born_from: Variant, born_to: Variant, to_year: Variant, before_batches: Array, remaining_object_years: Variant = NAN) -> void:
|
||||
var products: Dictionary = yearly_products(world.catalog.entities[id])
|
||||
if products.is_empty() or count <= 0.0:
|
||||
if (products.is_empty() or E.le(count, 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 object_years: Variant = (remaining_object_years if E.finite(remaining_object_years) else E.multiply(count, E.maximum(0, E.subtract(to_year, E.multiply((E.add(born_from, born_to)), E.of("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 old : Dictionary in before_batches:
|
||||
var start: Variant = old.get("created_from", E.neg(INF))
|
||||
if (not previous_amounts.has(str(start))):
|
||||
previous_amounts[str(start)] = old.amount
|
||||
for batch : Dictionary in target.batches.get(id, []):
|
||||
var added: Variant = E.subtract(batch.amount, previous_amounts.get(str(batch.get("created_from", INF)), 0))
|
||||
object_years = E.subtract(object_years, E.multiply(E.maximum(0, added), world.batches.expired_years(to_year, batch.start_year, batch.end_year)))
|
||||
object_years = E.maximum(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)
|
||||
var output: Variant = E.multiply(E.multiply(object_years, products[resource]), WorldParameters.production(target, world.catalog.entities[id]))
|
||||
if E.contains(["H", "S"], resource):
|
||||
output = E.minimum(output, target.energy)
|
||||
var actual: Variant = world.ledger.credit(target, resource, output)
|
||||
if E.contains(["H", "S"], resource):
|
||||
target.energy = E.maximum(0, E.subtract(target.energy, actual))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
@@ -7,43 +8,42 @@ 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"}
|
||||
|
||||
static func total(pop: Dictionary) -> float:
|
||||
static func total(pop: Dictionary) -> Variant:
|
||||
return QuantityMath.sum(pop)
|
||||
|
||||
static func add(to: Dictionary, from: Dictionary) -> void:
|
||||
for key in from: to[key] = to.get(key, 0.0) + from[key]
|
||||
for key in from: to[key] = E.add(to.get(key, 0), from[key])
|
||||
|
||||
static func take(world, pop: Dictionary, count: float) -> Dictionary:
|
||||
static func take(world, pop: Dictionary, count: Variant) -> Dictionary:
|
||||
var taken: Dictionary = QuantityMath.allocate_whole(pop, count)
|
||||
for key in taken: pop[key] -= taken[key]
|
||||
for key in taken: pop[key] = E.subtract(pop[key], taken[key])
|
||||
return taken
|
||||
|
||||
static func rate(target: Dictionary, key: String, count: float, probability: float) -> float:
|
||||
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
||||
static func rate(target: Dictionary, key: String, count: Variant, probability: Variant) -> Variant:
|
||||
if (not target.has("civilization_remainders")): target.civilization_remainders = {}
|
||||
return QuantityMath.accumulate_rate(target.civilization_remainders, key, count, probability)
|
||||
|
||||
# Fixed-size largest-remainder apportionment with saved signed rounding errors.
|
||||
# Runtime depends on the number of branches, never the number of civilizations.
|
||||
static func split(target: Dictionary, key: String, count: float, weights: Dictionary) -> Dictionary:
|
||||
if count <= 0.0 or QuantityMath.sum(weights) <= 0.0: return {}
|
||||
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
||||
static func split(target: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
|
||||
if (E.le(count, 0) or E.le(QuantityMath.sum(weights), 0)): return {}
|
||||
if (not target.has("civilization_remainders")): target.civilization_remainders = {}
|
||||
return QuantityMath.split(target.civilization_remainders, key, count, weights)
|
||||
|
||||
static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictionary,
|
||||
diplomacy: bool = false, colony: bool = false) -> Dictionary:
|
||||
static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictionary, diplomacy: bool = false, colony: bool = false) -> Dictionary:
|
||||
var weights: Dictionary = {}
|
||||
for ethic in pop:
|
||||
var influence: float = float(rows[ethic].diplomacy_rate) if diplomacy else 1.0
|
||||
var influence: Variant = (E.of(rows[ethic].diplomacy_rate) if diplomacy else 1)
|
||||
for other in ETHICS:
|
||||
var weight: float = 9.0 if colony else 1.0
|
||||
if other == ethic: weight = 960.0 if colony else 196.0
|
||||
elif other == OPPOSITE[ethic]: weight = 4.0 if colony else 0.0
|
||||
weights[other] = weights.get(other, 0.0) + pop[ethic] * influence * weight
|
||||
if total(weights) <= 0.0: return pop.duplicate()
|
||||
var weight: Variant = (9 if colony else 1)
|
||||
if E.eq(other, ethic): weight = (960 if colony else 196)
|
||||
elif E.eq(other, OPPOSITE[ethic]): weight = (4 if colony else 0)
|
||||
weights[other] = E.add(weights.get(other, 0), E.multiply(E.multiply(pop[ethic], influence), weight))
|
||||
if E.le(total(weights), 0): return pop.duplicate()
|
||||
return split(target, key, total(pop), weights)
|
||||
|
||||
static func emit(plan: Dictionary, destination: String, pop: Dictionary) -> void:
|
||||
if not plan.outputs.has(destination): plan.outputs[destination] = {}
|
||||
if (not plan.outputs.has(destination)): plan.outputs[destination] = {}
|
||||
add(plan.outputs[destination], pop)
|
||||
|
||||
static func fall(target: Dictionary, plan: Dictionary, rules: Dictionary, pop: Dictionary) -> void:
|
||||
@@ -51,51 +51,50 @@ static func fall(target: Dictionary, plan: Dictionary, rules: Dictionary, pop: D
|
||||
var weights: Dictionary = {}
|
||||
for term in str(rules[plan.id].ethics[ethic].fall).split("^", false):
|
||||
var pair: PackedStringArray = term.split(":")
|
||||
weights[pair[0]] = float(pair[1])
|
||||
if weights.is_empty(): weights[rules[plan.id].previous] = 1.0
|
||||
var shares: Dictionary = split(target, "fall:" + plan.id + ":" + ethic, pop[ethic], weights)
|
||||
weights[pair[0]] = E.of(pair[1])
|
||||
if weights.is_empty(): weights[rules[plan.id].previous] = 1
|
||||
var shares: Dictionary = split(target, E.add(E.add(E.add("fall:", plan.id), ":"), ethic), pop[ethic], weights)
|
||||
for stage in shares: emit(plan, stage, {ethic: shares[stage]})
|
||||
|
||||
static func progress(world, target: Dictionary, rules: Dictionary, budget: Dictionary,
|
||||
plan: Dictionary, pop: Dictionary) -> Dictionary:
|
||||
static func progress(world, target: Dictionary, rules: Dictionary, budget: Dictionary, plan: Dictionary, pop: Dictionary) -> Dictionary:
|
||||
var successor: String = rules[plan.id].next
|
||||
var admitted: float = State.admit_transition(budget, rules, plan.id, successor, total(pop))
|
||||
var admitted: Variant = State.admit_transition(budget, rules, plan.id, successor, total(pop))
|
||||
var accepted: Dictionary = take(world, pop, admitted)
|
||||
emit(plan, successor, accepted)
|
||||
return pop
|
||||
|
||||
static func new_plan(id: String, year: float) -> Dictionary:
|
||||
return {"id": id, "from": year, "to": -INF, "amount": 0.0, "outputs": {}}
|
||||
static func new_plan(id: String, year: Variant) -> Dictionary:
|
||||
return {"id": id, "from": year, "to": E.neg(INF), "amount": 0, "outputs": {}}
|
||||
|
||||
static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop: Dictionary, count: float, year: float) -> Dictionary:
|
||||
var old_total: float = total(pop)
|
||||
static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop: Dictionary, count: Variant, year: Variant) -> Dictionary:
|
||||
var old_total: Variant = total(pop)
|
||||
var removed: Dictionary = take(world, pop, count)
|
||||
var actual: float = total(removed)
|
||||
if actual > 0.0:
|
||||
world.batches.scale_batches(target, plan.id, (old_total - actual) / old_total)
|
||||
plan.amount += actual
|
||||
plan.to = maxf(plan.to, year)
|
||||
var actual: Variant = total(removed)
|
||||
if E.gt(actual, 0):
|
||||
world.batches.scale_batches(target, plan.id, E.divide((E.subtract(old_total, actual)), old_total))
|
||||
plan.amount = E.add(plan.amount, actual)
|
||||
plan.to = E.maximum(plan.to, year)
|
||||
return removed
|
||||
|
||||
static func prepare(world, target: Dictionary, expired: Dictionary, year: float, budget: Dictionary, rules: Dictionary) -> Dictionary:
|
||||
static func prepare(world, target: Dictionary, expired: Dictionary, year: Variant, budget: Dictionary, rules: Dictionary) -> Dictionary:
|
||||
State.reconcile(world, target, rules)
|
||||
var remaining: Dictionary = target.ethics.duplicate(true)
|
||||
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 order: Variant = WorldParameters.value(target, "Order")
|
||||
if (not target.has("civilization_stats")): target.civilization_stats = {}
|
||||
var ids: Array = expired.keys()
|
||||
ids.sort()
|
||||
for id in ids:
|
||||
if not rules.has(id) or expired[id].is_empty(): continue
|
||||
if ((not rules.has(id)) or expired[id].is_empty()): continue
|
||||
var plan: Dictionary = new_plan(id, year)
|
||||
plans[id] = plan
|
||||
for part: Dictionary in expired[id]:
|
||||
plan.amount += part.amount
|
||||
plan.from = minf(plan.from, part.from)
|
||||
plan.to = maxf(plan.to, part.to)
|
||||
for part : Dictionary in expired[id]:
|
||||
plan.amount = E.add(plan.amount, part.amount)
|
||||
plan.from = E.minimum(plan.from, part.from)
|
||||
plan.to = E.maximum(plan.to, part.to)
|
||||
var triggered: Dictionary = take(world, remaining[id], plan.amount)
|
||||
var successes: Dictionary = {}
|
||||
var failures: Dictionary = {}
|
||||
@@ -103,54 +102,54 @@ 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) * 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 p: Variant = E.maximum(0, E.multiply(E.of(row.progress), modifiers.progress))
|
||||
var d: Variant = E.maximum(0, E.multiply(E.of(row.degradation), modifiers.degradation))
|
||||
var scale: Variant = E.maximum(1, E.add(p, d))
|
||||
var pro: Variant = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":pro"), triggered[ethic], E.divide(p, scale))
|
||||
var deg: Variant = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":deg"), triggered[ethic], E.divide(d, scale))
|
||||
var bounded: Dictionary = QuantityMath.allocate_whole({"pro": pro, "deg": deg}, triggered[ethic])
|
||||
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))
|
||||
if rules[id].fights: warriors[ethic] = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":war"), triggered[ethic], E.minimum(1, E.multiply(E.of(row.war), modifiers.war)))
|
||||
if rules[id].colonizes: settlers[ethic] = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":col"), triggered[ethic], E.minimum(1, E.multiply(E.of(row.colonization), modifiers.colonization)))
|
||||
successes[ethic] = bounded.pro
|
||||
failures[ethic] = bounded.deg
|
||||
triggered[ethic] -= bounded.pro + bounded.deg
|
||||
triggered[ethic] = E.subtract(triggered[ethic], E.add(bounded.pro, bounded.deg))
|
||||
add(triggered, progress(world, target, rules, budget, plan, successes))
|
||||
fall(target, plan, rules, failures)
|
||||
if rules[id].fights:
|
||||
for ethic in warriors: warriors[ethic] = minf(warriors[ethic], triggered[ethic])
|
||||
var wars: float = minf(total(warriors), floorf((total(triggered) + total(remaining[id])) / 2.0))
|
||||
for ethic in warriors: warriors[ethic] = E.minimum(warriors[ethic], triggered[ethic])
|
||||
var wars: Variant = E.minimum(total(warriors), E.floor_value(E.divide((E.add(total(triggered), total(remaining[id]))), 2)))
|
||||
warriors = QuantityMath.allocate_whole(warriors, wars)
|
||||
for ethic in warriors: triggered[ethic] -= warriors[ethic]
|
||||
var outcomes: Dictionary = split(target, id + ":wars", wars, {"deg": 1.0, "pro25": 2.0, "pro50": 3.0, "pro75": 3.0})
|
||||
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, rate(target, id + ":attackers_lost", outcomes.get("deg", 0.0) / order, 1.0))
|
||||
for ethic in warriors: triggered[ethic] = E.subtract(triggered[ethic], warriors[ethic])
|
||||
var outcomes: Dictionary = split(target, E.add(id, ":wars"), wars, {"deg": 1, "pro25": 2, "pro50": 3, "pro75": 3})
|
||||
var win_expected: Variant = E.add(E.add(E.multiply(outcomes.get("pro25", 0), E.of("0.25")), E.multiply(outcomes.get("pro50", 0), E.of("0.5"))), E.multiply(outcomes.get("pro75", 0), E.of("0.75")))
|
||||
var loss_expected: Variant = E.add(E.add(E.add(E.multiply(outcomes.get("deg", 0), 2), outcomes.get("pro25", 0)), E.multiply(outcomes.get("pro50", 0), E.of("1.5"))), E.multiply(outcomes.get("pro75", 0), 2))
|
||||
var winners: Dictionary = take(world, warriors, rate(target, E.add(id, ":wins"), win_expected, 1))
|
||||
var attackers_lost: Dictionary = take(world, warriors, rate(target, E.add(id, ":attackers_lost"), E.divide(outcomes.get("deg", 0), order), 1))
|
||||
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 / order, 1.0), total(triggered) + total(remaining[id]))
|
||||
var losses: Variant = E.minimum(rate(target, E.add(id, ":loss"), E.divide(loss_expected, order), 1), E.add(total(triggered), total(remaining[id])))
|
||||
var sides: Dictionary = QuantityMath.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 = 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
|
||||
var damage: Variant = rate(target, E.add(id, ":collateral"), E.divide((E.add(E.multiply(outcomes.get("pro50", 0), 10), E.multiply(outcomes.get("pro75", 0), 30))), order), 1)
|
||||
collateral[rules[id].previous] = E.add(collateral.get(rules[id].previous, 0), damage)
|
||||
target.civilization_stats.wars = E.add(target.civilization_stats.get("wars", 0), wars)
|
||||
emit(plan, id, triggered)
|
||||
if total(settlers) > 0.0: colonies.append({"stage": rules[id].colony, "source": id, "population": settlers})
|
||||
if E.gt(total(settlers), 0): colonies.append({"stage": rules[id].colony, "source": id, "population": settlers})
|
||||
# Collateral is applied after all stages planned their own attempts.
|
||||
for id in collateral:
|
||||
if not rules.has(id): continue
|
||||
if not plans.has(id): plans[id] = new_plan(id, year)
|
||||
if (not rules.has(id)): continue
|
||||
if (not plans.has(id)): plans[id] = new_plan(id, year)
|
||||
var plan: Dictionary = plans[id]
|
||||
var retry: Dictionary = plan.outputs.get(id, {})
|
||||
var amount: float = minf(collateral[id], total(retry) + total(remaining[id]))
|
||||
var amount: Variant = E.minimum(collateral[id], E.add(total(retry), total(remaining[id])))
|
||||
var sides: Dictionary = QuantityMath.allocate_whole({"due": total(retry), "later": total(remaining[id])}, amount)
|
||||
var victims: Dictionary = take(world, retry, sides.due)
|
||||
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
|
||||
fall(target, plan, rules, victims)
|
||||
target.civilization_stats.collateral = target.civilization_stats.get("collateral", 0.0) + total(victims)
|
||||
target.civilization_stats.collateral = E.add(target.civilization_stats.get("collateral", 0), total(victims))
|
||||
var incoming: Dictionary = {}
|
||||
var planetary: Dictionary = {}
|
||||
for id in plans:
|
||||
@@ -159,70 +158,70 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
||||
for destination in plan.outputs:
|
||||
var pop: Dictionary = plan.outputs[destination]
|
||||
if rules.has(destination):
|
||||
if destination != "Ct":
|
||||
pop = drift(target, id + ":" + destination + ":drift", pop, rules[id].ethics, destination == id and rules[id].diplomacy)
|
||||
if not incoming.has(destination): incoming[destination] = {}
|
||||
if E.ne(destination, "Ct"):
|
||||
pop = drift(target, E.add(E.add(E.add(id, ":"), destination), ":drift"), pop, rules[id].ethics, (E.eq(destination, id) and rules[id].diplomacy))
|
||||
if (not incoming.has(destination)): incoming[destination] = {}
|
||||
add(incoming[destination], pop)
|
||||
products[destination] = total(pop)
|
||||
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}]
|
||||
if (State.is_space_civilization(id, rules) and E.eq(world.catalog.entities[destination].group_as, "Planet")):
|
||||
planetary[destination] = E.add(planetary.get(destination, 0), total(pop))
|
||||
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": E.maximum(plan.from, plan.to), "products": products}]
|
||||
return {"remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies}
|
||||
|
||||
static func finish(world, target: Dictionary, transaction: Dictionary, year: float, rules: Dictionary) -> void:
|
||||
static func finish(world, target: Dictionary, transaction: Dictionary, year: Variant, rules: Dictionary) -> void:
|
||||
target.ethics = transaction.remaining
|
||||
for id in transaction.incoming:
|
||||
if not target.ethics.has(id): target.ethics[id] = {}
|
||||
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.ledger.physical_amounts(target)
|
||||
var hosts: Dictionary = {}
|
||||
for id in physical:
|
||||
if world.catalog.entities[id].event_group == "DwStar" and physical[id] > 0.0: hosts[id] = physical[id]
|
||||
if (E.eq(world.catalog.entities[id].event_group, "DwStar") and E.gt(physical[id], 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)
|
||||
var shares: Dictionary = split(target, E.add("return:", id), E.minimum(transaction.planetary[id], target.amount.get(id, 0)), hosts)
|
||||
for owner in shares:
|
||||
if not target.binds.has(owner): target.binds[owner] = {}
|
||||
target.binds[owner][id] = target.binds[owner].get(id, 0.0) + shares[owner]
|
||||
target.amount[id] -= shares[owner]
|
||||
if (not target.binds.has(owner)): target.binds[owner] = {}
|
||||
target.binds[owner][id] = E.add(target.binds[owner].get(id, 0), shares[owner])
|
||||
target.amount[id] = E.subtract(target.amount[id], shares[owner])
|
||||
colonize(world, target, transaction.colonies, year, rules)
|
||||
State.reconcile(world, target, rules)
|
||||
var worship: float = 0.0
|
||||
var worship: Variant = 0
|
||||
for id in rules:
|
||||
if not rules[id].worships: continue
|
||||
for ethic in target.ethics[id]: worship += target.ethics[id][ethic] * float(rules[id].ethics[ethic].worship)
|
||||
if (not rules[id].worships): continue
|
||||
for ethic in target.ethics[id]: worship = E.add(worship, E.multiply(target.ethics[id][ethic], E.of(rules[id].ethics[ethic].worship)))
|
||||
target.civilization_stats.worship = worship
|
||||
|
||||
static func colonize(world, target: Dictionary, colonies: Array, year: float, rules: Dictionary) -> void:
|
||||
static func colonize(world, target: Dictionary, colonies: Array, year: Variant, rules: Dictionary) -> void:
|
||||
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)
|
||||
for id in ["GG", "AB", "RP", "HP"]: stock[id] = physical.get(id, 0)
|
||||
var requests: Dictionary = {}
|
||||
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
|
||||
var granted: Dictionary = QuantityMath.allocate_whole(requests, total(stock))
|
||||
for i in colonies.size():
|
||||
var colony: Dictionary = colonies[i]
|
||||
var population: Dictionary = QuantityMath.allocate_whole(colony.population, granted[str(i)])
|
||||
population = drift(target, colony.source + ":colonies", population, rules[colony.source].ethics, false, true)
|
||||
var count: float = total(population)
|
||||
population = drift(target, E.add(colony.source, ":colonies"), population, rules[colony.source].ethics, false, true)
|
||||
var count: Variant = total(population)
|
||||
var targets: Dictionary = QuantityMath.allocate_whole(stock, count)
|
||||
for id in targets:
|
||||
var amount: float = targets[id]
|
||||
if amount <= 0.0: continue
|
||||
var slots: Dictionary = {"": target.amount.get(id, 0.0)}
|
||||
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0.0)
|
||||
var amount: Variant = targets[id]
|
||||
if E.le(amount, 0): continue
|
||||
var slots: Dictionary = {"": target.amount.get(id, 0)}
|
||||
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0)
|
||||
var taken: Dictionary = QuantityMath.allocate_whole(slots, amount)
|
||||
world.batches.scale_batches(target, id, 1.0 - amount / stock[id])
|
||||
stock[id] -= amount
|
||||
world.batches.scale_batches(target, id, E.subtract(1, E.divide(amount, stock[id])))
|
||||
stock[id] = E.subtract(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.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
|
||||
if E.le(taken[owner], 0): continue
|
||||
if E.eq(owner, ""): target.amount[id] = E.subtract(target.amount[id], taken[owner])
|
||||
else: target.binds[owner][id] = E.subtract(target.binds[owner][id], taken[owner])
|
||||
var created: Variant = world.ledger.credit(target, colony.stage, taken[owner])
|
||||
if E.ne(owner, ""):
|
||||
target.amount[colony.stage] = E.subtract(target.amount[colony.stage], created)
|
||||
target.binds[owner][colony.stage] = E.add(target.binds[owner].get(colony.stage, 0), created)
|
||||
world.batches.append_batch(target, colony.stage, amount, year, year)
|
||||
if not target.ethics.has(colony.stage): target.ethics[colony.stage] = {}
|
||||
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
|
||||
target.civilization_stats.colonies = E.add(target.civilization_stats.get("colonies", 0), count)
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
const ETHICS = ["m", "l", "w", "d", "c", "b"]
|
||||
|
||||
static func is_space_civilization(id: String, rules: Dictionary) -> bool:
|
||||
return rules.has(id) and str(rules[id].home) != ""
|
||||
return (rules.has(id) and E.ne(str(rules[id].home), ""))
|
||||
|
||||
# Preserve any existing ethics. New populations start equally distributed;
|
||||
# reductions (including blasts) are proportional and never leave negative counts.
|
||||
static func reconcile(world, target: Dictionary, rules: Dictionary) -> void:
|
||||
if not target.has("ethics"):
|
||||
if (not target.has("ethics")):
|
||||
target.ethics = {}
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
for id in rules:
|
||||
var count: float = physical.get(id, 0.0)
|
||||
var count: Variant = physical.get(id, 0)
|
||||
var current: Dictionary = target.ethics.get(id, {})
|
||||
var total: float = 0.0
|
||||
var total: Variant = 0
|
||||
for ethic in ETHICS:
|
||||
current[ethic] = maxf(0.0, current.get(ethic, 0.0))
|
||||
total += current[ethic]
|
||||
if total > count:
|
||||
current[ethic] = E.maximum(0, current.get(ethic, 0))
|
||||
total = E.add(total, current[ethic])
|
||||
if E.gt(total, count):
|
||||
current = QuantityMath.allocate_whole(current, count)
|
||||
elif total < count:
|
||||
var missing: float = count - total
|
||||
var base: float = floorf(missing / ETHICS.size())
|
||||
var rest: int = int(missing - base * ETHICS.size())
|
||||
elif E.lt(total, count):
|
||||
var missing: Variant = E.subtract(count, total)
|
||||
var base: Variant = E.floor_value(E.divide(missing, ETHICS.size()))
|
||||
var rest: int = E.integer(E.subtract(missing, E.multiply(base, ETHICS.size())))
|
||||
for i in ETHICS.size():
|
||||
current[ETHICS[i]] += base + (1.0 if i < rest else 0.0)
|
||||
current[ETHICS[i]] = E.add(current[ETHICS[i]], E.add(base, ((1 if E.lt(i, rest) else 0))))
|
||||
target.ethics[id] = current
|
||||
|
||||
static func detach_space_civilizations(target: Dictionary, rules: Dictionary) -> void:
|
||||
for held: Dictionary in target.binds.values():
|
||||
for held : Dictionary in target.binds.values():
|
||||
for id in held:
|
||||
if is_space_civilization(id, rules):
|
||||
target.amount[id] = target.amount.get(id, 0.0) + held[id]
|
||||
held[id] = 0.0
|
||||
target.amount[id] = E.add(target.amount.get(id, 0), held[id])
|
||||
held[id] = 0
|
||||
|
||||
# A shared budget per stellar family, including stars bound into structures.
|
||||
# Reservations affect capacity, not the physical stock or its production.
|
||||
@@ -45,65 +46,64 @@ static func stellar_budget(world, target: Dictionary, rules: Dictionary) -> Dict
|
||||
for id in physical:
|
||||
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]
|
||||
capacity["SMBH"] = maxf(0.0, target.amount.get("SMBH", 0.0))
|
||||
for held: Dictionary in target.binds.values():
|
||||
capacity["SMBH"] += maxf(0.0, held.get("SMBH", 0.0))
|
||||
if ((E.eq(entity.event_group, home) or E.eq(entity.count_as, home)) or E.eq(id, home)):
|
||||
capacity[home] = E.add(capacity.get(home, 0), physical[id])
|
||||
capacity["SMBH"] = E.maximum(0, target.amount.get("SMBH", 0))
|
||||
for held : Dictionary in target.binds.values():
|
||||
capacity["SMBH"] = E.add(capacity["SMBH"], E.maximum(0, held.get("SMBH", 0)))
|
||||
var populations: Dictionary = {}
|
||||
var requested: Dictionary = {}
|
||||
for id in rules:
|
||||
if not is_space_civilization(id, rules):
|
||||
if (not is_space_civilization(id, rules)):
|
||||
continue
|
||||
populations[id] = physical.get(id, 0.0)
|
||||
populations[id] = physical.get(id, 0)
|
||||
var home: String = rules[id].home
|
||||
requested[home] = requested.get(home, 0.0) + populations[id] * maxf(1.0, rules[id].stars)
|
||||
requested[home] = E.add(requested.get(home, 0), E.multiply(populations[id], E.maximum(1, rules[id].stars)))
|
||||
var free: Dictionary = {}
|
||||
for home in capacity:
|
||||
free[home] = maxf(0.0, capacity[home] - requested.get(home, 0.0))
|
||||
free[home] = E.maximum(0, E.subtract(capacity[home], requested.get(home, 0)))
|
||||
return {"capacity": capacity, "requested": requested, "free": free, "populations": populations, "habitats": habitat_budget(world, target, rules)}
|
||||
|
||||
# Account for the source's existing reservation when changing stages. A failed
|
||||
# transition retains its population at the source, rather than losing objects.
|
||||
static func admit_transition(budget: Dictionary, rules: Dictionary, source: String,
|
||||
destination: String, requested: float) -> float:
|
||||
if source == destination or not is_space_civilization(destination, rules):
|
||||
static func admit_transition(budget: Dictionary, rules: Dictionary, source: String, destination: String, requested: Variant) -> Variant:
|
||||
if (E.eq(source, destination) or (not is_space_civilization(destination, rules))):
|
||||
return requested
|
||||
var home: String = rules[destination].home
|
||||
var cost: float = maxf(1.0, rules[destination].stars)
|
||||
var cost: Variant = E.maximum(1, rules[destination].stars)
|
||||
var old_home: String = ""
|
||||
var old_cost: float = 0.0
|
||||
var old_cost: Variant = 0
|
||||
if is_space_civilization(source, rules):
|
||||
old_home = rules[source].home
|
||||
old_cost = maxf(1.0, rules[source].stars)
|
||||
old_cost = E.maximum(1, rules[source].stars)
|
||||
# Existing overcapacity is handled by the separate population policy.
|
||||
if budget.requested.get(home, 0.0) > budget.capacity.get(home, 0.0):
|
||||
return 0.0
|
||||
var net_cost: float = cost - old_cost if old_home == home else cost
|
||||
var admitted: float = floorf(maxf(0.0, requested))
|
||||
if net_cost > 0.0:
|
||||
admitted = minf(admitted, floorf(budget.free.get(home, 0.0) / net_cost))
|
||||
if E.gt(budget.requested.get(home, 0), budget.capacity.get(home, 0)):
|
||||
return 0
|
||||
var net_cost: Variant = (E.subtract(cost, old_cost) if E.eq(old_home, home) else cost)
|
||||
var admitted: Variant = E.floor_value(E.maximum(0, requested))
|
||||
if E.gt(net_cost, 0):
|
||||
admitted = E.minimum(admitted, E.floor_value(E.divide(budget.free.get(home, 0), net_cost)))
|
||||
if budget.has("habitats"):
|
||||
var housing: Dictionary = budget.habitats
|
||||
var new_req: Dictionary = housing.requirements.get(destination, {})
|
||||
var old_req: Dictionary = housing.requirements.get(source, {})
|
||||
if not new_req.is_empty():
|
||||
var net_units: float = new_req.units - (old_req.units if old_req.get("id", "") == new_req.id else 0.0)
|
||||
if net_units > 0.0:
|
||||
var free_units: float = maxf(0.0, housing.capacity.get(new_req.id, 0.0) - housing.requested.get(new_req.id, 0.0))
|
||||
admitted = minf(admitted, floorf(free_units / net_units))
|
||||
if not old_req.is_empty(): housing.requested[old_req.id] = maxf(0.0, housing.requested.get(old_req.id, 0.0) - admitted * old_req.units)
|
||||
if not new_req.is_empty(): housing.requested[new_req.id] = housing.requested.get(new_req.id, 0.0) + admitted * new_req.units
|
||||
if old_home != "":
|
||||
budget.requested[old_home] = maxf(0.0, budget.requested.get(old_home, 0.0) - admitted * old_cost)
|
||||
budget.free[old_home] = maxf(0.0, budget.capacity.get(old_home, 0.0) - budget.requested[old_home])
|
||||
budget.requested[home] = budget.requested.get(home, 0.0) + admitted * cost
|
||||
budget.free[home] = maxf(0.0, budget.capacity.get(home, 0.0) - budget.requested[home])
|
||||
if (not new_req.is_empty()):
|
||||
var net_units: Variant = E.subtract(new_req.units, ((old_req.units if E.eq(old_req.get("id", ""), new_req.id) else 0)))
|
||||
if E.gt(net_units, 0):
|
||||
var free_units: Variant = E.maximum(0, E.subtract(housing.capacity.get(new_req.id, 0), housing.requested.get(new_req.id, 0)))
|
||||
admitted = E.minimum(admitted, E.floor_value(E.divide(free_units, net_units)))
|
||||
if (not old_req.is_empty()): housing.requested[old_req.id] = E.maximum(0, E.subtract(housing.requested.get(old_req.id, 0), E.multiply(admitted, old_req.units)))
|
||||
if (not new_req.is_empty()): housing.requested[new_req.id] = E.add(housing.requested.get(new_req.id, 0), E.multiply(admitted, new_req.units))
|
||||
if E.ne(old_home, ""):
|
||||
budget.requested[old_home] = E.maximum(0, E.subtract(budget.requested.get(old_home, 0), E.multiply(admitted, old_cost)))
|
||||
budget.free[old_home] = E.maximum(0, E.subtract(budget.capacity.get(old_home, 0), budget.requested[old_home]))
|
||||
budget.requested[home] = E.add(budget.requested.get(home, 0), E.multiply(admitted, cost))
|
||||
budget.free[home] = E.maximum(0, E.subtract(budget.capacity.get(home, 0), budget.requested[home]))
|
||||
return admitted
|
||||
|
||||
# Resolve shortages from the most advanced stages downwards. This gives existing
|
||||
# advanced populations priority and lets a demotion cascade in the same cycle.
|
||||
static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: float) -> void:
|
||||
static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: Variant) -> void:
|
||||
detach_space_civilizations(target, rules)
|
||||
reconcile(world, target, rules)
|
||||
var budget: Dictionary = stellar_budget(world, target, rules)
|
||||
@@ -114,50 +114,50 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
||||
if is_space_civilization(id, rules):
|
||||
stages.append(id)
|
||||
stages.sort()
|
||||
for index in range(stages.size() - 1, -1, -1):
|
||||
for index in range(E.subtract(stages.size(), 1), E.neg(1), E.neg(1)):
|
||||
var id: String = stages[index]
|
||||
var home: String = rules[id].home
|
||||
var cost: float = maxf(1.0, rules[id].stars)
|
||||
var count: float = target.amount.get(id, 0.0)
|
||||
var supported: float = minf(count, floorf(remaining.get(home, 0.0) / cost))
|
||||
var cost: Variant = E.maximum(1, rules[id].stars)
|
||||
var count: Variant = target.amount.get(id, 0)
|
||||
var supported: Variant = E.minimum(count, E.floor_value(E.divide(remaining.get(home, 0), cost)))
|
||||
var requirement: Dictionary = budget.habitats.requirements.get(id, {})
|
||||
if not requirement.is_empty():
|
||||
supported = minf(supported, floorf(remaining_homes.get(requirement.id, 0.0) / requirement.units))
|
||||
remaining_homes[requirement.id] = maxf(0.0, remaining_homes.get(requirement.id, 0.0) - supported * requirement.units)
|
||||
remaining[home] = maxf(0.0, remaining.get(home, 0.0) - supported * cost)
|
||||
var excess: float = count - supported
|
||||
if excess <= 0.0:
|
||||
if (not requirement.is_empty()):
|
||||
supported = E.minimum(supported, E.floor_value(E.divide(remaining_homes.get(requirement.id, 0), requirement.units)))
|
||||
remaining_homes[requirement.id] = E.maximum(0, E.subtract(remaining_homes.get(requirement.id, 0), E.multiply(supported, requirement.units)))
|
||||
remaining[home] = E.maximum(0, E.subtract(remaining.get(home, 0), E.multiply(supported, cost)))
|
||||
var excess: Variant = E.subtract(count, supported)
|
||||
if E.le(excess, 0):
|
||||
continue
|
||||
var previous: String = stages[index - 1] if index > 0 else "C1.9"
|
||||
world.batches.scale_batches(target, id, supported / count)
|
||||
var previous: String = (stages[E.subtract(index, 1)] if E.gt(index, 0) else "C1.9")
|
||||
world.batches.scale_batches(target, id, E.divide(supported, count))
|
||||
target.amount[id] = supported
|
||||
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):
|
||||
if (not target.ethics.has(previous)):
|
||||
target.ethics[previous] = {}
|
||||
for ethic in ETHICS:
|
||||
target.ethics[id][ethic] -= removed[ethic]
|
||||
target.ethics[previous][ethic] = target.ethics[previous].get(ethic, 0.0) + removed[ethic]
|
||||
if previous == "C1.9":
|
||||
target.ethics[id][ethic] = E.subtract(target.ethics[id][ethic], removed[ethic])
|
||||
target.ethics[previous][ethic] = E.add(target.ethics[previous].get(ethic, 0), removed[ethic])
|
||||
if E.eq(previous, "C1.9"):
|
||||
var stars: Dictionary = {}
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
for star in physical:
|
||||
if world.catalog.entities[star].event_group == "DwStar" and physical[star] > 0.0:
|
||||
if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(physical[star], 0)):
|
||||
stars[star] = physical[star]
|
||||
var weights: Dictionary = QuantityMath.normalize_distribution(stars)
|
||||
# Worlds are aggregates: a star may host several inhabited planets.
|
||||
var assigned: float = 0.0
|
||||
var assigned: Variant = 0
|
||||
var owners: Array = weights.keys()
|
||||
owners.sort()
|
||||
for i in owners.size():
|
||||
var owner: String = owners[i]
|
||||
var amount: float = floorf(excess * weights[owner]) if i + 1 < owners.size() else excess - assigned
|
||||
assigned += amount
|
||||
if not target.binds.has(owner):
|
||||
var amount: Variant = (E.floor_value(E.multiply(excess, weights[owner])) if E.lt(E.add(i, 1), owners.size()) else E.subtract(excess, assigned))
|
||||
assigned = E.add(assigned, amount)
|
||||
if (not target.binds.has(owner)):
|
||||
target.binds[owner] = {}
|
||||
target.binds[owner][previous] = target.binds[owner].get(previous, 0.0) + amount
|
||||
target.amount[previous] -= amount
|
||||
target.binds[owner][previous] = E.add(target.binds[owner].get(previous, 0), amount)
|
||||
target.amount[previous] = E.subtract(target.amount[previous], amount)
|
||||
reconcile(world, target, rules)
|
||||
assign_homes(world, target, rules)
|
||||
|
||||
@@ -168,13 +168,10 @@ static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dict
|
||||
var structures: Array = []
|
||||
var capacity: Dictionary = {}
|
||||
for id in world.catalog.entities:
|
||||
if world.catalog.entities[id].claim != "":
|
||||
if E.ne(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.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))
|
||||
capacity[id] = physical.get(id, 0)
|
||||
structures.sort_custom(func ( a , b ) : return (E.gt(world.catalog.entities[a].civ_bonus, world.catalog.entities[b].civ_bonus) if E.ne(world.catalog.entities[a].civ_bonus, world.catalog.entities[b].civ_bonus) else E.lt(str(a), str(b))))
|
||||
var stages: Array = []
|
||||
for id in rules:
|
||||
if is_space_civilization(id, rules): stages.append(id)
|
||||
@@ -182,23 +179,23 @@ static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dict
|
||||
var requirements: Dictionary = {}
|
||||
var anchors: Dictionary = {}
|
||||
var anchor: String = ""
|
||||
var units: float = 1.0
|
||||
var units: Variant = 1
|
||||
for id in stages:
|
||||
var direct: String = ""
|
||||
for structure in structures:
|
||||
if world.catalog.entities[structure].claim == id: direct = structure
|
||||
if direct != "":
|
||||
if E.eq(world.catalog.entities[structure].claim, id): direct = structure
|
||||
if E.ne(direct, ""):
|
||||
anchor = direct
|
||||
units = 1.0
|
||||
units = 1
|
||||
else:
|
||||
units *= 10.0
|
||||
units = E.multiply(units, 10)
|
||||
anchors[id] = anchor
|
||||
if id.begins_with("C3.") or id.begins_with("C4."):
|
||||
if (id.begins_with("C3.") or id.begins_with("C4.")):
|
||||
requirements[id] = {"id": anchor, "units": units}
|
||||
var requested: Dictionary = {}
|
||||
for id in requirements:
|
||||
var req: Dictionary = requirements[id]
|
||||
requested[req.id] = requested.get(req.id, 0.0) + physical.get(id, 0.0) * req.units
|
||||
requested[req.id] = E.add(requested.get(req.id, 0), E.multiply(physical.get(id, 0), req.units))
|
||||
return {"capacity": capacity, "requested": requested, "requirements": requirements, "structures": structures, "anchors": anchors}
|
||||
|
||||
static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
|
||||
@@ -209,29 +206,29 @@ static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
|
||||
stages.sort()
|
||||
stages.reverse()
|
||||
for id in stages:
|
||||
var left: float = target.amount.get(id, 0.0)
|
||||
var left: Variant = target.amount.get(id, 0)
|
||||
var homes: Dictionary = {}
|
||||
var allowed: bool = budget.anchors[id] == ""
|
||||
var allowed: bool = E.eq(budget.anchors[id], "")
|
||||
for structure in budget.structures:
|
||||
if structure == budget.anchors[id]: allowed = true
|
||||
if not allowed or budget.capacity[structure] <= 0.0: continue
|
||||
var amount: float = left
|
||||
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
|
||||
left -= amount
|
||||
if left <= 0.0: break
|
||||
if E.eq(structure, budget.anchors[id]): allowed = true
|
||||
if ((not allowed) or E.le(budget.capacity[structure], 0)): continue
|
||||
var amount: Variant = left
|
||||
if E.eq(world.catalog.entities[structure].claim, id):
|
||||
amount = E.minimum(amount, E.maximum(0, E.subtract(budget.capacity[structure], used.get(structure, 0))))
|
||||
used[structure] = E.add(used.get(structure, 0), amount)
|
||||
if E.gt(amount, 0): homes[structure] = amount
|
||||
left = E.subtract(left, amount)
|
||||
if E.le(left, 0): break
|
||||
target.civilization_homes[id] = homes
|
||||
|
||||
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> float:
|
||||
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
|
||||
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> Variant:
|
||||
if (not rules.has(id)): return world.catalog.entities[id].lifespan
|
||||
var base: Variant = rules[id].span
|
||||
if (not is_space_civilization(id, rules)): return base
|
||||
assign_homes(world, target, rules)
|
||||
var population: float = target.amount.get(id, 0.0)
|
||||
if population <= 0.0: return base
|
||||
var weighted: float = population
|
||||
var population: Variant = target.amount.get(id, 0)
|
||||
if E.le(population, 0): return base
|
||||
var weighted: Variant = population
|
||||
for home in target.civilization_homes.get(id, {}):
|
||||
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
|
||||
weighted = E.add(weighted, E.multiply(target.civilization_homes[id][home], (E.subtract(E.maximum(0, world.catalog.entities[home].civ_bonus), 1))))
|
||||
return (E.divide(E.multiply(base, population), weighted) if E.gt(weighted, 0) else base)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.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")
|
||||
@@ -12,26 +13,26 @@ func initialize_life_batches() -> void:
|
||||
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)
|
||||
var tracked: Variant = 0
|
||||
for batch : Dictionary in world.state.batches.get(id, []):
|
||||
tracked = E.add(tracked, batch.amount)
|
||||
world.batches.append_batch(world.state, id, E.maximum(0, E.subtract(physical.get(id, 0), tracked)), world.state.time.datetime, world.state.time.datetime)
|
||||
|
||||
|
||||
func evolute_to_datetime(datetime: float) -> void:
|
||||
func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}) -> 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:
|
||||
var processed_year: Variant = SimulationTime.processed_year(datetime)
|
||||
if E.gt(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:
|
||||
next.speed = clampi(E.integer(previous.speed), 1, Global.SPEED_LIMIT)
|
||||
next.speed_in_year = E.power(10, E.subtract(next.speed, 1))
|
||||
var from_year: Variant = previous.time.year
|
||||
var to_year: Variant = processed_year
|
||||
if E.gt(to_year, from_year):
|
||||
var quantities: Dictionary = world.ledger.physical_amounts(previous)
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
|
||||
next.distribution = distribution
|
||||
@@ -40,23 +41,23 @@ func evolute_to_datetime(datetime: float) -> void:
|
||||
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}
|
||||
WorldParameters.advance(previous, next, Global.CivilizationRules, E.subtract(to_year, from_year))
|
||||
next.time = clock if not clock.is_empty() else {"datetime": datetime, "year": to_year}
|
||||
|
||||
# Publish only after every phase has finished; previous stays untouched.
|
||||
world.state = next
|
||||
if to_year > from_year:
|
||||
world.state = E.bound_state(next)
|
||||
if E.gt(to_year, from_year):
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
|
||||
|
||||
func evolve_batches(target: Dictionary, to_year: float) -> void:
|
||||
func evolve_batches(target: Dictionary, to_year: Variant) -> 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:
|
||||
func evolve_batch_generation(target: Dictionary, to_year: Variant, 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)
|
||||
@@ -65,81 +66,81 @@ func evolve_batch_generation(target: Dictionary, to_year: float, completed: Dict
|
||||
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:
|
||||
if (world.catalog.entities.has(id) and E.gt(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:
|
||||
if ((not world.catalog.entities.has(id)) or E.le(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)
|
||||
var dying: Variant = 0
|
||||
for part : Dictionary in completed_now.get(id, []):
|
||||
dying = E.add(dying, part.amount)
|
||||
for part : Dictionary in world.batches.withdraw_batches(preview, id, to_year):
|
||||
dying = E.add(dying, part.amount)
|
||||
if E.gt(quantities.get(id, 0), 0):
|
||||
world.stellar.destroy_planets(target, id, E.divide(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)
|
||||
expired[id] = E.add(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():
|
||||
for parts : Array in expired.values():
|
||||
if (not parts.is_empty()):
|
||||
has_expired = true
|
||||
break
|
||||
if not has_expired:
|
||||
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 total: Variant = 0
|
||||
for part : Dictionary in expired[id]:
|
||||
total = E.add(total, part.amount)
|
||||
fractions[id] = (E.clamp_value(E.divide(total, quantities[id]), 0, 1) if E.gt(quantities.get(id, 0), 0) else 0)
|
||||
var combined: Dictionary = {}
|
||||
for part: Dictionary in expired[id]:
|
||||
if not part.has("products"):
|
||||
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)
|
||||
var requested: Variant = part.products[successor]
|
||||
var admitted: Variant = 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
|
||||
if E.lt(admitted, requested):
|
||||
part.products[id] = E.subtract(E.add(part.products.get(id, 0), requested), admitted)
|
||||
for successor in part.products:
|
||||
combined[successor] = combined.get(successor, 0.0) + part.products[successor]
|
||||
combined[successor] = E.add(combined.get(successor, 0), part.products[successor])
|
||||
outputs[id] = {}
|
||||
if total > 0.0:
|
||||
if E.gt(total, 0):
|
||||
for successor in combined:
|
||||
outputs[id][successor] = combined[successor] / total
|
||||
outputs[id][successor] = E.divide(combined[successor], total)
|
||||
|
||||
var incoming: Dictionary = world.ledger.transfer_evolution(target, fractions, outputs)
|
||||
|
||||
var new_owners: Dictionary = {}
|
||||
var blast_power: float = 0.0
|
||||
var blast_power: Variant = 0
|
||||
var environment: Dictionary = world.environment.stellar_environment(target)
|
||||
for id in expired:
|
||||
for part: Dictionary in expired[id]:
|
||||
for part : Dictionary in expired[id]:
|
||||
for successor in part.products:
|
||||
var amount: float = part.products[successor]
|
||||
if successor == "blast":
|
||||
blast_power += amount
|
||||
var amount: Variant = part.products[successor]
|
||||
if E.eq(successor, "blast"):
|
||||
blast_power = E.add(blast_power, amount)
|
||||
continue
|
||||
if not world.catalog.entities.has(successor):
|
||||
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
|
||||
if (E.eq(successor, id) and (((not world.catalog.entities[id].life_rule.is_empty()) or Global.CivilizationRules.has(id)))):
|
||||
target.amount[id] = E.add(target.amount.get(id, 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)
|
||||
var cutoff: Variant = (to_year if world.stellar.uses_short_lived_fast_path(successor) else E.neg(INF))
|
||||
var integral: Variant = 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
|
||||
new_owners[successor] = E.add(new_owners.get(successor, 0), amount)
|
||||
world.ledger.accept_transfers(target, incoming)
|
||||
world.ledger.bind_new_owners(target, new_owners)
|
||||
world.ledger.round_object_allocations(target)
|
||||
|
||||
@@ -1,101 +1,90 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
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):
|
||||
return "—"
|
||||
if absf(value) >= 1e5:
|
||||
var exponent: int = int(floor(log(absf(value)) / log(10.0)))
|
||||
return "%.4fE%+d" % [value / pow(10.0, exponent), exponent]
|
||||
return "%.*f" % [digits, value]
|
||||
static func number(value, digits: int = 0) -> String:
|
||||
if not E.finite(value): return "—"
|
||||
return E.scientific(value, 4) if E.ge(E.absolute(value), 100000) else E.fixed(value, digits)
|
||||
|
||||
static func colored(text: String, color: String) -> String:
|
||||
return "[color=" + color + "]" + text + "[/color]"
|
||||
return E.add(E.add(E.add(E.add("[color=", color), "]"), text), "[/color]")
|
||||
|
||||
static func matches(world, id: String) -> Array:
|
||||
var result: Array = []
|
||||
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):
|
||||
if ((E.eq(key, id) or E.eq(entity.count_as, id)) or E.eq(entity.group_as, id)):
|
||||
if (not world.catalog.is_group(key)):
|
||||
result.append(key)
|
||||
return result
|
||||
|
||||
static func quantities(world, id: String) -> Dictionary:
|
||||
var free: float = 0.0
|
||||
var total: float = 0.0
|
||||
var free: Variant = 0
|
||||
var total: Variant = 0
|
||||
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)
|
||||
if id == "SMBH":
|
||||
free = world.state.amount.get(id, 0.0)
|
||||
free = E.add(free, world.state.amount.get(key, 0))
|
||||
total = E.add(total, physical.get(key, 0))
|
||||
if E.eq(id, "SMBH"):
|
||||
free = world.state.amount.get(id, 0)
|
||||
total = free
|
||||
return {"free": free, "total": total}
|
||||
|
||||
static func costs(world, global: Node, id: String) -> String:
|
||||
if not world.catalog.entities.has(id):
|
||||
if (not world.catalog.entities.has(id)):
|
||||
return ""
|
||||
var lines: PackedStringArray = []
|
||||
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:
|
||||
var required: Variant = requirements[resource]
|
||||
if E.lt(required, 0):
|
||||
continue
|
||||
var available: float = floorf(world.ledger.available(world.state, resource))
|
||||
var available: Variant = E.floor_value(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))
|
||||
lines.append(colored(E.add(E.add(E.add(E.add(E.add(name, ": "), number(required)), " ("), number(available)), ")"), (GREEN if E.ge(available, required) else RED)))
|
||||
return "\n".join(lines)
|
||||
|
||||
static func variables(world, global: Node, id: String, rate: float = 0.0) -> Dictionary:
|
||||
static func variables(world, global: Node, id: String, rate: Variant = 0.0) -> Dictionary:
|
||||
var counts: Dictionary = quantities(world, id)
|
||||
var show_total: bool = false
|
||||
for key in matches(world, id):
|
||||
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)
|
||||
show_total = (show_total or E.eq(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 E.gt(counts.total, counts.free)):
|
||||
amount = E.add(amount, E.add(" ", colored(E.add(E.add("(", number(counts.total)), ")"), GREY)))
|
||||
if E.ne(rate, 0):
|
||||
amount = E.add(amount, E.add(" | ", colored(E.add((("−" if E.lt(rate, 0) else "+")), number(E.absolute(rate), 1)), (RED if E.lt(rate, 0) else GREEN))))
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
var voids: float = 0.0
|
||||
var voids: Variant = 0
|
||||
for key in physical:
|
||||
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)
|
||||
voids = E.add(voids, E.multiply(physical[key], world.catalog.entities[key].voids))
|
||||
var he: Variant = world.state.amount.get("He", 0)
|
||||
var h: Variant = world.state.amount.get("H", 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) + "%"
|
||||
if E.gt(E.add(he, h), 0):
|
||||
mixture = E.add(E.add(E.add(E.add("He: ", number(E.divide(E.multiply(100, he), (E.add(he, h))), 2)), "% / H: "), number(E.divide(E.multiply(100, h), (E.add(he, h))), 2)), "%")
|
||||
var environment: Dictionary = world.environment.stellar_environment(world.state)
|
||||
return {
|
||||
"amount": amount, "colored costs": costs(world, global, id),
|
||||
"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),
|
||||
"лет в секунду": number(world.state.speed_in_year),
|
||||
"amount1": "—", "amount2": "—", "amount3": "—", "amount4": "—",
|
||||
}
|
||||
return {"amount": amount, "colored costs": costs(world, global, id), "lifespan": (number(world.catalog.entities[id].lifespan) if world.catalog.entities.has(id) else "—"), "voids": number(voids), "he / h": E.add("\n", mixture), "metallicity": E.add(number(E.multiply(environment.metallicity, 100), 2), "%"), "энергия": number(world.state.energy), "плотность": number(environment.density, 6), "лет в секунду": number(world.state.speed_in_year), "amount1": "—", "amount2": "—", "amount3": "—", "amount4": "—"}
|
||||
|
||||
static func render(world, global: Node, id: String, category: String = "Entity", rate: float = 0.0) -> String:
|
||||
static func render(world, global: Node, id: String, category: String = "Entity", rate: Variant = 0.0) -> String:
|
||||
var values: Dictionary = variables(world, global, id, rate)
|
||||
if category == "Indicator":
|
||||
if id == "Density":
|
||||
if E.eq(category, "Indicator"):
|
||||
if E.eq(id, "Density"):
|
||||
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])
|
||||
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":
|
||||
var level: Variant = Parameters.value(world.state, scales[id])
|
||||
values.amount = number(level, (1 if E.ne(level, E.floor_value(level)) else 0))
|
||||
values.scale = global.render_template(global.get_template(E.add("Scale", str(clampi(E.integer(E.round_value(level)), 1, 19))), "Indicator", "Scale"), {})
|
||||
if E.eq(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) + "%)"
|
||||
values[trend.to_lower()] = E.add(number(E.multiply(world.state.get("trends", {}).get(trend, 0), 100), 2), "%")
|
||||
if E.eq(id, "Energy"):
|
||||
values["энергия"] = E.add(E.add(E.add(number(world.state.energy), " ("), number(E.multiply(E.clamp_value(E.divide(world.state.energy, Parameters.INITIAL_ENERGY), 0, 1), 100), 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
|
||||
return E.add(E.add(E.add("[b]", ((title if E.ne(title, "") else id))), "[/b]\n"), body)
|
||||
|
||||
@@ -1,54 +1,55 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
## Read-only projections of simulation state. No controls or simulation mutations.
|
||||
|
||||
var pending_amounts: Dictionary[String, float] = {}
|
||||
var pending_amounts: Dictionary [ String , Variant ] = {}
|
||||
var deferred_updates: bool = false
|
||||
var dirty: bool = true
|
||||
|
||||
func refresh(state: Dictionary, catalog, force: bool = false) -> void:
|
||||
dirty = true
|
||||
if deferred_updates and not force:
|
||||
if (deferred_updates and (not force)):
|
||||
return
|
||||
pending_amounts = amounts(state, catalog)
|
||||
dirty = false
|
||||
|
||||
## Aliases are presentation-only; group counters are not counted twice.
|
||||
static func amounts(state: Dictionary, catalog, total_produced: bool = false) -> Dictionary[String, float]:
|
||||
var source: Dictionary = state.stats.total_amount if total_produced else state.amount
|
||||
if not total_produced:
|
||||
static func amounts(state: Dictionary, catalog, total_produced: bool = false) -> Dictionary [ String, Variant ]:
|
||||
var source: Dictionary = (state.stats.total_amount if total_produced else state.amount)
|
||||
if (not total_produced):
|
||||
source = source.duplicate()
|
||||
var physical: Dictionary = {}
|
||||
for id in catalog.entities:
|
||||
if not catalog.is_group(id):
|
||||
physical[id] = maxf(0.0, state.amount.get(id, 0.0))
|
||||
for bindings: Dictionary in state.binds.values():
|
||||
if (not catalog.is_group(id)):
|
||||
physical[id] = E.maximum(0, state.amount.get(id, 0))
|
||||
for bindings : Dictionary in state.binds.values():
|
||||
for id in bindings:
|
||||
if physical.has(id):
|
||||
physical[id] += maxf(0.0, bindings[id])
|
||||
var planets: float = 0.0
|
||||
physical[id] = E.add(physical[id], E.maximum(0, bindings[id]))
|
||||
var planets: Variant = 0
|
||||
for id in physical:
|
||||
if catalog.entities[id].group_as == "Planet":
|
||||
if E.eq(catalog.entities[id].group_as, "Planet"):
|
||||
source[id] = physical[id]
|
||||
planets += physical[id]
|
||||
planets = E.add(planets, physical[id])
|
||||
source["Planet"] = planets
|
||||
var result: Dictionary[String, float] = {}
|
||||
var result: Dictionary [ String , Variant ] = {}
|
||||
for id in source:
|
||||
result[id] = 0.0
|
||||
result[id] = 0
|
||||
for id in source:
|
||||
# group_as counters are already sums and must not be counted a second time.
|
||||
if catalog.is_group(id):
|
||||
continue
|
||||
var item_id: String = id
|
||||
if catalog.entities.has(id) and catalog.entities[id].count_as != "":
|
||||
if (catalog.entities.has(id) and E.ne(catalog.entities[id].count_as, "")):
|
||||
item_id = catalog.entities[id].count_as
|
||||
# Detailed icons need their own count as well as the summary alias.
|
||||
# Aggregate only from source, never from these presentation entries.
|
||||
if item_id != id:
|
||||
result[id] = result.get(id, 0.0) + float(source[id])
|
||||
result[item_id] = result.get(item_id, 0.0) + float(source[id])
|
||||
if E.ne(item_id, id):
|
||||
result[id] = E.add(result.get(id, 0), E.of(source[id]))
|
||||
result[item_id] = E.add(result.get(item_id, 0), E.of(source[id]))
|
||||
for id in source:
|
||||
if catalog.is_group(id):
|
||||
result[id] = float(source[id])
|
||||
result[id] = E.of(source[id])
|
||||
return result
|
||||
|
||||
|
||||
@@ -63,12 +64,12 @@ static func unlocks(state: Dictionary, catalog, locked: Array) -> Dictionary:
|
||||
var entity = catalog.entities[id]
|
||||
var eligible: bool = true
|
||||
for requirement in entity.unlock:
|
||||
if maxf(state.stats.total_amount.get(requirement, 0.0), display_totals.get(requirement, 0.0)) < entity.unlock[requirement]:
|
||||
if E.lt(E.maximum(state.stats.total_amount.get(requirement, 0), display_totals.get(requirement, 0)), entity.unlock[requirement]):
|
||||
eligible = false
|
||||
break
|
||||
if eligible:
|
||||
for item_id in [id, entity.count_as]:
|
||||
if item_id != "" and not visible.has(item_id):
|
||||
if (E.ne(item_id, "") and (not visible.has(item_id))):
|
||||
visible.append(item_id)
|
||||
else:
|
||||
remaining.append(id)
|
||||
|
||||
@@ -1,58 +1,56 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.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:
|
||||
func life_products(target: Dictionary, entity: OwniverseEntity, count: Variant) -> 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"):
|
||||
var probabilities: Dictionary = {"progress": E.multiply(E.multiply(E.of(rule.progress), modifiers.progress), WorldParameters.value(target, "Enlightenment")), "degradation": E.multiply(E.of(rule.degradation), modifiers.degradation)}
|
||||
var probability_sum: Variant = E.maximum(1, E.add(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)
|
||||
var key: String = E.add(E.add(entity.id, ":"), channel)
|
||||
var expected: Variant = E.add(E.divide(E.multiply(count, probabilities[channel]), probability_sum), target.life_remainders.get(key, 0))
|
||||
target.life_remainders[key] = expected
|
||||
pending[channel] = floorf(snappedf(expected, 1.0e-12))
|
||||
pending[channel] = E.floor_value(expected)
|
||||
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}
|
||||
var key: String = E.add(E.add(entity.id, ":"), channel)
|
||||
target.life_remainders[key] = E.maximum(0, E.subtract(target.life_remainders[key], assigned[channel]))
|
||||
var result: Dictionary = {entity.id: E.subtract(E.subtract(count, assigned.progress), assigned.degradation)}
|
||||
result[rule.next] = assigned.progress
|
||||
var weights: Dictionary = {}
|
||||
var weight_sum: float = 0.0
|
||||
var weight_sum: Variant = 0
|
||||
for term in str(rule.fall).split("^"):
|
||||
var pair: PackedStringArray = term.split(":")
|
||||
weights[pair[0]] = float(pair[1])
|
||||
weight_sum += float(pair[1])
|
||||
weights[pair[0]] = E.of(pair[1])
|
||||
weight_sum = E.add(weight_sum, E.of(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)
|
||||
var key: String = E.add(E.add(entity.id, ":fall:"), id)
|
||||
var expected: Variant = E.add(E.divide(E.multiply(assigned.degradation, weights[id]), weight_sum), target.life_remainders.get(key, 0))
|
||||
target.life_remainders[key] = expected
|
||||
requests[id] = maxf(0.0, floorf(snappedf(expected, 1.0e-12)))
|
||||
requests[id] = E.maximum(0, E.floor_value(expected))
|
||||
var falls: Dictionary = QuantityMath.allocate_whole(requests, assigned.degradation)
|
||||
var distributed: float = 0.0
|
||||
var distributed: Variant = 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]
|
||||
distributed = E.add(distributed, falls[id])
|
||||
result[id] = E.add(result.get(id, 0), falls[id])
|
||||
var key: String = E.add(E.add(entity.id, ":fall:"), id)
|
||||
target.life_remainders[key] = E.subtract(target.life_remainders[key], falls[id])
|
||||
# Fill sub-unit fall quotas deterministically, retaining signed allocation errors.
|
||||
while distributed < assigned.degradation:
|
||||
while E.lt(distributed, assigned.degradation):
|
||||
var selected: String = ""
|
||||
var largest: float = -INF
|
||||
var largest: Variant = E.neg(INF)
|
||||
for id in weights:
|
||||
var rest: float = target.life_remainders[entity.id + ":fall:" + id]
|
||||
if rest > largest:
|
||||
var rest: Variant = target.life_remainders[E.add(E.add(entity.id, ":fall:"), id)]
|
||||
if E.gt(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
|
||||
result[selected] = E.add(result.get(selected, 0), 1)
|
||||
target.life_remainders[E.add(E.add(entity.id, ":fall:"), selected)] = E.subtract(target.life_remainders[E.add(E.add(entity.id, ":fall:"), selected)], 1)
|
||||
distributed = E.add(distributed, 1)
|
||||
return result
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.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):
|
||||
func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant, automatic: bool = false) -> bool:
|
||||
if (E.le(amount, 0) or E.ne(amount, E.floor_value(amount))):
|
||||
return false
|
||||
var trial: Dictionary = target.duplicate()
|
||||
trial.amount = target.amount.duplicate()
|
||||
@@ -13,26 +13,26 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: float,
|
||||
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:
|
||||
var cost: Variant = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount))
|
||||
if (automatic and E.lt(trial.auto_resources.get(resource, 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
|
||||
var total: Variant = 0
|
||||
for quantity in taken.values():
|
||||
total += quantity
|
||||
if total != cost:
|
||||
total = E.add(total, quantity)
|
||||
if E.ne(total, cost):
|
||||
return false
|
||||
if automatic:
|
||||
trial.auto_resources[resource] = maxf(0.0, trial.auto_resources.get(resource, 0.0) - cost)
|
||||
trial.auto_resources[resource] = E.maximum(0, E.subtract(trial.auto_resources.get(resource, 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):
|
||||
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
|
||||
var factor: Variant = (E.subtract(1, E.divide(taken[id], before[id])) if E.gt(before.get(id, 0), 0) else 0)
|
||||
world.batches.scale_batches(trial, id, factor)
|
||||
if not world.ledger.bind_new_owners(trial, {entity.id: amount}, true):
|
||||
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)
|
||||
@@ -46,72 +46,71 @@ 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
|
||||
if (E.eq(entity.count_as, "BH") and E.gt(entity.mass, 0)):
|
||||
masses[id] = E.multiply(E.floor_value(E.maximum(0, world.state.amount.get(id, 0))), entity.mass)
|
||||
return masses
|
||||
|
||||
|
||||
func smbh_available() -> float:
|
||||
var mass: float = 0.0
|
||||
func smbh_available() -> Variant:
|
||||
var mass: Variant = 0
|
||||
for value in smbh_sources().values():
|
||||
mass += value
|
||||
return floorf(mass)
|
||||
mass = E.add(mass, value)
|
||||
return E.floor_value(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):
|
||||
func plan_smbh(desired_amount: Variant) -> Dictionary:
|
||||
var empty: Dictionary = {"amount": 0, "taken": {}}
|
||||
if (E.le(desired_amount, 0) or (not E.finite(desired_amount))):
|
||||
return empty
|
||||
var total_mass: float = smbh_available()
|
||||
if total_mass <= 0.0:
|
||||
var total_mass: Variant = smbh_available()
|
||||
if E.le(total_mass, 0):
|
||||
return empty
|
||||
var requested: float = minf(floorf(desired_amount), total_mass)
|
||||
if requested <= 0.0:
|
||||
var requested: Variant = E.minimum(E.floor_value(desired_amount), total_mass)
|
||||
if E.le(requested, 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
|
||||
var consumed_mass: Variant = 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))
|
||||
var expected: Variant = E.divide(E.multiply(requested, distribution[id]), world.catalog.entities[id].mass)
|
||||
taken[id] = E.minimum(E.floor_value(expected), E.floor_value(world.state.amount[id]))
|
||||
consumed_mass = E.add(consumed_mass, E.multiply(taken[id], world.catalog.entities[id].mass))
|
||||
order.append({"id": id, "remainder": E.subtract(expected, taken[id])})
|
||||
order.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.remainder, b.remainder) if E.ne(a.remainder, b.remainder) else E.lt(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:
|
||||
for candidate : Dictionary in order:
|
||||
if E.ge(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):
|
||||
if E.lt(taken[id], E.floor_value(world.state.amount[id])):
|
||||
taken[id] = E.add(taken[id], 1)
|
||||
consumed_mass = E.add(consumed_mass, world.catalog.entities[id].mass)
|
||||
if (E.le(consumed_mass, 0) or E.ne(consumed_mass, E.floor_value(consumed_mass))):
|
||||
return empty
|
||||
return {"amount": consumed_mass, "taken": taken}
|
||||
|
||||
|
||||
func produce_smbh(desired_amount: float) -> float:
|
||||
func produce_smbh(desired_amount: Variant) -> Variant:
|
||||
var plan: Dictionary = plan_smbh(desired_amount)
|
||||
var consumed_mass: float = plan.amount
|
||||
var consumed_mass: Variant = plan.amount
|
||||
var taken: Dictionary = plan.taken
|
||||
if consumed_mass <= 0.0:
|
||||
return 0.0
|
||||
if E.le(consumed_mass, 0):
|
||||
return 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:
|
||||
if E.le(taken[id], 0):
|
||||
continue
|
||||
next.amount[id] -= taken[id]
|
||||
var fraction: float = taken[id] / physical[id]
|
||||
world.batches.scale_batches(next, id, 1.0 - fraction)
|
||||
next.amount[id] = E.subtract(next.amount[id], taken[id])
|
||||
var fraction: Variant = E.divide(taken[id], physical[id])
|
||||
world.batches.scale_batches(next, id, E.subtract(1, 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
|
||||
var released: Variant = E.multiply(next.binds[id][resource], fraction)
|
||||
next.binds[id][resource] = E.subtract(next.binds[id][resource], released)
|
||||
next.amount[resource] = E.add(next.amount.get(resource, 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)
|
||||
@@ -122,29 +121,29 @@ func produce_smbh(desired_amount: float) -> float:
|
||||
return consumed_mass
|
||||
|
||||
|
||||
func produce(item_id: String, desired_amount: float) -> float:
|
||||
if item_id == "SMBH":
|
||||
func produce(item_id: String, desired_amount: Variant) -> Variant:
|
||||
if E.eq(item_id, "SMBH"):
|
||||
return produce_smbh(desired_amount)
|
||||
# Use special production for S and H
|
||||
if item_id == "S" or item_id == "H":
|
||||
if (E.eq(item_id, "S") or E.eq(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))
|
||||
|
||||
if E.le(desired_amount, 0):
|
||||
return 0
|
||||
|
||||
var max_producible = E.floor_value(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
|
||||
|
||||
|
||||
if E.le(max_producible, 0):
|
||||
return 0
|
||||
|
||||
var metallicity: Variant = world.environment.stellar_environment(world.state).metallicity
|
||||
if (not try_production(world.state, entity, max_producible)):
|
||||
return 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)
|
||||
@@ -154,49 +153,49 @@ func produce(item_id: String, desired_amount: float) -> float:
|
||||
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
|
||||
func produce_hs(entity_id: String, value: Variant) -> Variant:
|
||||
if E.le(value, 0):
|
||||
return 0
|
||||
|
||||
var to_produce: Variant = E.floor_value(value)
|
||||
to_produce = E.minimum(to_produce, E.floor_value(world.state.energy))
|
||||
|
||||
if E.le(to_produce, 0):
|
||||
return 0
|
||||
|
||||
world.state.energy = E.subtract(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 production_step(entity: OwniverseEntity) -> Variant:
|
||||
return QuantityMath.production_step(E.add(entity.cost.values(), entity.bind.values()))
|
||||
|
||||
|
||||
func calculate_max_producible(entity: OwniverseEntity, desired_amount: float = 1.0E150) -> float:
|
||||
if entity.id == "SMBH":
|
||||
func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant = 1.0E150) -> Variant:
|
||||
if E.eq(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])
|
||||
requirements[resource] = E.maximum(0, entity.cost[resource])
|
||||
for resource in entity.bind:
|
||||
requirements[resource] = requirements.get(resource, 0.0) + maxf(0.0, entity.bind[resource])
|
||||
requirements[resource] = E.add(requirements.get(resource, 0), E.maximum(0, entity.bind[resource]))
|
||||
var has_requirements: bool = false
|
||||
for quantity in requirements.values():
|
||||
if quantity > 0.0:
|
||||
if E.gt(quantity, 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)
|
||||
if (not has_requirements):
|
||||
return 0
|
||||
var available: Variant = E.maximum(0, desired_amount)
|
||||
for resource in requirements:
|
||||
if requirements[resource] > 0.0:
|
||||
available = minf(available, floorf(world.ledger.available(world.state, resource) / requirements[resource]))
|
||||
if E.gt(requirements[resource], 0):
|
||||
available = E.minimum(available, E.floor_value(E.divide(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
|
||||
if E.gt(concrete[resource], 0):
|
||||
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), concrete[resource])))
|
||||
var step: Variant = production_step(entity)
|
||||
return (E.multiply(E.floor_value(E.divide(available, step)), step) if E.finite(step) else 0)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Числа симуляции
|
||||
|
||||
Количество объектов, запасы, энергия, расходы, привязки и статистика считаются
|
||||
целочисленно, без перехода через `float`. Значения до `10^27` сохраняют каждую
|
||||
единицу; промежуточные произведения могут превышать этот диапазон.
|
||||
|
||||
`big_integer.gd` хранит модуль числа массивом разрядов по основанию `10^9`,
|
||||
младший разряд первым. Переносы при умножении не переполняют `int64`.
|
||||
Деление использует нормализованную оценку цифры частного.
|
||||
|
||||
`exact.gd` — единый API арифметики. Малые значения остаются обычными `int`,
|
||||
большие числа и дроби представлены `exact_value.gd`: знак, целый числитель,
|
||||
целый знаменатель. Дроби сокращаются. Операции создают новые значения;
|
||||
поля `ExactValue` и массивы его разрядов нельзя изменять после создания.
|
||||
|
||||
```gdscript
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
var stock = E.of("1e27") # Строка, не литерал float 1e27!
|
||||
stock = E.subtract(stock, 1) # 999999999999999999999999999
|
||||
var price = E.of("0.125") # Точно 1/8
|
||||
var cost = E.multiply(price, 8) # Точно 1
|
||||
if E.ge(stock, cost):
|
||||
stock = E.subtract(stock, cost)
|
||||
```
|
||||
|
||||
Использовать `E.add/subtract/multiply/divide`, `E.eq/lt/le/gt/ge`,
|
||||
`E.floor_value/ceil_value/round_value`. Перегрузки операторов в GDScript нет:
|
||||
обычные `+`, `==`, `min`, `float(...)` для чисел симуляции не подходят.
|
||||
Сигнатуры принимают `Variant`, а не `float`. `E.integer` допускается только
|
||||
для значений в диапазоне `int64`, например индексов и состояния генератора.
|
||||
Для поиска чисел в массиве используется `E.contains`, поскольку сравнение
|
||||
объектов по ссылке не является сравнением числовых значений.
|
||||
|
||||
## Границы точности
|
||||
|
||||
- Десятичные параметры CSV разбираются из строк точно. Стоимости, проверка
|
||||
доступности и размер целой производственной партии не округляются.
|
||||
- Суммы распределений и переносы целых объектов сохраняются точно.
|
||||
- Промежуточная рациональная арифметика точна. Перед публикацией следующего
|
||||
состояния знаменатели сохранённых дробей ограничиваются `10^36`.
|
||||
Если знаменатель больше, значение усекается к нулю с ошибкой меньше
|
||||
`10^-36`. Целые числа эта операция не меняет. Это предотвращает рост
|
||||
знаменателей при многократном накоплении меняющихся долей. Такая точность
|
||||
относится к каждой операции ограничения, а не является обещанием нулевой
|
||||
суммарной ошибки при бесконечном числе циклов.
|
||||
- Логарифмы, корни, нецелые степени и нормальная случайная выборка остаются
|
||||
приближёнными. Результаты трансцендентных функций переводятся обратно в
|
||||
рациональные коэффициенты с 12 знаками после точки. Статистическая выборка
|
||||
не обеспечивает равномерный выбор каждого отдельного целого около `10^27`;
|
||||
последующий учёт выбранного количества и остатка точен.
|
||||
- Возраст хранится в целых тиках (`10^9` тиков на год). Малый остаток кадрового
|
||||
времени остаётся `float` и переносится между кадрами; большой возраст
|
||||
никогда не складывается с малым приращением через `float`.
|
||||
- Интерфейс преобразует в `float` только координаты индикаторов. Текст чисел,
|
||||
доступное количество и значения кнопок формируются из точных чисел.
|
||||
|
||||
`E.of(float)` сохраняет фактическое двоичное значение входного числа: он
|
||||
не может восстановить единицы, уже потерянные предыдущими вычислениями.
|
||||
|
||||
## Сохранение
|
||||
|
||||
Формат сохранения — **3**, версия состояния — **0.2.0**. На главном потоке
|
||||
`E.encode` строит независимый снимок, затем существующий фоновый писатель
|
||||
сохраняет его. Большие целые и дроби кодируются как `{"$exact": "число"}`;
|
||||
для дробей используется строка `числитель/знаменатель`. Целые за пределами
|
||||
точного диапазона JSON/float тоже кодируются строкой. Зашифрованный файл и
|
||||
диагностический JSON используют один снимок. После проверки `E.decode`
|
||||
восстанавливает числа, без загрузки сериализованных объектов GDScript.
|
||||
|
||||
Миграции старой арифметики нет. Старый формат несовместим: существующий
|
||||
механизм загрузки сохраняет его в `.incompatible.bak` и начинает новую игру.
|
||||
Старое сохранение игрока не используется тестами и не изменялось при разработке.
|
||||
|
||||
## Проверки
|
||||
|
||||
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
|
||||
|
||||
Независимый эталон `tests/fixtures/exact_oracle.json` получен через Python
|
||||
`fractions.Fraction` (120 пар, seed 4321). Тест сравнивает строковые результаты
|
||||
суммы, разности, произведения, деления и округления вниз. Дополнительно
|
||||
проверяются границы `int64`, JSON, рецепты, распределения, жизнь при `10^27`,
|
||||
единичный расход энергии, тик времени и 100 циклов с меняющимися долями звёзд.
|
||||
@@ -0,0 +1,102 @@
|
||||
extends RefCounted
|
||||
## Unsigned little-endian base 10^9 arithmetic. Inputs are never mutated.
|
||||
const BASE: int = 1000000000
|
||||
|
||||
static func trim(a: Array) -> Array:
|
||||
while a.size() > 1 and a.back() == 0: a.pop_back()
|
||||
return a
|
||||
|
||||
static func parse(text: String) -> Array:
|
||||
var result: Array = []
|
||||
var end: int = text.length()
|
||||
while end > 0:
|
||||
var start: int = maxi(0, end - 9)
|
||||
result.append(int(text.substr(start, end - start)))
|
||||
end = start
|
||||
return trim(result if not result.is_empty() else [0])
|
||||
|
||||
static func decimal(a: Array) -> String:
|
||||
var result: String = str(a.back())
|
||||
for i in range(a.size() - 2, -1, -1): result += "%09d" % a[i]
|
||||
return result
|
||||
|
||||
static func compare(a: Array, b: Array) -> int:
|
||||
if a.size() != b.size(): return 1 if a.size() > b.size() else -1
|
||||
for i in range(a.size() - 1, -1, -1):
|
||||
if a[i] != b[i]: return 1 if a[i] > b[i] else -1
|
||||
return 0
|
||||
|
||||
static func add(a: Array, b: Array) -> Array:
|
||||
var result: Array = []
|
||||
var carry: int = 0
|
||||
for i in maxi(a.size(), b.size()):
|
||||
var value: int = carry + (a[i] if i < a.size() else 0) + (b[i] if i < b.size() else 0)
|
||||
result.append(value % BASE)
|
||||
carry = value / BASE
|
||||
if carry: result.append(carry)
|
||||
return result
|
||||
|
||||
static func subtract(a: Array, b: Array) -> Array:
|
||||
assert(compare(a, b) >= 0, "Unsigned subtraction underflow")
|
||||
var result: Array = []
|
||||
var borrow: int = 0
|
||||
for i in a.size():
|
||||
var value: int = a[i] - borrow - (b[i] if i < b.size() else 0)
|
||||
borrow = 1 if value < 0 else 0
|
||||
result.append(value + BASE if borrow else value)
|
||||
return trim(result)
|
||||
|
||||
static func multiply(a: Array, b: Array) -> Array:
|
||||
var result: Array = []
|
||||
result.resize(a.size() + b.size())
|
||||
result.fill(0)
|
||||
for i in a.size():
|
||||
var carry: int = 0
|
||||
for j in b.size():
|
||||
# < BASE^2 + 2*BASE: comfortably inside signed int64.
|
||||
var value: int = a[i] * b[j] + result[i + j] + carry
|
||||
result[i + j] = value % BASE
|
||||
carry = value / BASE
|
||||
result[i + b.size()] = carry
|
||||
return trim(result)
|
||||
|
||||
static func divmod(a: Array, b: Array) -> Array:
|
||||
assert(b != [0], "Division by zero")
|
||||
if compare(a, b) < 0: return [[0], a.duplicate()]
|
||||
if b.size() == 1:
|
||||
var quotient: Array = []
|
||||
quotient.resize(a.size())
|
||||
var carry: int = 0
|
||||
for i in range(a.size() - 1, -1, -1):
|
||||
var value: int = carry * BASE + a[i]
|
||||
quotient[i] = value / b[0]
|
||||
carry = value % b[0]
|
||||
return [trim(quotient), [carry]]
|
||||
# Normalize the leading divisor limb so the two-limb quotient estimate
|
||||
# needs at most two downward corrections (Knuth's division estimate).
|
||||
var factor: int = BASE / (b.back() + 1)
|
||||
a = multiply(a, [factor])
|
||||
b = multiply(b, [factor])
|
||||
var quotient: Array = []
|
||||
quotient.resize(a.size())
|
||||
var remainder: Array = [0]
|
||||
for i in range(a.size() - 1, -1, -1):
|
||||
remainder.push_front(a[i])
|
||||
trim(remainder)
|
||||
var high: int = remainder[b.size()] if remainder.size() > b.size() else 0
|
||||
var low: int = remainder[b.size() - 1] if remainder.size() >= b.size() else 0
|
||||
var estimate: int = mini(BASE - 1, (high * BASE + low) / b.back())
|
||||
var product: Array = multiply(b, [estimate])
|
||||
while compare(product, remainder) > 0:
|
||||
estimate -= 1
|
||||
product = subtract(product, b)
|
||||
quotient[i] = estimate
|
||||
remainder = subtract(remainder, product)
|
||||
return [trim(quotient), divmod(remainder, [factor])[0]]
|
||||
|
||||
static func gcd(a: Array, b: Array) -> Array:
|
||||
while b != [0]:
|
||||
var remainder: Array = divmod(a, b)[1]
|
||||
a = b
|
||||
b = remainder
|
||||
return a
|
||||
@@ -0,0 +1 @@
|
||||
uid://6wfbh3vq5smd
|
||||
@@ -0,0 +1,322 @@
|
||||
extends RefCounted
|
||||
## Exact signed rational arithmetic. Native small integers are the fast path.
|
||||
const Big = preload("res://world/owniverse/math/big_integer.gd")
|
||||
const Value = preload("res://world/owniverse/math/exact_value.gd")
|
||||
const SMALL: int = 1000000000
|
||||
const MAX_INT: int = 9223372036854775807
|
||||
const MIN_INT: int = -9223372036854775807 - 1
|
||||
const CARRY_SCALE: Array = [0, 0, 0, 0, 1] # 10^36
|
||||
|
||||
static func make(n: Array, d: Array = [1], polarity: int = 1):
|
||||
assert(d != [0], "Zero denominator")
|
||||
if n == [0]: return 0
|
||||
if d != [1]:
|
||||
var common: Array = Big.gcd(n, d)
|
||||
n = Big.divmod(n, common)[0]
|
||||
d = Big.divmod(d, common)[0]
|
||||
if d == [1] and (n.size() == 1 or (n.size() == 2 and n[1] < SMALL)):
|
||||
return polarity * (n[0] + (n[1] * SMALL if n.size() == 2 else 0))
|
||||
if d == [1] and n.size() == 3:
|
||||
var limit: Array = [854775807 if polarity > 0 else 854775808, 223372036, 9]
|
||||
if Big.compare(n, limit) <= 0:
|
||||
return (("-" if polarity < 0 else "") + Big.decimal(n)).to_int()
|
||||
var result = Value.new()
|
||||
result.numerator = n
|
||||
result.denominator = d
|
||||
result.polarity = polarity
|
||||
return result
|
||||
|
||||
static func of(value):
|
||||
if value is Value or value is int: return value
|
||||
if value is float:
|
||||
if not is_finite(value): return value
|
||||
if value == floorf(value) and absf(value) < 1e18: return int(value)
|
||||
# Imports the actual binary float, not an invented decimal rounding.
|
||||
var data := PackedByteArray()
|
||||
data.resize(8)
|
||||
data.encode_double(0, value)
|
||||
var bits: int = data.decode_s64(0)
|
||||
var exponent: int = (bits >> 52) & 2047
|
||||
var mantissa: int = bits & 4503599627370495
|
||||
if exponent: mantissa += 4503599627370496
|
||||
var power: int = exponent - 1075 if exponent else -1074
|
||||
var n: Array = Big.parse(str(mantissa))
|
||||
var d: Array = [1]
|
||||
for i in absi(power):
|
||||
if power > 0: n = Big.multiply(n, [2])
|
||||
else: d = Big.multiply(d, [2])
|
||||
return make(n, d, -1 if value < 0 else 1)
|
||||
var text: String = str(value).strip_edges().to_lower()
|
||||
if text == "": return 0
|
||||
if "/" in text:
|
||||
var terms = text.split("/")
|
||||
assert(terms.size() == 2)
|
||||
return divide(of(terms[0]), of(terms[1]))
|
||||
var polarity: int = -1 if text.begins_with("-") else 1
|
||||
text = text.trim_prefix("-").trim_prefix("+")
|
||||
var terms = text.split("e")
|
||||
var power: int = int(terms[1]) if terms.size() == 2 else 0
|
||||
text = terms[0]
|
||||
var point: int = text.find(".")
|
||||
if point >= 0: power -= text.length() - point - 1
|
||||
text = text.replace(".", "").replace("_", "")
|
||||
assert(text.is_valid_int(), "Invalid exact number: " + str(value))
|
||||
assert(absi(power) <= 10000, "Unreasonable decimal exponent")
|
||||
return make(Big.parse(text + "0".repeat(maxi(0, power))), Big.parse("1" + "0".repeat(maxi(0, -power))), polarity)
|
||||
|
||||
static func parts(value) -> Array:
|
||||
value = of(value)
|
||||
if value is int: return [Big.parse(str(value).trim_prefix("-")), [1], -1 if value < 0 else 1]
|
||||
return [value.numerator, value.denominator, value.polarity]
|
||||
|
||||
static func numeric(value) -> bool:
|
||||
return value is int or value is float or value is Value
|
||||
|
||||
static func finite(value) -> bool:
|
||||
return value is Value or value is int or (value is float and is_finite(value))
|
||||
|
||||
static func approx(value) -> float:
|
||||
if not value is Value: return float(value)
|
||||
var a: String = Big.decimal(value.numerator)
|
||||
var b: String = Big.decimal(value.denominator)
|
||||
return value.polarity * float(a.left(16)) / float(b.left(16)) * pow(10.0, maxi(0, a.length() - 16) - maxi(0, b.length() - 16))
|
||||
|
||||
static func compare(a, b) -> int:
|
||||
if a is int and b is int: return 0 if a == b else (1 if a > b else -1)
|
||||
if not finite(a) or not finite(b):
|
||||
var x: float = approx(a)
|
||||
var y: float = approx(b)
|
||||
return 0 if x == y else (1 if x > y else -1)
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
if x[2] != y[2]: return 1 if x[2] > y[2] else -1
|
||||
return x[2] * Big.compare(Big.multiply(x[0], y[1]), Big.multiply(y[0], x[1]))
|
||||
|
||||
static func eq(a, b) -> bool:
|
||||
if a == null: return b == null
|
||||
if b == null: return false
|
||||
if numeric(a) and numeric(b):
|
||||
if (a is float and is_nan(a)) or (b is float and is_nan(b)): return false
|
||||
return compare(a, b) == 0
|
||||
if a is Dictionary and b is Dictionary:
|
||||
if a.size() != b.size(): return false
|
||||
for k in a:
|
||||
if not b.has(k) or not eq(a[k], b[k]): return false
|
||||
return true
|
||||
if a is Array and b is Array:
|
||||
if a.size() != b.size(): return false
|
||||
for i in a.size():
|
||||
if not eq(a[i], b[i]): return false
|
||||
return true
|
||||
if (a is String or a is StringName) and (b is String or b is StringName):
|
||||
return str(a) == str(b)
|
||||
if typeof(a) != typeof(b): return false
|
||||
return a == b
|
||||
|
||||
static func ne(a, b) -> bool: return not eq(a, b)
|
||||
static func lt(a, b) -> bool: return compare(a, b) < 0 if numeric(a) and numeric(b) else a < b
|
||||
static func le(a, b) -> bool: return compare(a, b) <= 0 if numeric(a) and numeric(b) else a <= b
|
||||
static func gt(a, b) -> bool: return compare(a, b) > 0 if numeric(a) and numeric(b) else a > b
|
||||
static func ge(a, b) -> bool: return compare(a, b) >= 0 if numeric(a) and numeric(b) else a >= b
|
||||
static func minimum(a, b): return a if le(a, b) else b
|
||||
static func maximum(a, b): return a if ge(a, b) else b
|
||||
static func clamp_value(a, low, high): return minimum(maximum(a, low), high)
|
||||
static func neg(a): return subtract(0, a)
|
||||
static func absolute(a): return neg(a) if lt(a, 0) else a
|
||||
|
||||
static func add(a, b):
|
||||
if not numeric(a) or not numeric(b): return a + b
|
||||
if a is int and b is int:
|
||||
if (b >= 0 and a <= MAX_INT - b) or (b < 0 and a >= MIN_INT - b): return a + b
|
||||
if not finite(a) or not finite(b): return approx(a) + approx(b)
|
||||
if a is int and a == 0: return of(b)
|
||||
if b is int and b == 0: return of(a)
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
var common: Array = Big.gcd(x[1], y[1])
|
||||
var xd: Array = Big.divmod(x[1], common)[0]
|
||||
var yd: Array = Big.divmod(y[1], common)[0]
|
||||
var left: Array = Big.multiply(x[0], yd)
|
||||
var right: Array = Big.multiply(y[0], xd)
|
||||
var denominator: Array = Big.multiply(xd, y[1])
|
||||
if x[2] == y[2]: return make(Big.add(left, right), denominator, x[2])
|
||||
var order: int = Big.compare(left, right)
|
||||
return make(Big.subtract(left, right) if order >= 0 else Big.subtract(right, left), denominator, x[2] if order >= 0 else y[2])
|
||||
|
||||
static func subtract(a, b):
|
||||
if not numeric(a) or not numeric(b): return a - b
|
||||
if a is int and b is int:
|
||||
if (b >= 0 and a >= MIN_INT + b) or (b < 0 and a <= MAX_INT + b): return a - b
|
||||
if not finite(a) or not finite(b): return approx(a) - approx(b)
|
||||
if b is int and b == 0: return of(a)
|
||||
var y: Array = parts(b)
|
||||
return add(a, make(y[0], y[1], -y[2]))
|
||||
|
||||
static func multiply(a, b):
|
||||
if not numeric(a) or not numeric(b): return a * b
|
||||
if not finite(a) or not finite(b): return approx(a) * approx(b)
|
||||
if (a is int and a == 0) or (b is int and b == 0): return 0
|
||||
if a is int and a == 1: return of(b)
|
||||
if b is int and b == 1: return of(a)
|
||||
if a is int and b is int and a != MIN_INT and b != MIN_INT:
|
||||
if absi(a) <= MAX_INT / absi(b): return a * b
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
var g1: Array = Big.gcd(x[0], y[1])
|
||||
var g2: Array = Big.gcd(y[0], x[1])
|
||||
return make(Big.multiply(Big.divmod(x[0], g1)[0], Big.divmod(y[0], g2)[0]), Big.multiply(Big.divmod(x[1], g2)[0], Big.divmod(y[1], g1)[0]), x[2] * y[2])
|
||||
|
||||
static func divide(a, b):
|
||||
if not numeric(a) or not numeric(b): return a / b
|
||||
if not finite(a) or not finite(b): return approx(a) / approx(b)
|
||||
if b is int and b == 1: return of(a)
|
||||
if a is int and b is int and b != 0 and not (a == MIN_INT and b == -1):
|
||||
if a % b == 0: return a / b
|
||||
var y: Array = parts(b)
|
||||
assert(y[0] != [0], "Exact division by zero")
|
||||
return multiply(a, make(y[1], y[0], y[2]))
|
||||
|
||||
static func floor_value(a):
|
||||
if not finite(a): return a
|
||||
if a is int: return a
|
||||
var x: Array = parts(a)
|
||||
var qr: Array = Big.divmod(x[0], x[1])
|
||||
if x[2] < 0 and qr[1] != [0]: qr[0] = Big.add(qr[0], [1])
|
||||
return make(qr[0], [1], x[2])
|
||||
|
||||
static func ceil_value(a): return neg(floor_value(neg(a)))
|
||||
static func round_value(a): return multiply(-1 if lt(a, 0) else 1, floor_value(add(absolute(a), of("0.5"))))
|
||||
static func remainder(a, b): return subtract(a, multiply(truncate(divide(a, b)), b))
|
||||
static func truncate(a): return ceil_value(a) if lt(a, 0) else floor_value(a)
|
||||
static func integer(a) -> int:
|
||||
var result = truncate(of(a))
|
||||
assert(result is int, "Value does not fit native int64")
|
||||
return result
|
||||
|
||||
## Bound persistent fractions, never integer quantities or recipe arithmetic.
|
||||
## Truncation towards zero changes a value by less than 10^-36.
|
||||
static func bounded(value):
|
||||
if not value is Value or Big.compare(value.denominator, CARRY_SCALE) <= 0:
|
||||
return value
|
||||
var units: Array = Big.divmod(Big.multiply(value.numerator, CARRY_SCALE), value.denominator)[0]
|
||||
return make(units, CARRY_SCALE, value.polarity)
|
||||
|
||||
## Only call on a newly owned state snapshot, never on a published old state.
|
||||
static func bound_state(value):
|
||||
if value is Value: return bounded(value)
|
||||
if value is Dictionary:
|
||||
for key in value: value[key] = bound_state(value[key])
|
||||
elif value is Array:
|
||||
for i in value.size(): value[i] = bound_state(value[i])
|
||||
return value
|
||||
|
||||
static func power(a, b):
|
||||
if finite(b) and eq(b, floor_value(b)) and le(absolute(b), 10000):
|
||||
var exponent: int = integer(absolute(b))
|
||||
var result = 1
|
||||
while exponent:
|
||||
if exponent % 2: result = multiply(result, a)
|
||||
exponent /= 2
|
||||
if exponent: a = multiply(a, a)
|
||||
return divide(1, result) if lt(b, 0) else result
|
||||
return coefficient(pow(approx(a), approx(b)))
|
||||
|
||||
static func coefficient(value: float):
|
||||
# Only transcendental/RNG boundaries are approximate: 12 decimal places.
|
||||
if not is_finite(value): return value
|
||||
return of(String.num(value, 12))
|
||||
|
||||
static func square_root(a): return coefficient(sqrt(approx(a)))
|
||||
static func logarithm(a): return coefficient(log(approx(a)))
|
||||
static func near(a, b) -> bool:
|
||||
return le(absolute(subtract(a, b)), multiply(of("0.00001"), maximum(1, maximum(absolute(a), absolute(b)))))
|
||||
|
||||
static func contains(container, value) -> bool:
|
||||
if container is Array:
|
||||
for item in container:
|
||||
if eq(item, value): return true
|
||||
return false
|
||||
return value in container
|
||||
|
||||
static func fixed(value, digits: int = 0) -> String:
|
||||
var scale = power(10, digits)
|
||||
var rounded = round_value(multiply(value, scale))
|
||||
var text: String = str(absolute(rounded))
|
||||
if digits:
|
||||
text = text.lpad(digits + 1, "0")
|
||||
text = text.left(text.length() - digits) + "." + text.right(digits)
|
||||
return ("-" if lt(rounded, 0) else "") + text
|
||||
|
||||
static func scientific(value, digits: int = 4) -> String:
|
||||
if eq(value, 0): return fixed(0, digits) + "E+0"
|
||||
var terms: Array = parts(absolute(value))
|
||||
var exponent: int = Big.decimal(terms[0]).length() - Big.decimal(terms[1]).length()
|
||||
if lt(absolute(value), power(10, exponent)): exponent -= 1
|
||||
var mantissa = divide(value, power(10, exponent))
|
||||
if ge(absolute(round_value(multiply(mantissa, power(10, digits)))), multiply(10, power(10, digits))):
|
||||
mantissa = divide(mantissa, 10)
|
||||
exponent += 1
|
||||
return fixed(mantissa, digits) + ("E+%d" if exponent >= 0 else "E%d") % exponent
|
||||
|
||||
static func grouped(value, digits: int = 0) -> String:
|
||||
if eq(value, 0): return ""
|
||||
if ge(absolute(value), 1000000000): return scientific(value, digits)
|
||||
var text: String = fixed(absolute(value), digits)
|
||||
var parts_text: PackedStringArray = text.split(".")
|
||||
var integer_text: String = parts_text[0]
|
||||
var result: String = ""
|
||||
while integer_text.length() > 3:
|
||||
result = " " + integer_text.right(3) + result
|
||||
integer_text = integer_text.left(integer_text.length() - 3)
|
||||
return ("-" if lt(value, 0) else "") + integer_text + result + ("." + parts_text[1] if parts_text.size() > 1 else "")
|
||||
|
||||
static func encode(value):
|
||||
if value is Value: return {"$exact": str(value)}
|
||||
if value is int and (value > 9007199254740991 or value < -9007199254740991):
|
||||
return {"$exact": str(value)}
|
||||
if value is Dictionary:
|
||||
var result: Dictionary = {}
|
||||
for k in value: result[k] = encode(value[k])
|
||||
return result
|
||||
if value is Array:
|
||||
var result: Array = []
|
||||
for item in value: result.append(encode(item))
|
||||
return result
|
||||
return value
|
||||
|
||||
static func decode(value):
|
||||
if value is Dictionary:
|
||||
if value.size() == 1 and value.has("$exact"): return of(value["$exact"])
|
||||
var result: Dictionary = {}
|
||||
for k in value: result[k] = decode(value[k])
|
||||
return result
|
||||
if value is Array:
|
||||
var result: Array = []
|
||||
for item in value: result.append(decode(item))
|
||||
return result
|
||||
return value
|
||||
|
||||
static func valid_encoded(value) -> bool:
|
||||
if value is Dictionary:
|
||||
if value.has("$exact"):
|
||||
if value.size() != 1 or not value["$exact"] is String: return false
|
||||
var text: String = value["$exact"]
|
||||
if text.length() > 4096: return false
|
||||
var terms: PackedStringArray = text.split("/")
|
||||
if terms.size() > 2: return false
|
||||
var n: String = terms[0].trim_prefix("-")
|
||||
if n.is_empty(): return false
|
||||
for ch in n:
|
||||
if ch < "0" or ch > "9": return false
|
||||
if terms.size() == 2:
|
||||
if terms[1].is_empty(): return false
|
||||
for ch in terms[1]:
|
||||
if ch < "0" or ch > "9": return false
|
||||
if Big.parse(terms[1]) == [0]: return false
|
||||
return true
|
||||
for k in value:
|
||||
if not valid_encoded(value[k]): return false
|
||||
if value is Array:
|
||||
for item in value:
|
||||
if not valid_encoded(item): return false
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://cfxfoprykygry
|
||||
@@ -0,0 +1,9 @@
|
||||
extends RefCounted
|
||||
## Immutable by contract; arithmetic creates new values. Signed reduced fraction.
|
||||
var polarity: int = 1
|
||||
var numerator: Array = [0]
|
||||
var denominator: Array = [1]
|
||||
|
||||
func _to_string() -> String:
|
||||
const Big = preload("res://world/owniverse/math/big_integer.gd")
|
||||
return ("-" if polarity < 0 else "") + Big.decimal(numerator) + ("/" + Big.decimal(denominator) if denominator != [1] else "")
|
||||
@@ -0,0 +1 @@
|
||||
uid://qv2rdp4s0d36
|
||||
+111
-123
@@ -1,181 +1,169 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.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:
|
||||
func scale_batches(target: Dictionary, id: String, factor: Variant) -> void:
|
||||
var batches: Array = target.batches.get(id, [])
|
||||
var total: float = 0.0
|
||||
var assigned: float = 0.0
|
||||
var total: Variant = 0
|
||||
var assigned: Variant = 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)
|
||||
for batch : Dictionary in batches:
|
||||
var expected: Variant = E.multiply(batch.amount, E.maximum(0, factor))
|
||||
total = E.add(total, expected)
|
||||
batch.amount = E.floor_value(expected)
|
||||
assigned = E.add(assigned, batch.amount)
|
||||
shares.append({"batch": batch, "rest": E.subtract(expected, batch.amount)})
|
||||
shares.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return E.gt(a.rest, b.rest))
|
||||
for share : Dictionary in shares:
|
||||
if E.lt(assigned, E.round_value(total)):
|
||||
share.batch.amount = E.add(share.batch.amount, 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
share.batch.withdraw_remainder = E.minimum(share.batch.get("withdraw_remainder", 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:
|
||||
func append_batch(target: Dictionary, id: String, amount: Variant, created_from: Variant, created_to: Variant, completed: Dictionary = { }, completed_before: Variant = - INF) -> Variant:
|
||||
if ((E.le(amount, 0) or (not world.catalog.entities.has(id))) or E.le(world.catalog.entities[id].lifespan, 0)):
|
||||
return NAN
|
||||
if created_to < created_from:
|
||||
if E.lt(created_to, created_from):
|
||||
return NAN
|
||||
if not target.batches.has(id):
|
||||
if (not target.batches.has(id)):
|
||||
target.batches[id] = []
|
||||
if not target.has("batch_origins"):
|
||||
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:
|
||||
var lifespan: Variant = CivilizationState.effective_span(world, target, id, Global.CivilizationRules)
|
||||
if E.eq(world.catalog.entities[id].group_as, "Star"):
|
||||
lifespan = E.divide(lifespan, WorldParameters.effective(target, "Bur"))
|
||||
var batch_duration: Variant = (E.divide(lifespan, Global.batch_count) if E.gt(lifespan, 10) else lifespan)
|
||||
if E.le(batch_duration, 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
|
||||
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: Variant = target.batch_origins[id]
|
||||
var cursor: Variant = created_from
|
||||
var duration: Variant = E.subtract(created_to, created_from)
|
||||
var remaining_amount: Variant = amount
|
||||
var completed_amount: Variant = 0
|
||||
var completed_from: Variant = INF
|
||||
var completed_to: Variant = E.neg(INF)
|
||||
# Subtract each completed window's lifetime production exactly.
|
||||
var remaining_object_years: Variant = (E.multiply(amount, E.maximum(0, E.subtract(completed_before, E.multiply((E.add(created_from, created_to)), E.of("0.5"))))) if E.finite(completed_before) else NAN)
|
||||
# Advance an exact window index rather than repeatedly finding a boundary.
|
||||
var window_index: Variant = E.floor_value(E.divide((E.subtract(cursor, origin)), batch_duration))
|
||||
if E.le(E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration)), cursor):
|
||||
window_index = E.add(window_index, 1)
|
||||
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)
|
||||
for existing : Dictionary in batches:
|
||||
var start: Variant = existing.get("created_from", E.neg(INF))
|
||||
if (not batches_by_start.has(str(start))):
|
||||
batches_by_start[str(start)] = []
|
||||
batches_by_start[str(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)
|
||||
var window_start: Variant = E.add(origin, E.multiply(window_index, batch_duration))
|
||||
var window_end: Variant = E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration))
|
||||
if E.le(window_end, cursor):
|
||||
push_error("Batch window does not advance: " + 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:
|
||||
var part_end: Variant = E.minimum(created_to, window_end)
|
||||
var part_amount: Variant = remaining_amount
|
||||
if (E.gt(duration, 0) and E.lt(part_end, created_to)):
|
||||
part_amount = E.subtract(E.floor_value(E.divide(E.multiply(amount, (E.subtract(part_end, created_from))), duration)), (E.subtract(amount, remaining_amount)))
|
||||
if E.le(E.add(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)
|
||||
completed_amount = E.add(completed_amount, part_amount)
|
||||
completed_from = E.minimum(completed_from, E.add(window_start, lifespan))
|
||||
completed_to = E.maximum(completed_to, E.add(window_end, lifespan))
|
||||
remaining_object_years = E.subtract(remaining_object_years, E.multiply(part_amount, expired_years(completed_before, E.add(window_start, lifespan), E.add(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:
|
||||
for existing : Dictionary in batches_by_start.get(str(window_start), []):
|
||||
if ((E.eq(existing.get("created_from", E.neg(INF)), window_start) and E.eq(existing.start_year, E.add(window_start, lifespan))) and E.eq(existing.end_year, E.add(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,
|
||||
}
|
||||
batch = {"created_from": window_start, "created_to": window_end, "start_year": E.add(window_start, lifespan), "end_year": E.add(window_end, lifespan), "amount": 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:
|
||||
if (not batches_by_start.has(str(window_start))):
|
||||
batches_by_start[str(window_start)] = []
|
||||
batches_by_start[str(window_start)].append(batch)
|
||||
batch.amount = E.add(batch.amount, part_amount)
|
||||
remaining_amount = E.subtract(remaining_amount, part_amount)
|
||||
if (E.eq(duration, 0) or E.ge(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] = []
|
||||
window_index = E.add(window_index, 1)
|
||||
batches.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return E.lt(a.created_from, b.created_from))
|
||||
if E.gt(completed_amount, 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:
|
||||
func withdraw_batches(target: Dictionary, id: String, to_year: Variant) -> 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:
|
||||
for batch : Dictionary in target.batches.get(id, []):
|
||||
var start: Variant = E.maximum(batch.start_year, batch.get("processed_until", batch.start_year))
|
||||
var end: Variant = batch.end_year
|
||||
var amount: Variant = batch.amount
|
||||
if E.le(amount, 0):
|
||||
continue
|
||||
if to_year < start or (to_year == start and end > start):
|
||||
if (E.lt(to_year, start) or ((E.eq(to_year, start) and E.gt(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)
|
||||
var consumed: Variant = amount
|
||||
var until: Variant = E.minimum(to_year, end)
|
||||
if E.lt(to_year, end):
|
||||
var carry: Variant = batch.get("withdraw_remainder", 0)
|
||||
var expected: Variant = E.add(carry, E.divide(E.multiply((E.subtract(amount, carry)), (E.subtract(to_year, start))), (E.subtract(end, start))))
|
||||
consumed = E.floor_value(expected)
|
||||
batch.withdraw_remainder = E.subtract(expected, consumed)
|
||||
batch.amount = E.maximum(0, E.subtract(amount, consumed))
|
||||
batch.processed_until = to_year
|
||||
remaining.append(batch)
|
||||
if consumed > 0.0:
|
||||
if E.gt(consumed, 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 object_years(source: Dictionary, id: String, quantity: Variant, from_year: Variant, to_year: Variant) -> Variant:
|
||||
var result: Variant = E.multiply(quantity, SimulationTime.elapsed_years(from_year, to_year))
|
||||
for batch : Dictionary in source.batches.get(id, []):
|
||||
var start: Variant = E.maximum(batch.start_year, batch.get("processed_until", batch.start_year))
|
||||
var end: Variant = batch.end_year
|
||||
result = E.subtract(result, E.multiply(batch.amount, (E.subtract(expired_years(to_year, start, end), expired_years(from_year, start, end)))))
|
||||
return E.maximum(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 expired_years(year: Variant, start: Variant, end: Variant) -> Variant:
|
||||
if E.le(year, start):
|
||||
return 0
|
||||
if E.le(end, start):
|
||||
return E.subtract(year, start)
|
||||
if E.ge(year, end):
|
||||
return E.add(E.subtract(year, end), E.multiply((E.subtract(end, start)), E.of("0.5")))
|
||||
return E.divide(E.multiply((E.subtract(year, start)), (E.subtract(year, start))), (E.multiply(2, (E.subtract(end, start)))))
|
||||
|
||||
|
||||
func consume_from_batches(entity: OwniverseEntity, current_year: float) -> float:
|
||||
var total: float = 0.0
|
||||
func consume_from_batches(entity: OwniverseEntity, current_year: Variant) -> Variant:
|
||||
var total: Variant = 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 part : Dictionary in withdraw_batches(world.state, entity.id, current_year):
|
||||
total = E.add(total, part.amount)
|
||||
var fraction: Variant = (E.clamp_value(E.divide(total, physical[entity.id]), 0, 1) if E.gt(physical.get(entity.id, 0), 0) else 0)
|
||||
world.state.amount[entity.id] = E.multiply(world.state.amount.get(entity.id, 0), (E.subtract(1, 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.state.binds[owner_id][entity.id] = E.multiply(world.state.binds[owner_id][entity.id], E.subtract(1, 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:
|
||||
func add_entity_to_batch(entity_id: String, amount: Variant, current_year: Variant) -> void:
|
||||
append_batch(world.state, entity_id, amount, current_year, current_year)
|
||||
|
||||
+117
-124
@@ -1,23 +1,24 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.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():
|
||||
if (not world.catalog.is_group(id)):
|
||||
amounts[id] = E.maximum(0, source.amount.get(id, 0))
|
||||
for bindings : Dictionary in source.binds.values():
|
||||
for id in bindings:
|
||||
if amounts.has(id):
|
||||
amounts[id] += maxf(0.0, bindings[id])
|
||||
amounts[id] = E.add(amounts[id], E.maximum(0, bindings[id]))
|
||||
return amounts
|
||||
|
||||
|
||||
func available(source: Dictionary, id: String) -> float:
|
||||
var amount: float = 0.0
|
||||
func available(source: Dictionary, id: String) -> Variant:
|
||||
var amount: Variant = 0
|
||||
for member in world.catalog.members(id):
|
||||
amount += maxf(0.0, source.amount.get(member, 0.0))
|
||||
amount = E.add(amount, E.maximum(0, source.amount.get(member, 0)))
|
||||
return amount
|
||||
|
||||
|
||||
@@ -26,42 +27,41 @@ func expand_requirements(source: Dictionary, requirements: Dictionary) -> Dictio
|
||||
for id in requirements:
|
||||
var weights: Dictionary = {}
|
||||
for member in world.catalog.members(id):
|
||||
weights[member] = source.amount.get(member, 0.0)
|
||||
weights[member] = source.amount.get(member, 0)
|
||||
weights = QuantityMath.normalize_distribution(weights)
|
||||
for member in weights:
|
||||
expanded[member] = expanded.get(member, 0.0) + maxf(0.0, requirements[id]) * weights[member]
|
||||
expanded[member] = E.add(expanded.get(member, 0), E.multiply(E.maximum(0, requirements[id]), weights[member]))
|
||||
return expanded
|
||||
|
||||
|
||||
func take_available(target: Dictionary, id: String, amount: float) -> Dictionary:
|
||||
func take_available(target: Dictionary, id: String, amount: Variant) -> Dictionary:
|
||||
var quantities: Dictionary = {}
|
||||
for member in world.catalog.members(id):
|
||||
quantities[member] = target.amount.get(member, 0.0)
|
||||
quantities[member] = target.amount.get(member, 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()
|
||||
var total: Variant = E.minimum(amount, available(target, id))
|
||||
var discrete: bool = (not distribution.is_empty())
|
||||
for member in distribution:
|
||||
discrete = discrete and is_discrete(member)
|
||||
discrete = (discrete and is_discrete(member))
|
||||
if discrete:
|
||||
total = floorf(total)
|
||||
total = E.floor_value(total)
|
||||
var shares: Array = []
|
||||
var assigned: float = 0.0
|
||||
var assigned: Variant = 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]})
|
||||
var expected: Variant = E.multiply(total, distribution[member])
|
||||
taken[member] = (E.floor_value(expected) if discrete else expected)
|
||||
assigned = E.add(assigned, taken[member])
|
||||
shares.append({"id": member, "rest": E.subtract(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:
|
||||
shares.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
|
||||
for share : Dictionary in shares:
|
||||
if E.ge(assigned, total):
|
||||
break
|
||||
taken[share.id] += 1.0
|
||||
assigned += 1.0
|
||||
taken[share.id] = E.add(taken[share.id], 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
for member in taken:
|
||||
target.amount[member] = maxf(0.0, target.amount.get(member, 0.0) - taken[member])
|
||||
target.amount[member] = E.maximum(0, E.subtract(target.amount.get(member, 0), taken[member]))
|
||||
return taken
|
||||
|
||||
|
||||
@@ -69,45 +69,44 @@ func is_discrete(_id: String) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
func credit(target: Dictionary, id: String, amount: float) -> float:
|
||||
if amount <= 0.0:
|
||||
return 0.0
|
||||
func credit(target: Dictionary, id: String, amount: Variant) -> Variant:
|
||||
if E.le(amount, 0):
|
||||
return 0
|
||||
if is_discrete(id):
|
||||
if not target.has("production_remainders"):
|
||||
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 expected: Variant = E.add(amount, target.production_remainders.get(id, 0))
|
||||
amount = E.floor_value(expected)
|
||||
target.production_remainders[id] = E.subtract(expected, amount)
|
||||
target.amount[id] = E.add(target.amount.get(id, 0), amount)
|
||||
target.stats.total_amount[id] = E.add(target.stats.total_amount.get(id, 0), amount)
|
||||
if ((world.catalog.entities.has(id) and E.ne(world.catalog.entities[id].group_as, "")) and E.ne(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
|
||||
target.stats.total_amount[group] = E.add(target.stats.total_amount.get(group, 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):
|
||||
if (not is_discrete(id)):
|
||||
continue
|
||||
var slots: Array = [ {"owner": "", "value": target.amount.get(id, 0.0)}]
|
||||
var slots: Array = [{"owner": "", "value": target.amount.get(id, 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 == "":
|
||||
var assigned: Variant = 0
|
||||
for slot : Dictionary in slots:
|
||||
slot.whole = E.floor_value(slot.value)
|
||||
slot.rest = E.subtract(slot.value, slot.whole)
|
||||
assigned = E.add(assigned, slot.whole)
|
||||
slots.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.owner), str(b.owner))))
|
||||
var left: Variant = E.subtract(E.round_value(physical[id]), assigned)
|
||||
for slot : Dictionary in slots:
|
||||
if E.gt(left, 0):
|
||||
slot.whole = E.add(slot.whole, 1)
|
||||
left = E.subtract(left, 1)
|
||||
if E.eq(slot.owner, ""):
|
||||
target.amount[id] = slot.whole
|
||||
else:
|
||||
target.binds[slot.owner][id] = slot.whole
|
||||
@@ -117,48 +116,42 @@ 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)
|
||||
if E.ne(group, ""):
|
||||
groups[group] = E.add(groups.get(group, 0), target.amount.get(id, 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))
|
||||
target.auto_resources[id] = E.minimum(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 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):
|
||||
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)):
|
||||
var quantity: Variant = E.multiply(amounts[owner_id], E.maximum(0, world.catalog.entities[owner_id].bind[id]))
|
||||
if (not E.eq(quantity, E.round_value(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:
|
||||
if E.gt(quantity, 0):
|
||||
requests.append({"owner": owner_id, "id": id, "amount": E.round_value(quantity), "members": world.catalog.members(id).size()})
|
||||
requests.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.lt(a.members, b.members) if E.ne(a.members, b.members) else ((E.lt(str(a.id), str(b.id)) if E.ne(a.id, b.id) else E.lt(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
|
||||
var total: Variant = 0
|
||||
for quantity in taken.values():
|
||||
total += quantity
|
||||
if total != request.amount:
|
||||
total = E.add(total, quantity)
|
||||
if E.ne(total, request.amount):
|
||||
return false
|
||||
if not trial.binds.has(request.owner):
|
||||
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]
|
||||
trial.binds[request.owner][id] = E.add(trial.binds[request.owner].get(id, 0), taken[id])
|
||||
target.amount = trial.amount
|
||||
target.binds = trial.binds
|
||||
return true
|
||||
@@ -169,51 +162,51 @@ func bind_new_owners(target: Dictionary, amounts: Dictionary, require_full: bool
|
||||
return bind_full(target, amounts)
|
||||
var requests: Dictionary = {}
|
||||
for owner_id in amounts:
|
||||
if not world.catalog.entities.has(owner_id):
|
||||
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 quantity: Variant = E.multiply(amounts[owner_id], E.maximum(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[member] = target.amount.get(member, 0)
|
||||
weights = QuantityMath.normalize_distribution(weights)
|
||||
for member in weights:
|
||||
if not requests.has(member):
|
||||
if (not requests.has(member)):
|
||||
requests[member] = {}
|
||||
requests[member][owner_id] = requests[member].get(owner_id, 0.0) + quantity * weights[member]
|
||||
requests[member][owner_id] = E.add(requests[member].get(owner_id, 0), E.multiply(quantity, weights[member]))
|
||||
for id in requests:
|
||||
var requested: float = 0.0
|
||||
var requested: Variant = 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
|
||||
requested = E.add(requested, quantity)
|
||||
var available: Variant = available(target, id)
|
||||
var factor: Variant = (E.minimum(1, E.divide(available, requested)) if E.gt(requested, 0) else 0)
|
||||
for owner_id in requests[id]:
|
||||
var quantity: float = requests[id][owner_id] * factor
|
||||
if not target.binds.has(owner_id):
|
||||
var quantity: Variant = E.multiply(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)
|
||||
target.binds[owner_id][id] = E.add(target.binds[owner_id].get(id, 0), quantity)
|
||||
target.amount[id] = E.maximum(0, E.subtract(target.amount.get(id, 0), quantity))
|
||||
|
||||
return true
|
||||
|
||||
|
||||
func apply_costs(entity: OwniverseEntity, amount: float) -> void:
|
||||
func apply_costs(entity: OwniverseEntity, amount: Variant) -> void:
|
||||
var costs: Dictionary = {}
|
||||
for resource in entity.cost:
|
||||
costs[resource] = roundf(maxf(0.0, entity.cost[resource]) * amount)
|
||||
costs[resource] = E.round_value(E.multiply(E.maximum(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
|
||||
var factor: Variant = (E.subtract(1, E.divide(taken[id], quantities[id])) if E.gt(quantities.get(id, 0), 0) else 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):
|
||||
func apply_binds(item_id: String, _entity: OwniverseEntity, amount: Variant) -> bool:
|
||||
if (not bind_new_owners(world.state, {item_id: amount}, true)):
|
||||
return false
|
||||
refresh_groups(world.state)
|
||||
clamp_auto_resources(world.state)
|
||||
@@ -222,20 +215,20 @@ func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> bo
|
||||
return true
|
||||
|
||||
|
||||
func process_entity(entity_id: String, amount: float) -> void:
|
||||
if amount == 0:
|
||||
func process_entity(entity_id: String, amount: Variant) -> void:
|
||||
if E.eq(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)
|
||||
|
||||
|
||||
world.state.amount[entity_id] = E.add(world.state.amount.get(entity_id, 0), amount)
|
||||
world.state.stats.total_amount[entity_id] = E.add(world.state.stats.total_amount.get(entity_id, 0), E.maximum(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)
|
||||
if E.ne(entity.group_as, ""):
|
||||
world.state.amount[entity.group_as] = E.add(world.state.amount.get(entity.group_as, 0), amount)
|
||||
world.state.stats.total_amount[entity.group_as] = E.add(world.state.stats.total_amount.get(entity.group_as, 0), E.maximum(0, amount))
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
|
||||
|
||||
@@ -245,33 +238,33 @@ func transfer_evolution(target: Dictionary, fractions: Dictionary, outputs: Dict
|
||||
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)
|
||||
var owner_survival: Variant = E.subtract(1, fractions.get(owner_id, 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)
|
||||
var held: Variant = bindings[owner_id][id]
|
||||
var released: Variant = E.multiply(held, (E.subtract(1, owner_survival)))
|
||||
target.amount[id] = E.add(target.amount.get(id, 0), released)
|
||||
var heirs: Dictionary = (world.stellar.stellar_successors(outputs.get(owner_id, {})) if (world.catalog.entities.has(id) and E.eq(world.catalog.entities[id].group_as, "Planet")) else {})
|
||||
var fraction: Variant = fractions.get(id, 0)
|
||||
for heir in heirs:
|
||||
if not incoming.has(heir):
|
||||
if (not incoming.has(heir)):
|
||||
incoming[heir] = {}
|
||||
incoming[heir][id] = incoming[heir].get(id, 0.0) + released * (1.0 - fraction) * heirs[heir]
|
||||
incoming[heir][id] = E.add(incoming[heir].get(id, 0), E.multiply(E.multiply(released, (E.subtract(1, 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
|
||||
if (world.catalog.entities.has(child) and (not CivilizationState.is_space_civilization(child, Global.CivilizationRules))):
|
||||
incoming[heir][child] = E.add(incoming[heir].get(child, 0), E.multiply(E.multiply(E.multiply(released, fraction), heirs[heir]), outputs[id][child]))
|
||||
var remaining: Variant = E.multiply(E.multiply(held, owner_survival), (E.subtract(1, fraction)))
|
||||
target.binds[owner_id][id] = E.add(target.binds[owner_id].get(id, 0), remaining)
|
||||
for successor in outputs.get(id, {}):
|
||||
if not world.catalog.entities.has(successor) or CivilizationState.is_space_civilization(successor, Global.CivilizationRules):
|
||||
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):
|
||||
var transferred: Variant = E.multiply(E.multiply(E.multiply(held, owner_survival), fraction), E.maximum(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
|
||||
incoming[owner_id][successor] = E.add(incoming[owner_id].get(successor, 0), transferred)
|
||||
|
||||
for id in fractions:
|
||||
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) * (1.0 - fractions[id]))
|
||||
target.amount[id] = E.maximum(0, E.multiply(target.amount.get(id, 0), (E.subtract(1, fractions[id]))))
|
||||
|
||||
return incoming
|
||||
|
||||
@@ -279,9 +272,9 @@ func transfer_evolution(target: Dictionary, fractions: Dictionary, outputs: Dict
|
||||
## 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):
|
||||
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)
|
||||
var transferred: Variant = E.minimum(incoming[owner_id][id], target.amount.get(id, 0))
|
||||
target.binds[owner_id][id] = E.add(target.binds[owner_id].get(id, 0), transferred)
|
||||
target.amount[id] = E.maximum(0, E.subtract(target.amount.get(id, 0), transferred))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
extends Node
|
||||
class_name Owniverse
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
## Owns the state and modules. Rules live in the modules below.
|
||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
@@ -16,10 +17,7 @@ var evolution = preload("res://world/owniverse/evolution_coordinator.gd").new(se
|
||||
|
||||
var state: Dictionary = {
|
||||
'version': SimulationTime.STATE_VERSION,
|
||||
'time': {
|
||||
'datetime': 0.0,
|
||||
'year': 0.0,
|
||||
},
|
||||
'time': {'datetime': 0, 'year': 0},
|
||||
'speed': 1,
|
||||
'speed_in_year': 1,
|
||||
'energy': WorldParameters.INITIAL_ENERGY,
|
||||
@@ -43,22 +41,19 @@ var state: Dictionary = {
|
||||
'civilization_stats': {},
|
||||
'civilization_homes': {},
|
||||
'ethics': {},
|
||||
'stats': {
|
||||
'total_amount': {},
|
||||
}
|
||||
'stats': {'total_amount': {}},
|
||||
}
|
||||
|
||||
var speed: int:
|
||||
var speed: int :
|
||||
get:
|
||||
return state.speed
|
||||
set(value):
|
||||
state.speed = clampi(value, 1, Global.SPEED_LIMIT)
|
||||
state.speed_in_year = pow(10.0, state.speed - 1)
|
||||
state.speed_in_year = E.power(10, E.subtract(state.speed, 1))
|
||||
|
||||
func evolute(delta: float) -> void:
|
||||
if delta <= 0.0 or not is_finite(delta):
|
||||
func evolute(delta: Variant) -> void:
|
||||
if (E.le(delta, 0) or (not E.finite(delta))):
|
||||
return
|
||||
var datetime: float = state.time.datetime + delta * pow(10.0, speed - 1)
|
||||
if not is_finite(datetime) or not is_finite(datetime * 10.0):
|
||||
return
|
||||
evolution.evolute_to_datetime(datetime)
|
||||
var clock: Dictionary = SimulationTime.advance(state.time, E.approx(delta), speed)
|
||||
if not E.finite(clock.datetime): return
|
||||
evolution.evolute_to_datetime(clock.datetime, clock)
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
extends Node
|
||||
class_name OwniverseEntity
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
var id: String
|
||||
var is_visible: bool = false
|
||||
var unlock: Dictionary = {}
|
||||
var cost: Dictionary = {}
|
||||
var bind: Dictionary = {}
|
||||
var mass: float
|
||||
var mass: Variant
|
||||
var space: int
|
||||
var life_rule: Dictionary = {}
|
||||
var lifespan: float
|
||||
var lifespan_delta: float
|
||||
var auto: float
|
||||
var gamma: float
|
||||
var lifespan: Variant
|
||||
var lifespan_delta: Variant
|
||||
var auto: Variant
|
||||
var gamma: Variant
|
||||
var blast: int
|
||||
var blastability: bool = false
|
||||
var life_product: Dictionary = {}
|
||||
var product_per_year_type: String = ""
|
||||
var product_per_year_amount: float = 0
|
||||
var product_per_year_amount: Variant = 0
|
||||
var voids: int
|
||||
var output_or: Dictionary = {}
|
||||
var output_and: Dictionary = {}
|
||||
@@ -27,49 +28,49 @@ var planets: Dictionary = {}
|
||||
var count_as: String
|
||||
var group_as: String
|
||||
var event_group: String
|
||||
var space_bonus: float
|
||||
var civ_bonus: float
|
||||
var space_bonus: Variant
|
||||
var civ_bonus: Variant
|
||||
var claim: String
|
||||
|
||||
var amount: float = 0.0
|
||||
var amount: Variant = 0
|
||||
|
||||
|
||||
func _init(values: Dictionary) -> void:
|
||||
id = values.id
|
||||
is_visible = values.is_visible == "1"
|
||||
is_visible = E.eq(values.is_visible, "1")
|
||||
unlock = _parse_and_string(values.unlock)
|
||||
cost = _parse_and_string(values.cost)
|
||||
bind = _parse_and_string(values.bind)
|
||||
#print(id, " | ", cost, " | ", bind)
|
||||
mass = float(values.mass)
|
||||
space = int(values.space)
|
||||
mass = E.of(values.mass)
|
||||
space = E.integer(values.space)
|
||||
life_rule = Global.LifeRules.get(id, {}).duplicate(true)
|
||||
if Global.CivilizationRules.has(id):
|
||||
life_rule = {"ethical": true, "span": Global.CivilizationRules[id].span}
|
||||
lifespan = float(life_rule.span) if not life_rule.is_empty() else float(values.lifespan)
|
||||
lifespan_delta = lifespan / Global.batch_count if lifespan > 10 else lifespan
|
||||
auto = float(values.auto)
|
||||
gamma = float(values.gamma)
|
||||
blast = int(values.blast)
|
||||
blastability = str(values.get("blastability", "")) == "1"
|
||||
|
||||
lifespan = (E.of(life_rule.span) if (not life_rule.is_empty()) else E.of(values.lifespan))
|
||||
lifespan_delta = (E.divide(lifespan, Global.batch_count) if E.gt(lifespan, 10) else lifespan)
|
||||
auto = E.of(values.auto)
|
||||
gamma = E.of(values.gamma if str(values.gamma).is_valid_float() else "0")
|
||||
blast = E.integer(values.blast)
|
||||
blastability = E.eq(str(values.get("blastability", "")), "1")
|
||||
|
||||
life_product = _parse_and_string(values.life_product)
|
||||
for key in life_product.keys():
|
||||
product_per_year_type = key
|
||||
product_per_year_amount = life_product[key] / lifespan if lifespan != 0 else 0
|
||||
product_per_year_amount = (E.divide(life_product[key], lifespan) if E.ne(lifespan, 0) else 0)
|
||||
#print(id, " | ", lifespan_delta, " | ", lifespan)
|
||||
|
||||
voids = int(values.voids)
|
||||
|
||||
voids = E.integer(values.voids)
|
||||
output_or = _parse_or_string(values.output)
|
||||
output_and = _parse_and_string(values.output)
|
||||
destroy_level = int(values.destroy_level)
|
||||
destroy_level = E.integer(values.destroy_level)
|
||||
degradation = _parse_and_string(values.degradation)
|
||||
planets = _parse_planet_string(values.planets)
|
||||
count_as = values.count_as
|
||||
group_as = values.group_as
|
||||
event_group = values.event_group
|
||||
space_bonus = float(values.space_bonus)
|
||||
civ_bonus = float(values.civ_bonus)
|
||||
space_bonus = E.of(values.space_bonus)
|
||||
civ_bonus = E.of(values.civ_bonus)
|
||||
claim = values.claim
|
||||
|
||||
|
||||
@@ -78,39 +79,39 @@ func _parse_or_string(source_line: String) -> Dictionary:
|
||||
if source_line.contains("^"):
|
||||
var first_split = source_line.split("^")
|
||||
var second_split = first_split[1].split("%")
|
||||
var probability = float(second_split[1])
|
||||
var probability = E.of(second_split[1])
|
||||
result["chance"] = probability
|
||||
result["positive"] = second_split[0]
|
||||
result["negative"] = first_split[0]
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
func _parse_and_string(source_line: String) -> Dictionary:
|
||||
var result = {}
|
||||
var first_split = source_line.split("&")
|
||||
|
||||
|
||||
for elem in first_split:
|
||||
var second_split = elem.split(":")
|
||||
if len(second_split) == 2:
|
||||
result[second_split[0]] = float(second_split[1])
|
||||
|
||||
if E.eq(len(second_split), 2):
|
||||
result[second_split[0]] = E.of(second_split[1])
|
||||
|
||||
return result
|
||||
|
||||
func _parse_planet_string(source_line: String) -> Dictionary:
|
||||
var result = {}
|
||||
for elem in source_line.split("&", false):
|
||||
var parts: PackedStringArray = elem.strip_edges().split(":")
|
||||
if parts.size() != 3 or parts[0].strip_edges().is_empty():
|
||||
push_warning("Invalid planet definition for %s: %s" % [id, elem])
|
||||
if (E.ne(parts.size(), 3) or parts[0].strip_edges().is_empty()):
|
||||
push_warning(("Invalid planet definition for %s: %s" % [id, elem]))
|
||||
continue
|
||||
if not parts[1].is_valid_float() or not parts[2].is_valid_float():
|
||||
push_warning("Invalid planet numbers for %s: %s" % [id, elem])
|
||||
if ((not parts[1].is_valid_float()) or (not parts[2].is_valid_float())):
|
||||
push_warning(("Invalid planet numbers for %s: %s" % [id, elem]))
|
||||
continue
|
||||
var count: float = float(parts[1])
|
||||
var chance: float = float(parts[2])
|
||||
if not is_finite(count) or not is_finite(chance) or count < 0.0 or count != floorf(count) or chance < 0.0 or chance > 1.0:
|
||||
push_warning("Planet values out of range for %s: %s" % [id, elem])
|
||||
var count: Variant = E.of(parts[1])
|
||||
var chance: Variant = E.of(parts[2])
|
||||
if ((((((not E.finite(count)) or (not E.finite(chance))) or E.lt(count, 0)) or E.ne(count, E.floor_value(count))) or E.lt(chance, 0)) or E.gt(chance, 1)):
|
||||
push_warning(("Planet values out of range for %s: %s" % [id, elem]))
|
||||
continue
|
||||
# Planet chances are fractions (0.01 = 1%), unlike output_or percentages.
|
||||
result[parts[0].strip_edges()] = {"amount": count, "chance": chance}
|
||||
|
||||
@@ -1,60 +1,51 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
## Quantity arithmetic without scene tree or global settings.
|
||||
|
||||
static func accumulate_whole(target: Dictionary, field: String, key: String, value: float) -> float:
|
||||
if not target.has(field):
|
||||
static func accumulate_whole(target: Dictionary, field: String, key: String, value: Variant) -> Variant:
|
||||
if (not target.has(field)):
|
||||
target[field] = {}
|
||||
var expected: float = maxf(0.0, value) + target[field].get(key, 0.0)
|
||||
var whole: float = floorf(snappedf(expected, 1.0e-12))
|
||||
target[field][key] = maxf(0.0, expected - whole)
|
||||
var expected: Variant = E.add(E.maximum(0, value), target[field].get(key, 0))
|
||||
var whole: Variant = E.floor_value(expected)
|
||||
target[field][key] = E.maximum(0, E.subtract(expected, whole))
|
||||
return whole
|
||||
|
||||
|
||||
static func allocate_whole(requests: Dictionary, limit: float) -> Dictionary:
|
||||
var total: float = 0.0
|
||||
static func allocate_whole(requests: Dictionary, limit: Variant) -> Dictionary:
|
||||
var total: Variant = 0
|
||||
for amount in requests.values():
|
||||
total += floorf(maxf(0.0, amount))
|
||||
var available: float = floorf(minf(maxf(0.0, limit), total))
|
||||
total = E.add(total, E.floor_value(E.maximum(0, amount)))
|
||||
var available: Variant = E.floor_value(E.minimum(E.maximum(0, limit), total))
|
||||
var result: Dictionary = {}
|
||||
var order: Array = []
|
||||
var assigned: float = 0.0
|
||||
var assigned: Variant = 0
|
||||
for id in requests:
|
||||
var expected: float = floorf(maxf(0.0, requests[id])) * available / total if total > 0.0 else 0.0
|
||||
result[id] = floorf(expected)
|
||||
assigned += result[id]
|
||||
order.append({"id": id, "rest": expected - result[id]})
|
||||
order.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 item: Dictionary in order:
|
||||
if assigned >= available:
|
||||
var expected: Variant = (E.divide(E.multiply(E.floor_value(E.maximum(0, requests[id])), available), total) if E.gt(total, 0) else 0)
|
||||
result[id] = E.floor_value(expected)
|
||||
assigned = E.add(assigned, result[id])
|
||||
order.append({"id": id, "rest": E.subtract(expected, result[id])})
|
||||
order.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
|
||||
for item : Dictionary in order:
|
||||
if E.ge(assigned, available):
|
||||
break
|
||||
if result[item.id] < floorf(requests[item.id]):
|
||||
result[item.id] += 1.0
|
||||
assigned += 1.0
|
||||
if E.lt(result[item.id], E.floor_value(requests[item.id])):
|
||||
result[item.id] = E.add(result[item.id], 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
return result
|
||||
|
||||
|
||||
static func production_step(costs: Array) -> float:
|
||||
var step: float = 1.0
|
||||
for value in costs:
|
||||
var cost: float = maxf(0.0, value)
|
||||
if cost == 0.0:
|
||||
continue
|
||||
var decimal: String = String.num(cost, 12)
|
||||
var point: int = decimal.find(".")
|
||||
var digits: int = decimal.length() - point - 1 if point >= 0 else 0
|
||||
var denominator: float = pow(10.0, digits)
|
||||
var numerator: float = roundf(cost * denominator)
|
||||
if numerator == 0.0:
|
||||
return INF
|
||||
denominator /= whole_gcd(numerator, denominator)
|
||||
step = step / whole_gcd(step, denominator) * denominator
|
||||
static func production_step(costs: Array):
|
||||
var step = 1
|
||||
for cost in costs:
|
||||
if E.le(cost, 0): continue
|
||||
var denominator = E.make(E.parts(cost)[1])
|
||||
step = E.multiply(E.divide(step, whole_gcd(step, denominator)), denominator)
|
||||
return step
|
||||
|
||||
|
||||
static func whole_gcd(a: float, b: float) -> float:
|
||||
while b > 0.0:
|
||||
var remainder: float = fmod(a, b)
|
||||
static func whole_gcd(a: Variant, b: Variant) -> Variant:
|
||||
while E.gt(b, 0):
|
||||
var remainder: Variant = E.remainder(a, b)
|
||||
a = b
|
||||
b = remainder
|
||||
return a
|
||||
@@ -62,50 +53,50 @@ static func whole_gcd(a: float, b: float) -> float:
|
||||
|
||||
static func normalize_distribution(values: Dictionary) -> Dictionary:
|
||||
var normalized_distribution: Dictionary = {}
|
||||
var distribution_total: float = 0
|
||||
var distribution_total: Variant = 0
|
||||
for id in values:
|
||||
if values[id] > 0:
|
||||
distribution_total += values[id]
|
||||
if E.gt(values[id], 0):
|
||||
distribution_total = E.add(distribution_total, values[id])
|
||||
normalized_distribution[id] = values[id]
|
||||
for id in normalized_distribution:
|
||||
normalized_distribution[id] /= distribution_total
|
||||
normalized_distribution[id] = E.divide(normalized_distribution[id], distribution_total)
|
||||
return normalized_distribution
|
||||
|
||||
|
||||
static func sum(pop: Dictionary) -> float:
|
||||
var result: float = 0.0
|
||||
for n in pop.values(): result += n
|
||||
static func sum(pop: Dictionary) -> Variant:
|
||||
var result: Variant = 0
|
||||
for n in pop.values(): result = E.add(result, n)
|
||||
return result
|
||||
|
||||
|
||||
static func split(remainders: Dictionary, key: String, count: float, weights: Dictionary) -> Dictionary:
|
||||
static func split(remainders: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
|
||||
var result: Dictionary = {}
|
||||
var sum_weights: float = sum(weights)
|
||||
if count <= 0.0 or sum_weights <= 0.0: return result
|
||||
var sum_weights: Variant = sum(weights)
|
||||
if (E.le(count, 0) or E.le(sum_weights, 0)): return result
|
||||
var expected: Dictionary = {}
|
||||
var positive_sum: float = 0.0
|
||||
var positive_sum: Variant = 0
|
||||
for id in weights:
|
||||
expected[id] = count * weights[id] / sum_weights + remainders.get(key + ":" + id, 0.0)
|
||||
positive_sum += maxf(0.0, expected[id])
|
||||
expected[id] = E.add(E.divide(E.multiply(count, weights[id]), sum_weights), remainders.get(E.add(E.add(key, ":"), id), 0))
|
||||
positive_sum = E.add(positive_sum, E.maximum(0, expected[id]))
|
||||
var order: Array = []
|
||||
var assigned: float = 0.0
|
||||
var assigned: Variant = 0
|
||||
for id in weights:
|
||||
var normalized: float = maxf(0.0, expected[id]) * count / positive_sum if positive_sum > 0.0 else 0.0
|
||||
result[id] = floorf(normalized)
|
||||
assigned += result[id]
|
||||
order.append({"id": id, "rest": normalized - result[id]})
|
||||
order.sort_custom(func(a, b): return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
|
||||
var normalized: Variant = (E.divide(E.multiply(E.maximum(0, expected[id]), count), positive_sum) if E.gt(positive_sum, 0) else 0)
|
||||
result[id] = E.floor_value(normalized)
|
||||
assigned = E.add(assigned, result[id])
|
||||
order.append({"id": id, "rest": E.subtract(normalized, result[id])})
|
||||
order.sort_custom(func ( a , b ) : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
|
||||
for entry in order:
|
||||
if assigned >= count: break
|
||||
result[entry.id] += 1.0
|
||||
assigned += 1.0
|
||||
if E.ge(assigned, count): break
|
||||
result[entry.id] = E.add(result[entry.id], 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
for id in weights:
|
||||
remainders[key + ":" + id] = clampf(expected[id] - result[id], -1.0, 1.0)
|
||||
remainders[E.add(E.add(key, ":"), id)] = E.clamp_value(E.subtract(expected[id], result[id]), E.neg(1), 1)
|
||||
return result
|
||||
|
||||
|
||||
static func accumulate_rate(remainders: Dictionary, key: String, count: float, probability: float) -> float:
|
||||
var expected: float = count * maxf(0.0, probability) + remainders.get(key, 0.0)
|
||||
var actual: float = floorf(snappedf(expected, 1e-10))
|
||||
remainders[key] = maxf(0.0, expected - actual)
|
||||
static func accumulate_rate(remainders: Dictionary, key: String, count: Variant, probability: Variant) -> Variant:
|
||||
var expected: Variant = E.add(E.multiply(count, E.maximum(0, probability)), remainders.get(key, 0))
|
||||
var actual: Variant = E.floor_value(expected)
|
||||
remainders[key] = E.maximum(0, E.subtract(expected, actual))
|
||||
return actual
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
extends RefCounted
|
||||
## datetime is accumulated time; year is the last processed tenth of a year.
|
||||
const STATE_VERSION: String = "0.1.1"
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const STATE_VERSION: String = "0.2.0"
|
||||
const TICKS_PER_YEAR: int = 1000000000
|
||||
|
||||
static func processed_year(datetime: float) -> float:
|
||||
# Remove only sub-nanotick accumulation noise at decimal boundaries.
|
||||
# A relative epsilon would incorrectly advance large ages by whole years.
|
||||
return floorf(snappedf(datetime * 10.0, 1e-9)) / 10.0
|
||||
static func processed_year(datetime):
|
||||
return E.divide(E.floor_value(E.multiply(datetime, 10)), 10)
|
||||
|
||||
static func elapsed_years(from_year: float, to_year: float) -> float:
|
||||
# Subtract integer tenths before returning to float years. Otherwise 0.3-0.2
|
||||
# can fall below 0.1 and postpone a whole unit of resource production.
|
||||
return (roundf(to_year * 10.0) - roundf(from_year * 10.0)) / 10.0
|
||||
static func elapsed_years(from_year, to_year):
|
||||
return E.subtract(to_year, from_year)
|
||||
|
||||
## Frame fractions stay small; the universe age is an exact integer tick count.
|
||||
static func advance(time: Dictionary, delta: float, speed: int) -> Dictionary:
|
||||
var ticks = E.round_value(E.multiply(time.datetime, TICKS_PER_YEAR))
|
||||
var increment: float = delta * pow(10.0, speed - 1) * TICKS_PER_YEAR + float(time.get("tick_remainder", 0.0))
|
||||
var whole: float = roundf(increment) if absf(increment - roundf(increment)) < 1e-6 else floorf(increment)
|
||||
ticks = E.add(ticks, E.of(whole))
|
||||
var datetime = E.divide(ticks, TICKS_PER_YEAR)
|
||||
return {"datetime": datetime, "year": processed_year(datetime), "ticks": ticks, "tick_remainder": increment - whole}
|
||||
|
||||
@@ -1,118 +1,117 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.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:
|
||||
func add_output_terms(outputs: Dictionary, terms: String, count: Variant) -> void:
|
||||
if E.le(count, 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
|
||||
var amount: Variant = (E.of(parts[1]) if E.eq(parts.size(), 2) else 1)
|
||||
if (E.ne(id, "") and E.gt(amount, 0)):
|
||||
outputs[id] = E.add(outputs.get(id, 0), E.multiply(count, amount))
|
||||
|
||||
|
||||
func evolution_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary:
|
||||
if not entity.life_rule.is_empty():
|
||||
func evolution_products(target: Dictionary, entity: OwniverseEntity, count: Variant) -> 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():
|
||||
outputs[id] = E.multiply(count, E.maximum(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"):
|
||||
var chance: Variant = E.clamp_value(E.multiply(E.divide(entity.output_or.get("chance", 0), 100), WorldParameters.effective(target, "Exp")), 0, 1)
|
||||
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)
|
||||
var expected: Variant = E.add(E.multiply(count, chance), target.evolution_remainders.get(entity.id, 0))
|
||||
var positive: Variant = E.minimum(count, E.floor_value(expected))
|
||||
target.evolution_remainders[entity.id] = E.maximum(0, E.subtract(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)
|
||||
add_output_terms(outputs, str(entity.output_or.get("negative", "")), E.subtract(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")
|
||||
if E.gt(entity.blast, 0):
|
||||
outputs["blast"] = E.multiply(E.multiply(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():
|
||||
func create_planets(target: Dictionary, star_id: String, count: Variant, from_year: Variant, to_year: Variant, metallicity: Variant) -> void:
|
||||
if (E.le(count, 0) or world.catalog.entities[star_id].planets.is_empty()):
|
||||
return
|
||||
if not target.binds.has(star_id):
|
||||
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):
|
||||
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))
|
||||
var amount: Variant = QuantityMath.accumulate_whole(target, "planet_remainders", E.add(E.add(star_id, ":"), id), E.multiply(E.multiply(E.multiply(count, rule.amount), rule.chance), E.maximum(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
|
||||
target.amount[id] = E.subtract(target.amount[id], amount)
|
||||
target.binds[star_id][id] = E.add(target.binds[star_id].get(id, 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"):
|
||||
if (world.catalog.entities.has(id) and (((E.contains(["Star", "BH"], world.catalog.entities[id].group_as) or E.eq(world.catalog.entities[id].event_group, "SNGRBB")) or E.eq(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]:
|
||||
func destroy_planets(target: Dictionary, owner_id: String, ratio: Variant, level: int, cause: String = "local") -> void:
|
||||
if (E.le(ratio, 0) or not E.contains([1, 2], level)):
|
||||
return
|
||||
var held: Dictionary = target.binds.get(owner_id, {})
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
var rocky: float = 0.0
|
||||
var rocky: Variant = 0
|
||||
for id in held.keys():
|
||||
if not world.catalog.entities.has(id) or world.catalog.entities[id].group_as != "Planet":
|
||||
if ((not world.catalog.entities.has(id)) or E.ne(world.catalog.entities[id].group_as, "Planet")):
|
||||
continue
|
||||
if level == 1 and not world.catalog.entities[id].blastability:
|
||||
if (E.eq(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:
|
||||
var key: String = E.add(E.add(E.add(E.add(E.add(E.add(cause, ":"), str(level)), ":"), owner_id), ":"), id)
|
||||
var destroyed: Variant = E.minimum(held[id], QuantityMath.accumulate_whole(target, "destruction_remainders", key, E.multiply(held[id], E.minimum(1, ratio))))
|
||||
if E.eq(destroyed, held[id]):
|
||||
target.destruction_remainders[key] = 0
|
||||
if E.le(destroyed, 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:
|
||||
held[id] = E.subtract(held[id], destroyed)
|
||||
world.batches.scale_batches(target, id, E.subtract(1, E.divide(destroyed, physical[id])))
|
||||
if E.eq(level, 1):
|
||||
rocky = E.add(rocky, destroyed)
|
||||
if E.gt(rocky, 0):
|
||||
rocky = world.ledger.credit(target, "RP", rocky)
|
||||
target.amount.RP -= rocky
|
||||
held.RP = held.get("RP", 0.0) + rocky
|
||||
target.amount.RP = E.subtract(target.amount.RP, rocky)
|
||||
held.RP = E.add(held.get("RP", 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)
|
||||
func apply_blast(target: Dictionary, power: Variant, environment: Dictionary) -> void:
|
||||
var hits: Variant = QuantityMath.accumulate_whole(target, "blast_remainders", "hits", E.multiply(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:
|
||||
if ((not world.catalog.entities[id].planets.is_empty()) and E.gt(quantities.get(id, 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")
|
||||
destroy_planets(target, id, E.multiply(E.divide(E.multiply(hits, distribution[id]), quantities[id]), environment.density), 1, "blast")
|
||||
|
||||
|
||||
func uses_short_lived_fast_path(id: String) -> bool:
|
||||
if not direct_completions: return false
|
||||
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():
|
||||
if ((E.ne(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:
|
||||
if (world.catalog.entities.has(successor) and E.gt(world.catalog.entities[successor].lifespan, 0)):
|
||||
return false
|
||||
return true
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.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)
|
||||
var mass: Variant = source.amount.get("SMBH", 0)
|
||||
var volume: Variant = quantities.get("S", 0)
|
||||
for id in quantities:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
if id in ["H", "He", "Met", "SMBH"]:
|
||||
mass += quantities[id]
|
||||
if E.contains(["H", "He", "Met", "SMBH"], id):
|
||||
mass = E.add(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,
|
||||
}
|
||||
mass = E.add(mass, E.multiply(quantities[id], E.maximum(0, entity.mass)))
|
||||
volume = E.add(volume, E.multiply(quantities[id], E.maximum(0, entity.space)))
|
||||
volume = E.add(volume, E.multiply(E.multiply(quantities[id], E.maximum(0, entity.voids)), E.of("1.0e22")))
|
||||
var metallicity: Variant = 0
|
||||
if E.gt(mass, 0):
|
||||
metallicity = E.maximum(0, E.add(E.multiply(E.of("0.4"), E.logarithm(E.add(E.divide(E.multiply(100, source.amount.get("Met", 0)), mass), E.of("0.11")))), E.of("0.88")))
|
||||
var base_mass: Variant = E.multiply(65536, ((E.power(mass, E.of("0.1")) if (E.gt(mass, 0) and E.lt(mass, E.of("1.0e10"))) else ((10 if E.ge(mass, E.of("1.0e10")) else 1)))))
|
||||
return {"metallicity": metallicity, "density": E.divide(E.multiply(E.of("1.23456e-7"), E.square_root(E.add(base_mass, mass))), E.power(E.maximum(1, volume), E.divide(1, 6))), "blast": (E.clamp_value(E.divide(source.amount.get("S", 0), volume), 0, 1) if E.gt(volume, 0) else 1)}
|
||||
|
||||
|
||||
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
|
||||
func modify_world_parameter(key: String, change: Variant) -> void:
|
||||
if (not E.contains(["Bur", "Exp", "Agg"], key) or (not E.finite(change))): return
|
||||
var old_multiplier: Variant = WorldParameters.effective(world.state, key)
|
||||
if (not world.state.has("world_parameters")): world.state.world_parameters = WorldParameters.defaults()
|
||||
world.state.world_parameters[key] = E.clamp_value(E.add(WorldParameters.value(world.state, key), change), 1, 19)
|
||||
if E.eq(key, "Bur"):
|
||||
var ratio: Variant = E.divide(old_multiplier, WorldParameters.effective(world.state, key))
|
||||
var now: Variant = 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]:
|
||||
if ((not world.catalog.entities.has(id)) or E.ne(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
|
||||
if batch.has(field): batch[field] = E.add(now, E.multiply(E.maximum(0, E.subtract(batch[field], now)), ratio))
|
||||
|
||||
@@ -1,112 +1,106 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
const INITIAL_ENERGY = 1.0e27
|
||||
static var INITIAL_ENERGY = E.of("1.0e27")
|
||||
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],
|
||||
}
|
||||
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}
|
||||
return {"Bur": 10, "Exp": 10, "Agg": 10, "Federation": 1, "Enlightenment": 1, "Order": 1}
|
||||
|
||||
static func trend_defaults() -> Dictionary:
|
||||
return {"Antitheism": 0.0, "Integration": 0.0, "Xenocide": 0.0,
|
||||
"Reconstruction": 0.0, "Spirituality": 0.0}
|
||||
return {"Antitheism": 0, "Integration": 0, "Xenocide": 0, "Reconstruction": 0, "Spirituality": 0}
|
||||
|
||||
static func value(source: Dictionary, key: String) -> float:
|
||||
return source.get("world_parameters", {}).get(key, defaults().get(key, 1.0))
|
||||
static func value(source: Dictionary, key: String) -> Variant:
|
||||
return source.get("world_parameters", {}).get(key, defaults().get(key, 1))
|
||||
|
||||
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 effective(source: Dictionary, key: String) -> Variant:
|
||||
return E.power(10, E.divide((E.subtract(E.clamp_value(value(source, key), 1, 19), 10)), 9))
|
||||
|
||||
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}
|
||||
var aggression: Variant = effective(source, "Agg")
|
||||
return {"progress": aggression, "degradation": E.divide((E.subtract(1, E.divide((E.subtract(1, aggression)), 20))), value(source, "Order")), "war": E.subtract(1, E.divide((E.subtract(1, aggression)), 5)), "colonization": E.subtract(1, E.divide((E.subtract(1, aggression)), 2))}
|
||||
|
||||
static func production(source: Dictionary, entity) -> float:
|
||||
if entity.event_group == "SNGRBB":
|
||||
static func production(source: Dictionary, entity) -> Variant:
|
||||
if E.eq(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
|
||||
if E.eq(entity.group_as, "Star"):
|
||||
var burn: Variant = effective(source, "Bur")
|
||||
return (E.square_root(burn) if E.gt(burn, 1) else burn)
|
||||
return 1
|
||||
|
||||
static func density_display(density: float) -> Dictionary:
|
||||
if density <= 1.0e-5:
|
||||
static func density_display(density: Variant) -> Dictionary:
|
||||
if E.le(density, E.of("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)}
|
||||
return {"mode": "DensityRip", "position": E.clamp_value(E.divide((E.add(E.divide(E.logarithm(E.maximum(density, E.of("1e-7"))), E.logarithm(10)), 7)), 2), 0, 1)}
|
||||
if E.ge(density, E.of("0.9")):
|
||||
return {"mode": "DensityCrunch", "position": E.clamp_value(E.multiply((E.subtract(density, E.of("0.9"))), 10), 0, 1)}
|
||||
return {"mode": "Density", "position": E.clamp_value(E.divide(E.logarithm(E.multiply(density, 100000)), E.logarithm(90000)), 0, 1)}
|
||||
|
||||
## Unity ApocalypseCheck thresholds and WarningUI colors; visual warning only.
|
||||
static func density_warning_color(density: float) -> Color:
|
||||
if density >= 1.0: return Color.html("00c0ffff")
|
||||
if density >= 0.97: return Color.html("20b0dfbf")
|
||||
if density >= 0.9: return Color.html("40a0c07f")
|
||||
if density <= 1.0e-7: return Color.html("ff0000ff")
|
||||
if density <= 1.0e-6: return Color.html("df2020bf")
|
||||
if density <= 1.0e-5: return Color.html("c040407f")
|
||||
return Color(0.0, 0.0, 0.0, 0.0)
|
||||
static func density_warning_color(density: Variant) -> Color:
|
||||
if E.ge(density, 1): return Color.html("00c0ffff")
|
||||
if E.ge(density, E.of("0.97")): return Color.html("20b0dfbf")
|
||||
if E.ge(density, E.of("0.9")): return Color.html("40a0c07f")
|
||||
if E.le(density, E.of("1.0e-7")): return Color.html("ff0000ff")
|
||||
if E.le(density, E.of("1.0e-6")): return Color.html("df2020bf")
|
||||
if E.le(density, E.of("1.0e-5")): return Color.html("c040407f")
|
||||
return Color(0, 0, 0, 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]
|
||||
static func advance(previous: Dictionary, target: Dictionary, rules: Dictionary, years: Variant) -> void:
|
||||
if E.le(years, 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]
|
||||
var stages: Array = []
|
||||
for id in rules:
|
||||
if str(rules[id].home) != "": stages.append(id)
|
||||
if E.ne(str(rules[id].home), ""): stages.append(id)
|
||||
stages.sort()
|
||||
var weight: float = 1.0
|
||||
var weight: Variant = 1
|
||||
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:
|
||||
var count: Variant = 0
|
||||
for n in population.values(): count = E.add(count, n)
|
||||
if E.gt(count, 0):
|
||||
for ethic in population:
|
||||
for i in TRENDS.size():
|
||||
rates[i] += population[ethic] / count * WEIGHTS[ethic][i] * weight
|
||||
weight *= 2.0
|
||||
rates[i] = E.add(rates[i], E.multiply(E.multiply(E.divide(population[ethic], count), WEIGHTS[ethic][i]), weight))
|
||||
weight = E.multiply(weight, 2)
|
||||
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)
|
||||
var accumulated: Variant = E.add(target.trends[name], E.multiply(E.multiply(rates[i], years), E.of("1.0e-9")))
|
||||
var events: Variant = E.floor_value(accumulated)
|
||||
target.trends[name] = E.maximum(0, E.subtract(accumulated, events))
|
||||
if E.le(events, 0): continue
|
||||
var key: String = E.add("trend:", name)
|
||||
target.civilization_stats[key] = E.add(target.civilization_stats.get(key, 0), events)
|
||||
if E.eq(name, "Integration"): _integration(target, events)
|
||||
|
||||
static func _integration(target: Dictionary, events: float) -> void:
|
||||
static func _integration(target: Dictionary, events: Variant) -> 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()
|
||||
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
|
||||
rng.state = E.integer(target.civilization_stats.trend_rng)
|
||||
var counts: Array = [0, 0, 0]
|
||||
if E.le(events, 256):
|
||||
for i in E.integer(events):
|
||||
var chosen: int = rng.randi_range(0, 2)
|
||||
counts[chosen] = E.add(counts[chosen], 1)
|
||||
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]
|
||||
counts[0] = E.clamp_value(E.round_value(E.of(rng.randfn(E.approx(E.divide(events, 3)), E.approx(E.square_root(E.divide(E.multiply(events, 2), 9)))))), 0, events)
|
||||
var rest: Variant = E.subtract(events, counts[0])
|
||||
counts[1] = E.clamp_value(E.round_value(E.of(rng.randfn(E.approx(E.divide(rest, 2)), E.approx(E.square_root(E.divide(rest, 4)))))), 0, rest)
|
||||
counts[2] = E.subtract(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
|
||||
parameters.Agg = E.maximum(1, E.subtract(E.subtract(parameters.Agg, counts[0]), counts[1]))
|
||||
parameters.Federation = E.add(parameters.Federation, E.multiply(counts[0], E.of("0.1")))
|
||||
parameters.Enlightenment = E.add(parameters.Enlightenment, E.multiply(counts[1], E.of("0.2")))
|
||||
parameters.Order = E.add(parameters.Order, E.multiply(counts[2], E.of("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]
|
||||
target.civilization_stats[key] = E.add(target.civilization_stats.get(key, 0), counts[i])
|
||||
|
||||
+13
-8
@@ -99,6 +99,7 @@ size_flags_vertical = 3
|
||||
script = ExtResource("3_k83x5")
|
||||
|
||||
[node name="World" type="VBoxContainer" parent="Stage" unique_id=1878643860]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
@@ -1058,10 +1059,9 @@ texture = ExtResource("61_d36xh")
|
||||
stretch_mode = 3
|
||||
|
||||
[node name="Credits" type="VBoxContainer" parent="Stage" unique_id=785042592]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
offset_right = 1080.0
|
||||
offset_bottom = 1920.0
|
||||
script = ExtResource("62_o2i0n")
|
||||
|
||||
[node name="TopMenu" type="HBoxContainer" parent="Stage/Credits" unique_id=1983527467]
|
||||
@@ -1088,15 +1088,20 @@ texture = ExtResource("62_vu83j")
|
||||
custom_minimum_size = Vector2(1080, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Highlight" type="TextureRect" parent="Stage/Credits/Logo" unique_id=1035898922]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
texture = ExtResource("64_rur7c")
|
||||
|
||||
[node name="Lamp" type="TextureButton" parent="Stage/Credits/Logo" unique_id=1059180893]
|
||||
layout_mode = 2
|
||||
texture_normal = ExtResource("65_y1rwn")
|
||||
|
||||
[node name="Highlight" type="TextureRect" parent="Stage/Credits/Logo" unique_id=1035898922]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
texture = ExtResource("64_rur7c")
|
||||
|
||||
[node name="Control" type="Control" parent="Stage/Credits" unique_id=1835435348]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ItemAmountUpdate" type="Timer" parent="Stage" unique_id=1953928024]
|
||||
wait_time = 0.2
|
||||
autostart = true
|
||||
|
||||
Reference in New Issue
Block a user