6ee66a627d
- Enhanced the `civilization_evolution.gd` script to improve population handling and resource allocation during civilization transitions. - Introduced new functions in `civilization_state.gd` for profile normalization and merging, ensuring consistent population representation across stages. - Updated `evolution_coordinator.gd` to incorporate budget phases for better tracking of simulation performance. - Improved `frame_budget.gd` to monitor maximum slice durations and phases during processing. - Added a new benchmarking tool in `android_benchmark.gd` to measure performance metrics during simulation runs, including frame processing times and throughput. - Created associated scene and resource files for the benchmarking tool to facilitate testing and performance evaluation.
284 lines
16 KiB
GDScript
284 lines
16 KiB
GDScript
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 empty_profile() -> Dictionary:
|
|
var result: Dictionary = {}
|
|
for ethic in ETHICS: result[ethic] = 1.0 / ETHICS.size()
|
|
return result
|
|
|
|
static func normalize_profile(profile: Dictionary) -> Dictionary:
|
|
var result: Dictionary = {}
|
|
var total: float = 0.0
|
|
for ethic in ETHICS:
|
|
var value: float = maxf(0.0, E.approx(profile.get(ethic, 0.0)))
|
|
result[ethic] = value
|
|
total += value
|
|
if not is_finite(total) or total <= 0.0: return empty_profile()
|
|
if profile.size() == ETHICS.size() and absf(total - 1.0) <= 0.000000000001:
|
|
return result
|
|
var assigned: float = 0.0
|
|
for i in ETHICS.size() - 1:
|
|
var ethic: String = ETHICS[i]
|
|
result[ethic] = (0.0 + result[ethic]) / total
|
|
assigned += result[ethic]
|
|
result[ETHICS.back()] = maxf(0.0, 1.0 - assigned)
|
|
return result
|
|
|
|
static func profile_from_counts(population: Dictionary) -> Dictionary:
|
|
var total: Variant = QuantityMath.sum(population)
|
|
if E.le(total, 0): return empty_profile()
|
|
var profile: Dictionary = {}
|
|
for ethic in ETHICS:
|
|
profile[ethic] = E.approx(E.divide(population.get(ethic, 0), total))
|
|
return normalize_profile(profile)
|
|
|
|
static func materialize_profile(target: Dictionary, id: String, profile: Dictionary, count: Variant) -> Dictionary:
|
|
var normalized: Dictionary = normalize_profile(profile)
|
|
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
|
var shares: Array = []
|
|
for ethic in ETHICS: shares.append(normalized[ethic])
|
|
var result: Dictionary = QuantityMath.split_profile(target.civilization_remainders, "profile:" + id, count, ETHICS, shares)
|
|
for ethic in ETHICS:
|
|
if not result.has(ethic): result[ethic] = 0
|
|
return result
|
|
|
|
static func merge_profile(base: Dictionary, base_count: Variant, incoming: Dictionary) -> Dictionary:
|
|
var incoming_count: Variant = QuantityMath.sum(incoming)
|
|
return merge_profiles(base, base_count, profile_from_counts(incoming), incoming_count)
|
|
|
|
static func merge_profiles(base: Dictionary, base_count: Variant, incoming_profile: Dictionary, incoming_count: Variant) -> Dictionary:
|
|
var total: Variant = E.add(base_count, incoming_count)
|
|
if E.le(total, 0): return empty_profile()
|
|
var base_ratio: float = E.approx(E.divide(base_count, total)) if E.gt(base_count, 0) else 0.0
|
|
var incoming_ratio: float = E.approx(E.divide(incoming_count, total)) if E.gt(incoming_count, 0) else 0.0
|
|
var normalized_base: Dictionary = normalize_profile(base)
|
|
var result: Dictionary = {}
|
|
for ethic in ETHICS:
|
|
var incoming_share: float = E.approx(incoming_profile.get(ethic, 0.0)) if E.gt(incoming_count, 0) else 0.0
|
|
result[ethic] = (0.0 + normalized_base[ethic]) * base_ratio + incoming_share * incoming_ratio
|
|
return normalize_profile(result)
|
|
|
|
static func is_space_civilization(id: String, rules: Dictionary) -> bool:
|
|
return (rules.has(id) and E.ne(str(rules[id].home), ""))
|
|
|
|
# Keep one six-value float profile per stage. Absolute population remains in
|
|
# the exact physical object ledger and is materialized only when a transition needs it.
|
|
static func reconcile(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}, only_ids: Array = []) -> void:
|
|
if (not target.has("ethics")):
|
|
target.ethics = {}
|
|
if physical.is_empty():
|
|
physical = world.ledger.physical_amounts(target)
|
|
var ids: Variant = rules if only_ids.is_empty() else only_ids
|
|
for id in ids:
|
|
if not rules.has(id): continue
|
|
var count: Variant = physical.get(id, 0)
|
|
var current: Dictionary = normalize_profile(target.ethics.get(id, {}))
|
|
var cached: Dictionary = world.ledger.reconciled_ethics.get(id, {})
|
|
if not cached.is_empty() and E.eq(cached.count, count) and E.eq(cached.population, current):
|
|
continue
|
|
target.ethics[id] = current
|
|
world.ledger.reconciled_ethics[id] = {"count": count, "population": current.duplicate()}
|
|
|
|
static func detach_space_civilizations(target: Dictionary, rules: Dictionary) -> void:
|
|
for held : Dictionary in target.binds.values():
|
|
for id in held:
|
|
if is_space_civilization(id, rules):
|
|
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.
|
|
static func stellar_budget(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
|
|
if physical.is_empty():
|
|
physical = world.ledger.physical_amounts(target)
|
|
var layout: Dictionary = world.catalog.civilization_layout(rules)
|
|
var capacity: Dictionary = {}
|
|
for id in layout.star_homes:
|
|
for home in layout.star_homes[id]:
|
|
capacity[home] = E.add(capacity.get(home, 0), physical.get(id, 0))
|
|
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)):
|
|
continue
|
|
populations[id] = physical.get(id, 0)
|
|
var home: String = rules[id].home
|
|
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] = 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, physical)}
|
|
|
|
# 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: Variant) -> Variant:
|
|
if (E.eq(source, destination) or (not is_space_civilization(destination, rules))):
|
|
return requested
|
|
var home: String = rules[destination].home
|
|
var cost: Variant = E.maximum(1, rules[destination].stars)
|
|
var old_home: String = ""
|
|
var old_cost: Variant = 0
|
|
if is_space_civilization(source, rules):
|
|
old_home = rules[source].home
|
|
old_cost = E.maximum(1, rules[source].stars)
|
|
# Existing overcapacity is handled by the separate population policy.
|
|
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: 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: Variant) -> void:
|
|
var profile_started: int = world.profile_start()
|
|
detach_space_civilizations(target, rules)
|
|
var physical: Dictionary = world.ledger.physical_amounts(target)
|
|
reconcile(world, target, rules, physical)
|
|
var ethical_totals: Dictionary = physical.duplicate()
|
|
var budget: Dictionary = stellar_budget(world, target, rules, physical)
|
|
var remaining: Dictionary = budget.capacity.duplicate()
|
|
var remaining_homes: Dictionary = budget.habitats.capacity.duplicate()
|
|
var stages: Array = world.catalog.civilization_layout(rules).stages
|
|
var changed: bool = false
|
|
for index in range(E.subtract(stages.size(), 1), E.neg(1), E.neg(1)):
|
|
var id: String = stages[index]
|
|
var count: Variant = target.amount.get(id, 0)
|
|
if E.le(count, 0): continue
|
|
var home: String = rules[id].home
|
|
var cost: Variant = E.maximum(1, rules[id].stars)
|
|
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 = 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
|
|
changed = true
|
|
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 previous_count: Variant = ethical_totals.get(previous, physical.get(previous, target.amount.get(previous, 0)))
|
|
var previous_profile: Dictionary = target.ethics.get(previous, empty_profile())
|
|
var source_profile: Dictionary = target.ethics.get(id, empty_profile())
|
|
target.ethics[previous] = merge_profiles(previous_profile, previous_count, source_profile, excess)
|
|
ethical_totals[id] = supported
|
|
ethical_totals[previous] = E.add(previous_count, excess)
|
|
# Capacity losses are proportional, so the surviving source keeps its profile.
|
|
if E.eq(previous, "C1.9"):
|
|
var stars: Dictionary = {}
|
|
var current_physical: Dictionary = world.ledger.physical_amounts(target)
|
|
for star in current_physical:
|
|
if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(current_physical[star], 0)):
|
|
stars[star] = current_physical[star]
|
|
var weights: Dictionary = QuantityMath.normalize_distribution(stars)
|
|
# Worlds are aggregates: a star may host several inhabited planets.
|
|
var assigned: Variant = 0
|
|
var owners: Array = weights.keys()
|
|
owners.sort()
|
|
for i in owners.size():
|
|
var owner: String = owners[i]
|
|
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] = E.add(target.binds[owner].get(previous, 0), amount)
|
|
target.amount[previous] = E.subtract(target.amount[previous], amount)
|
|
if changed:
|
|
physical = world.ledger.physical_amounts(target)
|
|
reconcile(world, target, rules, physical)
|
|
assign_homes(world, target, rules, physical)
|
|
world.profile_end("enforce_capacity", profile_started)
|
|
|
|
# Unity's structure chain: direct Claim resets the required structure count;
|
|
# intervening stages require ten times more units of the preceding structure.
|
|
static func habitat_budget(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
|
|
if physical.is_empty():
|
|
physical = world.ledger.physical_amounts(target)
|
|
var layout: Dictionary = world.catalog.civilization_layout(rules)
|
|
var structures: Array = layout.structures
|
|
var capacity: Dictionary = {}
|
|
for id in structures: capacity[id] = physical.get(id, 0)
|
|
var requirements: Dictionary = layout.requirements
|
|
var anchors: Dictionary = layout.anchors
|
|
var requested: Dictionary = {}
|
|
for id in requirements:
|
|
var req: Dictionary = requirements[id]
|
|
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}
|
|
|
|
# A structure's direct reservation belongs only to its Claim stage. Other
|
|
# stages do not debit it, so each stage's placement can be calculated alone.
|
|
static func stage_homes(world, target: Dictionary, id: String, layout: Dictionary, capacity: Dictionary = {}) -> Dictionary:
|
|
var left: Variant = target.amount.get(id, 0)
|
|
var homes: Dictionary = {}
|
|
if E.le(left, 0): return homes
|
|
var allowed: bool = layout.anchors[id] == ""
|
|
for structure in layout.structures:
|
|
if structure == layout.anchors[id]: allowed = true
|
|
if not allowed: continue
|
|
var stock: Variant = capacity.get(structure, 0) if not capacity.is_empty() else world.ledger.physical_amount(target, structure)
|
|
if E.le(stock, 0): continue
|
|
var amount: Variant = left
|
|
if world.catalog.entities[structure].claim == id:
|
|
amount = E.minimum(amount, stock)
|
|
if E.gt(amount, 0): homes[structure] = amount
|
|
left = E.subtract(left, amount)
|
|
if E.le(left, 0): break
|
|
return homes
|
|
|
|
static func assign_homes(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> void:
|
|
var profile_started: int = world.profile_start()
|
|
var layout: Dictionary = world.catalog.civilization_layout(rules)
|
|
var capacity: Dictionary = {}
|
|
for structure in layout.structures:
|
|
capacity[structure] = world.ledger.physical_amount(target, structure) if physical.is_empty() else physical.get(structure, 0)
|
|
target.civilization_homes = {}
|
|
for index in range(layout.stages.size() - 1, -1, -1):
|
|
var id: String = layout.stages[index]
|
|
target.civilization_homes[id] = stage_homes(world, target, id, layout, capacity)
|
|
world.profile_end("assign_homes", profile_started)
|
|
|
|
|
|
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
|
|
var population: Variant = target.amount.get(id, 0)
|
|
if E.le(population, 0): return base
|
|
var homes: Dictionary = stage_homes(world, target, id, world.catalog.civilization_layout(rules))
|
|
target.civilization_homes[id] = homes
|
|
var weighted: Variant = population
|
|
for home in homes:
|
|
weighted = E.add(weighted, E.multiply(homes[home], (E.subtract(E.maximum(0, world.catalog.entities[home].civ_bonus), 1))))
|
|
return (E.mul_div(base, population, weighted) if E.gt(weighted, 0) else base)
|