Refactor production logic to incorporate helium share calculations and lifespan multipliers for stellar entities

This commit is contained in:
Kirill
2026-10-17 23:51:08 +03:00
parent 273093bd9e
commit f53181b9ad
4 changed files with 100 additions and 15 deletions
+10 -2
View File
@@ -232,10 +232,18 @@ CSVLoader разбирает строки, колонка `param export` пре
Начальная энергия относится к новым вселенным; запас энергии существующей
вселенной остаётся частью её сохранения.
G, C, PI, helium_multiplier, helium_bonus_limit, space_generation, ChatPerTick
и параметры ForceBox загружаются, но пока не используются в GI: соответствующие
G, C, space_generation, ChatPerTick и параметры ForceBox загружаются, но пока не
используются в GI: соответствующие
формулы и механики старой Unity-версии здесь отсутствуют. Производство объектов
в GI задаётся их собственной таблицей life_product и lifespan.
Для звёзд с прямой стоимостью H доля He в свободной смеси H+He заменяет часть
водородной стоимости. Применяются `helium_multiplier` и `helium_bonus_limit`, а
замещение ограничено 50%, как в Unity. Недостающий He не блокирует создание:
непокрытая часть оплачивается H. При создании таких звёзд срок жизни
умножается на `1 − 1.5 × доля замещения`; тот же коэффициент задаёт сроки их
партий. Автоматическое создание использует свободный He, а бюджет автоматического
H списывает только оставшуюся после замещения водородную часть.
`tests/test_extra_params.gd` проверяет загрузку точных чисел и изменение энергии,
плотности, объёма пустот, порогов индикатора и бонуса горючести.
+22 -7
View File
@@ -72,6 +72,7 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
# One common multiplier preserves the initial count ratios, even under scarcity.
var candidates: Dictionary = {}
var helium_share: Variant = world.production.stellar_helium_share(previous)
for id in distribution:
var entity: OwniverseEntity = world.catalog.entities[id]
if E.ne(entity.group_as, "Star"):
@@ -87,11 +88,16 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
return
var costs: Dictionary = {}
var needs: Dictionary = {}
var stellar_hydrogen_cost: Variant = 0
for id in candidates:
if E.le(auto_capacity(target, world.catalog.entities[id], budget), 0):
var entity: OwniverseEntity = world.catalog.entities[id]
if E.le(auto_capacity(target, entity, budget, helium_share), 0):
continue
for resource in entity.cost:
var cost: Variant = E.multiply(E.maximum(0, entity.cost[resource]), candidates[id])
if E.eq(resource, "H") and world.production.uses_stellar_helium(entity):
stellar_hydrogen_cost = E.add(stellar_hydrogen_cost, cost)
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:
@@ -100,6 +106,9 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
for resource in costs:
if E.gt(costs[resource], 0):
scale = E.minimum(scale, E.divide(budget.get(resource, 0), costs[resource]))
if E.gt(stellar_hydrogen_cost, 0):
var hydrogen_budget: Variant = E.minimum(budget.get("H", 0), world.ledger.available(target, "H"))
scale = E.minimum(scale, world.production.stellar_hydrogen_capacity(stellar_hydrogen_cost, hydrogen_budget, world.ledger.available(target, "He"), helium_share))
for resource in needs:
if E.gt(needs[resource], 0):
scale = E.minimum(scale, E.divide(world.ledger.available(target, resource), needs[resource]))
@@ -126,10 +135,10 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
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))
var actual: Variant = E.minimum(desired, auto_capacity(target, entity, budget, helium_share))
if E.le(actual, 0):
continue
if (not world.production.try_production(target, entity, actual, true, current_quantities)):
if (not world.production.try_production(target, entity, actual, true, current_quantities, helium_share)):
continue
budget = ordinary_star_budget(target)
created[id] = actual
@@ -139,7 +148,8 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
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)
var lifespan_multiplier: Variant = world.production.stellar_lifespan_multiplier(world.catalog.entities[id], helium_share)
world.batches.append_batch(target, id, created[id], from_year, to_year, {}, -INF, null, null, NAN, lifespan_multiplier)
world.stellar.create_planets(target, id, created[id], from_year, to_year, metallicity)
@@ -183,11 +193,16 @@ func ordinary_star_budget(target: Dictionary) -> Dictionary:
return budget
func auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Dictionary) -> Variant:
func auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Dictionary, helium_share_override: Variant = NAN) -> Variant:
var capacity: Variant = INF
var needs: Dictionary = {}
var helium_share: Variant = world.production.stellar_helium_share(source) if not E.finite(helium_share_override) else helium_share_override
for resource in entity.cost:
var cost: Variant = E.maximum(0, entity.cost[resource])
if E.eq(resource, "H") and world.production.uses_stellar_helium(entity):
var hydrogen_budget: Variant = E.minimum(budget.get("H", 0), world.ledger.available(source, "H"))
capacity = E.minimum(capacity, world.production.stellar_hydrogen_capacity(cost, hydrogen_budget, world.ledger.available(source, "He"), helium_share))
continue
needs[resource] = cost
if E.gt(cost, 0):
capacity = E.minimum(capacity, E.floor_value(E.divide(budget.get(resource, 0), cost)))
+66 -4
View File
@@ -3,7 +3,7 @@ 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: Variant, automatic: bool = false, physical: Dictionary = {}) -> bool:
func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant, automatic: bool = false, physical: Dictionary = {}, helium_share_override: Variant = NAN) -> bool:
if (E.le(amount, 0) or E.ne(amount, E.floor_value(amount))):
return false
var trial: Dictionary = target.duplicate()
@@ -17,8 +17,21 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant
if not shared_physical: physical = world.ledger.physical_amounts(trial)
var next_physical: Dictionary = physical.duplicate() if shared_physical else {}
var copied_batches: Dictionary = {}
var helium_share: Variant = stellar_helium_share(target) if not E.finite(helium_share_override) else helium_share_override
for resource in entity.cost:
var cost: Variant = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount))
if E.eq(resource, "H") and uses_stellar_helium(entity):
var helium_cost: Variant = E.minimum(E.floor_value(E.multiply(cost, helium_share)), world.ledger.available(trial, "He"))
cost = E.subtract(cost, helium_cost)
if E.gt(helium_cost, 0):
var helium_taken: Dictionary = world.ledger.take_available(trial, "He", helium_cost)
var total_helium_taken: Variant = 0
for id in helium_taken:
total_helium_taken = E.add(total_helium_taken, helium_taken[id])
if shared_physical and next_physical.has(id):
next_physical[id] = E.subtract(next_physical[id], helium_taken[id])
if E.ne(total_helium_taken, helium_cost):
return false
if (automatic and E.lt(trial.auto_resources.get(resource, 0), cost)):
return false
var taken: Dictionary = world.ledger.take_available(trial, resource, cost)
@@ -147,12 +160,14 @@ func produce(item_id: String, desired_amount: Variant) -> Variant:
return 0
var metallicity: Variant = world.environment.stellar_environment(world.state).metallicity
if (not try_production(world.state, entity, max_producible)):
var helium_share: Variant = stellar_helium_share(world.state)
if (not try_production(world.state, entity, max_producible, false, {}, helium_share)):
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)
var lifespan_multiplier: Variant = stellar_lifespan_multiplier(entity, helium_share)
world.batches.append_batch(world.state, item_id, max_producible, world.state.time.datetime, world.state.time.datetime, {}, -INF, null, null, NAN, lifespan_multiplier)
world.stellar.create_planets(world.state, item_id, max_producible, world.state.time.datetime, world.state.time.datetime, metallicity)
world.ledger.refresh_groups(world.state)
world.presentation.refresh(world.state, world.catalog)
@@ -179,6 +194,39 @@ func production_step(entity: OwniverseEntity) -> Variant:
return QuantityMath.production_step(E.add(entity.cost.values(), entity.bind.values()))
func uses_stellar_helium(entity: OwniverseEntity) -> bool:
return E.eq(entity.group_as, "Star") and entity.cost.has("H")
## Unity's Hel state is the free helium share of the H+He mixture, capped at 50%.
func stellar_helium_share(source: Dictionary) -> Variant:
var hydrogen: Variant = world.ledger.available(source, "H")
var helium: Variant = world.ledger.available(source, "He")
var total: Variant = E.add(hydrogen, helium)
if E.le(total, 0): return 0
var parameters: Dictionary = Global.ExtraParams
var share: Variant = E.multiply(E.divide(helium, total), parameters.get("helium_multiplier", 1))
share = E.minimum(share, E.of("0.5"))
share = E.minimum(share, E.maximum(0, parameters.get("helium_bonus_limit", 1)))
return E.clamp_value(share, 0, 1)
func stellar_lifespan_multiplier(entity: OwniverseEntity, helium_share: Variant) -> Variant:
return E.maximum(0, E.subtract(1, E.multiply(E.of("1.5"), helium_share))) if uses_stellar_helium(entity) else 1
## Maximum star count when helium may replace this share of its hydrogen cost.
func stellar_hydrogen_capacity(hydrogen_cost: Variant, hydrogen_available: Variant, helium_available: Variant, helium_share: Variant) -> Variant:
if E.le(hydrogen_cost, 0): return INF
if E.le(helium_share, 0) or E.le(helium_available, 0):
return E.floor_value(E.divide(hydrogen_available, hydrogen_cost))
var hydrogen_only_capacity: Variant = E.divide(hydrogen_available, E.multiply(hydrogen_cost, E.subtract(1, helium_share)))
var helium_saturation_capacity: Variant = E.divide(helium_available, E.multiply(hydrogen_cost, helium_share))
if E.le(hydrogen_only_capacity, helium_saturation_capacity):
return E.floor_value(hydrogen_only_capacity)
return E.floor_value(E.divide(E.add(hydrogen_available, helium_available), hydrogen_cost))
func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant = null) -> Variant:
if E.eq(entity.id, "SMBH"):
return plan_smbh(smbh_available() if desired_amount == null else desired_amount).amount
@@ -196,10 +244,24 @@ func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant =
if (not has_requirements):
return 0
var available: Variant = INF if desired_amount == null else E.maximum(0, desired_amount)
var helium_share: Variant = stellar_helium_share(world.state)
for resource in requirements:
if E.eq(resource, "H") and uses_stellar_helium(entity):
var hydrogen_cost: Variant = E.maximum(0, entity.cost.get("H", 0))
available = E.minimum(available, stellar_hydrogen_capacity(hydrogen_cost, world.ledger.available(world.state, "H"), world.ledger.available(world.state, "He"), helium_share))
var hydrogen_binding: Variant = E.maximum(0, entity.bind.get("H", 0))
if E.gt(hydrogen_binding, 0):
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, "H"), hydrogen_binding)))
continue
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)
var concrete_requirements: Dictionary = requirements.duplicate()
if uses_stellar_helium(entity):
# The direct H cost was already handled by the H/He capacity above;
# preserve any separate H binding requirement.
concrete_requirements["H"] = E.maximum(0, E.subtract(concrete_requirements.get("H", 0), entity.cost.get("H", 0)))
if E.le(concrete_requirements["H"], 0): concrete_requirements.erase("H")
var concrete: Dictionary = world.ledger.expand_requirements(world.state, concrete_requirements)
for resource in concrete:
if E.gt(concrete[resource], 0):
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), concrete[resource])))
+2 -2
View File
@@ -258,7 +258,7 @@ func scale_batches(target: Dictionary, id: String, factor: Variant) -> void:
share.batch.withdraw_remainder = E.minimum(share.batch.get("withdraw_remainder", 0), share.batch.amount)
func append_batch(target: Dictionary, id: String, amount: Variant, created_from: Variant, created_to: Variant, completed: Dictionary = { }, completed_before: Variant = - INF, budget: RefCounted = null, newborn_adjustment: Variant = null, newborn_until: Variant = NAN) -> Variant:
func append_batch(target: Dictionary, id: String, amount: Variant, created_from: Variant, created_to: Variant, completed: Dictionary = { }, completed_before: Variant = - INF, budget: RefCounted = null, newborn_adjustment: Variant = null, newborn_until: Variant = NAN, lifespan_multiplier: Variant = 1) -> Variant:
var _keep_alive: RefCounted = self
if Global.CivilizationRules.has(id): return NAN
if ((E.le(amount, 0) or (not world.catalog.entities.has(id))) or E.le(world.catalog.entities[id].lifespan, 0)):
@@ -271,7 +271,7 @@ func append_batch(target: Dictionary, id: String, amount: Variant, created_from:
if (not target.has("batch_origins")):
target.batch_origins = {}
var batches: Array = target.batches[id]
var lifespan: Variant = effective_lifespan(target, id)
var lifespan: Variant = E.multiply(effective_lifespan(target, id), E.maximum(0, lifespan_multiplier))
if uses_cycle_star(target, id, lifespan):
return await append_cycle_star(target, id, amount, created_from, created_to, lifespan, completed, completed_before, budget, newborn_adjustment, newborn_until)
var batch_duration: Variant = window_duration(target, lifespan)