Files
owniverse/world/owniverse/manual_production.gd
T

276 lines
14 KiB
GDScript

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: 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()
trial.amount = target.amount.duplicate()
trial.binds = target.binds.duplicate(true)
trial.auto_resources = target.auto_resources.duplicate()
trial.batches = target.batches.duplicate()
# Physical object totals do not change while resources are withdrawn from
# this transaction. Keep one snapshot instead of scanning every resource.
var shared_physical: bool = not physical.is_empty()
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)
var total: Variant = 0
for quantity in taken.values():
total = E.add(total, quantity)
if E.ne(total, cost):
return false
if automatic:
trial.auto_resources[resource] = E.maximum(0, E.subtract(trial.auto_resources.get(resource, 0), cost))
for id in taken:
if shared_physical and next_physical.has(id): next_physical[id] = E.subtract(next_physical[id], taken[id])
# Isolate only batches actually touched by this transaction.
if (trial.batches.has(id) and (not copied_batches.has(id))):
trial.batches[id] = trial.batches[id].duplicate(true)
copied_batches[id] = true
var factor: Variant = (E.subtract(1, E.divide(taken[id], physical[id])) if E.gt(physical.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)):
return false
world.ledger.refresh_groups(trial)
world.ledger.clamp_auto_resources(trial)
for field in ["amount", "binds", "batches", "auto_resources"]:
if trial.has(field):
target[field] = trial[field]
if shared_physical: physical.merge(next_physical, true)
return true
func smbh_sources() -> Dictionary:
var masses: Dictionary = {}
for id in world.catalog.entities:
var entity: OwniverseEntity = world.catalog.entities[id]
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() -> Variant:
var mass: Variant = 0
for value in smbh_sources().values():
mass = E.add(mass, value)
return E.floor_value(mass)
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: Variant = smbh_available()
if E.le(total_mass, 0):
return empty
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: Variant = 0
for id in distribution:
var expected: Variant = E.mul_div(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 E.ge(consumed_mass, requested):
break
var id: String = candidate.id
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: Variant) -> Variant:
if E.gt(world.state.get("apocalypse", {}).get("phase", 0), 0): return 0
var plan: Dictionary = plan_smbh(desired_amount)
var consumed_mass: Variant = plan.amount
var taken: Dictionary = plan.taken
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 E.le(taken[id], 0):
continue
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: 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)
world.ledger.refresh_groups(next)
world.ledger.clamp_auto_resources(next)
world.state = next
world.presentation.refresh(world.state, world.catalog)
world.apocalypse.check_density(world.state)
return consumed_mass
func produce(item_id: String, desired_amount: Variant) -> Variant:
if E.gt(world.state.get("apocalypse", {}).get("phase", 0), 0): return 0
if E.eq(item_id, "SMBH"):
return produce_smbh(desired_amount)
# Use special production for S and 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 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 E.le(max_producible, 0):
return 0
var metallicity: Variant = world.environment.stellar_environment(world.state).metallicity
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)
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)
world.apocalypse.check_density(world.state)
return max_producible
func produce_hs(entity_id: String, value: Variant) -> Variant:
if E.gt(world.state.get("apocalypse", {}).get("phase", 0), 0): return 0
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)
world.presentation.refresh(world.state, world.catalog)
world.apocalypse.check_density(world.state)
return to_produce
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
var requirements: Dictionary = {}
for resource in entity.cost:
requirements[resource] = E.maximum(0, entity.cost[resource])
for resource in entity.bind:
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 E.gt(quantity, 0):
has_requirements = true
break
# No recipe means unavailable for production, not unlimited free objects.
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_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])))
var step: Variant = production_step(entity)
return (E.multiply(E.floor_value(E.divide(available, step)), step) if E.finite(step) else 0)