From f53181b9adcf7c39f54716a34471d187c9e6434f Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 17 Oct 2026 23:51:08 +0300 Subject: [PATCH] Refactor production logic to incorporate helium share calculations and lifespan multipliers for stellar entities --- simulation_rules.md | 12 ++++- world/owniverse/automatic_production.gd | 29 +++++++--- world/owniverse/manual_production.gd | 70 +++++++++++++++++++++++-- world/owniverse/object_batches.gd | 4 +- 4 files changed, 100 insertions(+), 15 deletions(-) diff --git a/simulation_rules.md b/simulation_rules.md index d28716f..a96f511 100644 --- a/simulation_rules.md +++ b/simulation_rules.md @@ -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` проверяет загрузку точных чисел и изменение энергии, плотности, объёма пустот, порогов индикатора и бонуса горючести. diff --git a/world/owniverse/automatic_production.gd b/world/owniverse/automatic_production.gd index 67543a6..2a50065 100644 --- a/world/owniverse/automatic_production.gd +++ b/world/owniverse/automatic_production.gd @@ -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 world.catalog.entities[id].cost: - var cost: Variant = E.multiply(E.maximum(0, world.catalog.entities[id].cost[resource]), candidates[id]) + 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 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))) diff --git a/world/owniverse/manual_production.gd b/world/owniverse/manual_production.gd index 7df6e9f..874e16d 100644 --- a/world/owniverse/manual_production.gd +++ b/world/owniverse/manual_production.gd @@ -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]))) diff --git a/world/owniverse/object_batches.gd b/world/owniverse/object_batches.gd index e052ee2..9b3e70a 100644 --- a/world/owniverse/object_batches.gd +++ b/world/owniverse/object_batches.gd @@ -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)