c4e7302567
- Introduced `civilization_benchmark.gd` and `legacy_quantity_math.gd` for benchmarking and legacy quantity calculations. - Created `test_dynamic_math.gd`, `test_frame_evolution.gd`, and `test_sparse_batches.gd` to validate mathematical operations and frame evolution logic. - Implemented `frame_budget.gd` to manage cooperative work on the main thread, ensuring no partially calculated state is published. - Added UID files for all new scripts to maintain unique identifiers.
226 lines
13 KiB
GDScript
226 lines
13 KiB
GDScript
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 E.gt(entity.lifespan, 0)):
|
|
for resource in entity.life_product:
|
|
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: Variant, to_year: Variant) -> void:
|
|
var basic: Dictionary = {}
|
|
var automatic: Dictionary = {}
|
|
for id in quantities:
|
|
var entity: OwniverseEntity = world.catalog.entities[id]
|
|
var products: Dictionary = yearly_products(entity)
|
|
if products.is_empty(): continue
|
|
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)
|
|
for product in products:
|
|
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")):
|
|
target.resource_remainders = {"basic": {}, "automatic": {}}
|
|
var pending: Dictionary = target.resource_remainders
|
|
var requests: Dictionary = {}
|
|
for id in basic:
|
|
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] = E.maximum(0, E.add(pending.basic.get(id, 0), pending.automatic.get(id, 0)))
|
|
for id in totals:
|
|
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: 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: 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)
|
|
|
|
# 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] = E.add(budget.get(id, 0), automatic[id])
|
|
for id in budget:
|
|
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), 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 E.ne(entity.group_as, "Star"):
|
|
continue
|
|
var has_cost: bool = false
|
|
for resource in entity.cost:
|
|
if E.gt(entity.cost[resource], 0):
|
|
has_cost = true
|
|
if has_cost:
|
|
candidates[id] = distribution[id]
|
|
candidates = QuantityMath.normalize_distribution(candidates)
|
|
if candidates.is_empty():
|
|
return
|
|
var costs: Dictionary = {}
|
|
var needs: Dictionary = {}
|
|
for id in candidates:
|
|
if E.le(auto_capacity(target, world.catalog.entities[id], budget), 0):
|
|
continue
|
|
for resource in world.catalog.entities[id].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] = 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 E.gt(costs[resource], 0):
|
|
scale = E.minimum(scale, E.divide(budget.get(resource, 0), costs[resource]))
|
|
for resource in needs:
|
|
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 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")):
|
|
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 (E.lt(candidates[a], candidates[b]) if E.ne(candidates[a], candidates[b]) else E.lt(a, b)))
|
|
var created: Dictionary = {}
|
|
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: Variant = world.production.production_step(entity)
|
|
if (not E.finite(step)):
|
|
continue
|
|
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)):
|
|
continue
|
|
budget = ordinary_star_budget(target)
|
|
created[id] = 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] = 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: 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: Variant = entity.cost.get("H", 0)
|
|
if E.le(cost, 0):
|
|
return
|
|
var quota: Variant = E.add(target.auto_star_deficits.get("HBD", 0), E.mul_div(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 (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 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 = 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
|
|
|
|
|
|
func ordinary_star_budget(target: Dictionary) -> Dictionary:
|
|
var budget: Dictionary = target.auto_resources.duplicate()
|
|
if world.catalog.entities.has("HBD"):
|
|
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) -> Variant:
|
|
var capacity: Variant = INF
|
|
var needs: Dictionary = {}
|
|
for resource in entity.cost:
|
|
var cost: Variant = E.maximum(0, entity.cost[resource])
|
|
needs[resource] = 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] = E.add(needs.get(resource, 0), E.maximum(0, entity.bind[resource]))
|
|
for resource in needs:
|
|
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 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: 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 E.le(count, 0)):
|
|
return
|
|
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: 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 coefficient: Variant = E.multiply(products[resource], WorldParameters.production(target, world.catalog.entities[id]))
|
|
var output: Variant = E.multiply(object_years, coefficient)
|
|
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))
|