26347fca30
- Introduced a new test script for civilization evolution (`test_civilization_evolution.gd`) to validate the evolution mechanics, including population dynamics, ethical transitions, and war impacts. - Updated `README.md` to include instructions for running the new civilization evolution tests. - Modified `test_civilization_state.gd` to accommodate changes in civilization rules and ensure proper validation of ethical stages. - Enhanced `test_descriptions.gd` to check for accurate population counters and state validation. - Adjusted `test_life.gd` to reflect updates in life version management. - Improved `civilization_state.gd` to include habitat budget calculations and transition management. - Added `civilization_evolution.gd` to handle the logic for civilization evolution, including population drift, warfare, and colonization mechanics. - Updated `owniverse.gd` and `owniverse_entity.gd` to integrate new evolution logic and ensure proper initialization of entities.
236 lines
12 KiB
GDScript
236 lines
12 KiB
GDScript
extends RefCounted
|
|
|
|
const ETHICS = ["m", "l", "w", "d", "c", "b"]
|
|
|
|
static func is_space_civilization(id: String, rules: Dictionary) -> bool:
|
|
return rules.has(id) and str(rules[id].home) != ""
|
|
|
|
# Preserve any existing ethics. New populations start equally distributed;
|
|
# reductions (including blasts) are proportional and never leave negative counts.
|
|
static func reconcile(world, target: Dictionary, rules: Dictionary) -> void:
|
|
if not target.has("ethics"):
|
|
target.ethics = {}
|
|
var physical: Dictionary = world._physical_amounts(target)
|
|
for id in rules:
|
|
var count: float = physical.get(id, 0.0)
|
|
var current: Dictionary = target.ethics.get(id, {})
|
|
var total: float = 0.0
|
|
for ethic in ETHICS:
|
|
current[ethic] = maxf(0.0, current.get(ethic, 0.0))
|
|
total += current[ethic]
|
|
if total > count:
|
|
current = world._allocate_whole(current, count)
|
|
elif total < count:
|
|
var missing: float = count - total
|
|
var base: float = floorf(missing / ETHICS.size())
|
|
var rest: int = int(missing - base * ETHICS.size())
|
|
for i in ETHICS.size():
|
|
current[ETHICS[i]] += base + (1.0 if i < rest else 0.0)
|
|
target.ethics[id] = current
|
|
|
|
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] = target.amount.get(id, 0.0) + held[id]
|
|
held[id] = 0.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) -> Dictionary:
|
|
var physical: Dictionary = world._physical_amounts(target)
|
|
var capacity: Dictionary = {}
|
|
for id in physical:
|
|
var entity = world.entities[id]
|
|
for home in ["DwStar", "GiStar", "WD", "NS", "MGT", "BH"]:
|
|
if entity.event_group == home or entity.count_as == home or id == home:
|
|
capacity[home] = capacity.get(home, 0.0) + physical[id]
|
|
capacity["SMBH"] = maxf(0.0, target.amount.get("SMBH", 0.0))
|
|
for held: Dictionary in target.binds.values():
|
|
capacity["SMBH"] += maxf(0.0, held.get("SMBH", 0.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.0)
|
|
var home: String = rules[id].home
|
|
requested[home] = requested.get(home, 0.0) + populations[id] * maxf(1.0, rules[id].stars)
|
|
var free: Dictionary = {}
|
|
for home in capacity:
|
|
free[home] = maxf(0.0, capacity[home] - requested.get(home, 0.0))
|
|
return {"capacity": capacity, "requested": requested, "free": free, "populations": populations, "habitats": habitat_budget(world, target, rules)}
|
|
|
|
# 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: float) -> float:
|
|
if source == destination or not is_space_civilization(destination, rules):
|
|
return requested
|
|
var home: String = rules[destination].home
|
|
var cost: float = maxf(1.0, rules[destination].stars)
|
|
var old_home: String = ""
|
|
var old_cost: float = 0.0
|
|
if is_space_civilization(source, rules):
|
|
old_home = rules[source].home
|
|
old_cost = maxf(1.0, rules[source].stars)
|
|
# Existing overcapacity is handled by the separate population policy.
|
|
if budget.requested.get(home, 0.0) > budget.capacity.get(home, 0.0):
|
|
return 0.0
|
|
var net_cost: float = cost - old_cost if old_home == home else cost
|
|
var admitted: float = floorf(maxf(0.0, requested))
|
|
if net_cost > 0.0:
|
|
admitted = minf(admitted, floorf(budget.free.get(home, 0.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: float = new_req.units - (old_req.units if old_req.get("id", "") == new_req.id else 0.0)
|
|
if net_units > 0.0:
|
|
var free_units: float = maxf(0.0, housing.capacity.get(new_req.id, 0.0) - housing.requested.get(new_req.id, 0.0))
|
|
admitted = minf(admitted, floorf(free_units / net_units))
|
|
if not old_req.is_empty(): housing.requested[old_req.id] = maxf(0.0, housing.requested.get(old_req.id, 0.0) - admitted * old_req.units)
|
|
if not new_req.is_empty(): housing.requested[new_req.id] = housing.requested.get(new_req.id, 0.0) + admitted * new_req.units
|
|
if old_home != "":
|
|
budget.requested[old_home] = maxf(0.0, budget.requested.get(old_home, 0.0) - admitted * old_cost)
|
|
budget.free[old_home] = maxf(0.0, budget.capacity.get(old_home, 0.0) - budget.requested[old_home])
|
|
budget.requested[home] = budget.requested.get(home, 0.0) + admitted * cost
|
|
budget.free[home] = maxf(0.0, budget.capacity.get(home, 0.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: float) -> void:
|
|
detach_space_civilizations(target, rules)
|
|
reconcile(world, target, rules)
|
|
var budget: Dictionary = stellar_budget(world, target, rules)
|
|
var remaining: Dictionary = budget.capacity.duplicate()
|
|
var remaining_homes: Dictionary = budget.habitats.capacity.duplicate()
|
|
var stages: Array = []
|
|
for id in rules:
|
|
if is_space_civilization(id, rules):
|
|
stages.append(id)
|
|
stages.sort()
|
|
for index in range(stages.size() - 1, -1, -1):
|
|
var id: String = stages[index]
|
|
var home: String = rules[id].home
|
|
var cost: float = maxf(1.0, rules[id].stars)
|
|
var count: float = target.amount.get(id, 0.0)
|
|
var supported: float = minf(count, floorf(remaining.get(home, 0.0) / cost))
|
|
var requirement: Dictionary = budget.habitats.requirements.get(id, {})
|
|
if not requirement.is_empty():
|
|
supported = minf(supported, floorf(remaining_homes.get(requirement.id, 0.0) / requirement.units))
|
|
remaining_homes[requirement.id] = maxf(0.0, remaining_homes.get(requirement.id, 0.0) - supported * requirement.units)
|
|
remaining[home] = maxf(0.0, remaining.get(home, 0.0) - supported * cost)
|
|
var excess: float = count - supported
|
|
if excess <= 0.0:
|
|
continue
|
|
var previous: String = stages[index - 1] if index > 0 else "C1.9"
|
|
world._scale_batches(target, id, supported / count)
|
|
target.amount[id] = supported
|
|
world._credit(target, previous, excess)
|
|
world._append_batch(target, previous, excess, year, year)
|
|
var removed: Dictionary = world._allocate_whole(target.ethics[id], excess)
|
|
if not target.ethics.has(previous):
|
|
target.ethics[previous] = {}
|
|
for ethic in ETHICS:
|
|
target.ethics[id][ethic] -= removed[ethic]
|
|
target.ethics[previous][ethic] = target.ethics[previous].get(ethic, 0.0) + removed[ethic]
|
|
if previous == "C1.9":
|
|
var stars: Dictionary = {}
|
|
var physical: Dictionary = world._physical_amounts(target)
|
|
for star in physical:
|
|
if world.entities[star].event_group == "DwStar" and physical[star] > 0.0:
|
|
stars[star] = physical[star]
|
|
var weights: Dictionary = world._normalize_distribution(stars)
|
|
# Worlds are aggregates: a star may host several inhabited planets.
|
|
var assigned: float = 0.0
|
|
var owners: Array = weights.keys()
|
|
owners.sort()
|
|
for i in owners.size():
|
|
var owner: String = owners[i]
|
|
var amount: float = floorf(excess * weights[owner]) if i + 1 < owners.size() else excess - assigned
|
|
assigned += amount
|
|
if not target.binds.has(owner):
|
|
target.binds[owner] = {}
|
|
target.binds[owner][previous] = target.binds[owner].get(previous, 0.0) + amount
|
|
target.amount[previous] -= amount
|
|
reconcile(world, target, rules)
|
|
assign_homes(world, target, rules)
|
|
|
|
# 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) -> Dictionary:
|
|
var physical: Dictionary = world._physical_amounts(target)
|
|
var structures: Array = []
|
|
var capacity: Dictionary = {}
|
|
for id in world.entities:
|
|
if world.entities[id].claim != "":
|
|
structures.append(id)
|
|
capacity[id] = physical.get(id, 0.0)
|
|
structures.sort_custom(func(a, b):
|
|
var left: float = world.entities[a].civ_bonus
|
|
var right: float = world.entities[b].civ_bonus
|
|
return left > right if left != right else str(a) < str(b))
|
|
var stages: Array = []
|
|
for id in rules:
|
|
if is_space_civilization(id, rules): stages.append(id)
|
|
stages.sort()
|
|
var requirements: Dictionary = {}
|
|
var anchors: Dictionary = {}
|
|
var anchor: String = ""
|
|
var units: float = 1.0
|
|
for id in stages:
|
|
var direct: String = ""
|
|
for structure in structures:
|
|
if world.entities[structure].claim == id: direct = structure
|
|
if direct != "":
|
|
anchor = direct
|
|
units = 1.0
|
|
else:
|
|
units *= 10.0
|
|
anchors[id] = anchor
|
|
if id.begins_with("C3.") or id.begins_with("C4."):
|
|
requirements[id] = {"id": anchor, "units": units}
|
|
var requested: Dictionary = {}
|
|
for id in requirements:
|
|
var req: Dictionary = requirements[id]
|
|
requested[req.id] = requested.get(req.id, 0.0) + physical.get(id, 0.0) * req.units
|
|
return {"capacity": capacity, "requested": requested, "requirements": requirements, "structures": structures, "anchors": anchors}
|
|
|
|
static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
|
|
var budget: Dictionary = habitat_budget(world, target, rules)
|
|
var used: Dictionary = {}
|
|
target.civilization_homes = {}
|
|
var stages: Array = budget.anchors.keys()
|
|
stages.sort()
|
|
stages.reverse()
|
|
for id in stages:
|
|
var left: float = target.amount.get(id, 0.0)
|
|
var homes: Dictionary = {}
|
|
var allowed: bool = budget.anchors[id] == ""
|
|
for structure in budget.structures:
|
|
if structure == budget.anchors[id]: allowed = true
|
|
if not allowed or budget.capacity[structure] <= 0.0: continue
|
|
var amount: float = left
|
|
if world.entities[structure].claim == id:
|
|
amount = minf(amount, maxf(0.0, budget.capacity[structure] - used.get(structure, 0.0)))
|
|
used[structure] = used.get(structure, 0.0) + amount
|
|
if amount > 0.0: homes[structure] = amount
|
|
left -= amount
|
|
if left <= 0.0: break
|
|
target.civilization_homes[id] = homes
|
|
|
|
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> float:
|
|
if not rules.has(id): return world.entities[id].lifespan
|
|
var base: float = rules[id].span
|
|
if not is_space_civilization(id, rules): return base
|
|
assign_homes(world, target, rules)
|
|
var population: float = target.amount.get(id, 0.0)
|
|
if population <= 0.0: return base
|
|
var weighted: float = population
|
|
for home in target.civilization_homes.get(id, {}):
|
|
weighted += target.civilization_homes[id][home] * (maxf(0.0, world.entities[home].civ_bonus) - 1.0)
|
|
return base * population / weighted if weighted > 0.0 else base
|