Add world indicators and parameters to enhance universe simulation

- new universe creation function.
This commit is contained in:
Kirill
2026-09-28 22:19:13 +03:00
parent e79f3fa362
commit 15ee7833ed
14 changed files with 522 additions and 27 deletions
+10 -7
View File
@@ -1,6 +1,7 @@
extends RefCounted
const State = preload("res://world/owniverse/civilization_state.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
const ETHICS = State.ETHICS
const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"}
@@ -106,6 +107,8 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
var plans: Dictionary = {}
var colonies: Array = []
var collateral: Dictionary = {}
var modifiers: Dictionary = WorldParameters.life_modifiers(target)
var order: float = WorldParameters.value(target, "Order")
if not target.has("civilization_stats"): target.civilization_stats = {}
var ids: Array = expired.keys()
ids.sort()
@@ -124,14 +127,14 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
var settlers: Dictionary = {}
for ethic in triggered:
var row: Dictionary = rules[id].ethics[ethic]
var p: float = maxf(0.0, float(row.progress))
var d: float = maxf(0.0, float(row.degradation))
var p: float = maxf(0.0, float(row.progress) * modifiers.progress)
var d: float = maxf(0.0, float(row.degradation) * modifiers.degradation)
var scale: float = maxf(1.0, p + d)
var pro: float = rate(target, id + ":" + ethic + ":pro", triggered[ethic], p / scale)
var deg: float = rate(target, id + ":" + ethic + ":deg", triggered[ethic], d / scale)
var bounded: Dictionary = world._allocate_whole({"pro": pro, "deg": deg}, triggered[ethic])
if rules[id].fights: warriors[ethic] = rate(target, id + ":" + ethic + ":war", triggered[ethic], float(row.war))
if rules[id].colonizes: settlers[ethic] = rate(target, id + ":" + ethic + ":col", triggered[ethic], float(row.colonization))
if rules[id].fights: warriors[ethic] = rate(target, id + ":" + ethic + ":war", triggered[ethic], minf(1.0, float(row.war) * modifiers.war))
if rules[id].colonizes: settlers[ethic] = rate(target, id + ":" + ethic + ":col", triggered[ethic], minf(1.0, float(row.colonization) * modifiers.colonization))
successes[ethic] = bounded.pro
failures[ethic] = bounded.deg
triggered[ethic] -= bounded.pro + bounded.deg
@@ -146,16 +149,16 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
var win_expected: float = outcomes.get("pro25", 0.0) * 0.25 + outcomes.get("pro50", 0.0) * 0.5 + outcomes.get("pro75", 0.0) * 0.75
var loss_expected: float = outcomes.get("deg", 0.0) * 2.0 + outcomes.get("pro25", 0.0) + outcomes.get("pro50", 0.0) * 1.5 + outcomes.get("pro75", 0.0) * 2.0
var winners: Dictionary = take(world, warriors, rate(target, id + ":wins", win_expected, 1.0))
var attackers_lost: Dictionary = take(world, warriors, outcomes.get("deg", 0.0))
var attackers_lost: Dictionary = take(world, warriors, rate(target, id + ":attackers_lost", outcomes.get("deg", 0.0) / order, 1.0))
add(triggered, progress(world, target, rules, budget, plan, winners))
fall(target, plan, rules, attackers_lost)
var losses: float = minf(rate(target, id + ":loss", loss_expected, 1.0), total(triggered) + total(remaining[id]))
var losses: float = minf(rate(target, id + ":loss", loss_expected / order, 1.0), total(triggered) + total(remaining[id]))
var sides: Dictionary = world._allocate_whole({"due": total(triggered), "later": total(remaining[id])}, losses)
var victims: Dictionary = take(world, triggered, sides.due)
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
fall(target, plan, rules, victims)
add(triggered, warriors)
var damage: float = outcomes.get("pro50", 0.0) * 10.0 + outcomes.get("pro75", 0.0) * 30.0
var damage: float = rate(target, id + ":collateral", (outcomes.get("pro50", 0.0) * 10.0 + outcomes.get("pro75", 0.0) * 30.0) / order, 1.0)
collateral[rules[id].previous] = collateral.get(rules[id].previous, 0.0) + damage
target.civilization_stats.wars = target.civilization_stats.get("wars", 0.0) + wars
emit(plan, id, triggered)
+14
View File
@@ -3,6 +3,7 @@ extends RefCounted
const GREEN = "#40cc40"
const RED = "#ff6060"
const GREY = "#888888"
const Parameters = preload("res://world/owniverse/world_parameters.gd")
static func number(value: float, digits: int = 0) -> String:
if not is_finite(value):
@@ -82,6 +83,19 @@ static func variables(world, global: Node, id: String, rate: float = 0.0) -> Dic
static func render(world, global: Node, id: String, category: String = "Entity", rate: float = 0.0) -> String:
var values: Dictionary = variables(world, global, id, rate)
if category == "Indicator":
if id == "Density":
id = Parameters.density_display(world.stellar_environment(world.state).density).mode
var scales: Dictionary = {"Burnability": "Bur", "Explosiveness": "Exp", "Aggression": "Agg"}
if scales.has(id):
var level: float = Parameters.value(world.state, scales[id])
values.amount = number(level, 1 if level != floorf(level) else 0)
values.scale = global.render_template(global.get_template("Scale" + str(clampi(roundi(level), 1, 19)), "Indicator", "Scale"), {})
if id == "Trends":
for trend in Parameters.TRENDS:
values[trend.to_lower()] = number(world.state.get("trends", {}).get(trend, 0.0) * 100.0, 2) + "%"
if id == "Energy":
values["энергия"] = number(world.state.energy) + " (" + number(clampf(world.state.energy / Parameters.INITIAL_ENERGY, 0.0, 1.0) * 100.0, 2) + "%)"
var title: String = global.render_template(global.get_template(id, category, "Name"), values)
var body: String = global.render_template(global.get_template(id, category, "Description"), values)
return "[b]" + (title if title != "" else id) + "[/b]\n" + body
+37 -7
View File
@@ -3,6 +3,7 @@ class_name Owniverse
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
#var state: State = State.new()
@@ -30,7 +31,9 @@ var state: Dictionary = {
},
'speed': 1,
'speed_in_year': 1,
'energy': 1E14,
'energy': WorldParameters.INITIAL_ENERGY,
'world_parameters': WorldParameters.defaults(),
'trends': WorldParameters.trend_defaults(),
'batches': {},
'batch_origins': {},
'binds': {},
@@ -110,6 +113,7 @@ func _evolute_to_datetime(datetime: float) -> void:
_evolve_batches(next, to_year)
_refresh_groups(next)
_clamp_auto_resources(next)
WorldParameters.advance(previous, next, Global.CivilizationRules, to_year - from_year)
next.time = {"datetime": datetime, "year": to_year}
# Publish only after every phase has finished; previous stays untouched.
@@ -161,6 +165,25 @@ func _add_time_delta(delta: float) -> Dictionary:
return {"datetime": new_datetime, "year": SimulationTime.processed_year(new_datetime)}
## Public entry point for forces: update a world scale and preserve stellar ages.
func modify_world_parameter(key: String, change: float) -> void:
if key not in ["Bur", "Exp", "Agg"] or not is_finite(change): return
var old_multiplier: float = WorldParameters.effective(state, key)
if not state.has("world_parameters"): state.world_parameters = WorldParameters.defaults()
state.world_parameters[key] = clampf(WorldParameters.value(state, key) + change, 1.0, 19.0)
if key == "Bur":
var ratio: float = old_multiplier / WorldParameters.effective(state, key)
var now: float = state.time.year
for id in state.batches:
if not entities.has(id) or entities[id].group_as != "Star": continue
for batch: Dictionary in state.batches[id]:
# Only future survival time changes. Birth dates and the fractional
# withdrawal carry remain intact; already elapsed time is not replayed.
for field in ["start_year", "end_year", "processed_until"]:
if batch.has(field): batch[field] = now + maxf(0.0, batch[field] - now) * ratio
new_state = state
## Rebuild after changing group_as or replacing definitions without changing their count.
## Adding/removing definitions is detected automatically during initialization.
func rebuild_group_index() -> void:
@@ -355,6 +378,8 @@ func _append_batch(target: Dictionary, id: String, amount: float,
target.batch_origins = {}
var batches: Array = target.batches[id]
var lifespan: float = CivilizationState.effective_span(self, target, id, Global.CivilizationRules)
if entities[id].group_as == "Star":
lifespan /= WorldParameters.effective(target, "Bur")
var batch_duration: float = lifespan / Global.batch_count if lifespan > 10.0 else lifespan
if batch_duration <= 0.0:
return NAN
@@ -475,11 +500,17 @@ func _add_output_terms(outputs: Dictionary, terms: String, count: float) -> void
func _life_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary:
var rule: Dictionary = entity.life_rule
var pending: Dictionary = {}
var modifiers: Dictionary = WorldParameters.life_modifiers(target)
var probabilities: Dictionary = {
"progress": float(rule.progress) * modifiers.progress * WorldParameters.value(target, "Enlightenment"),
"degradation": float(rule.degradation) * modifiers.degradation,
}
var probability_sum: float = maxf(1.0, probabilities.progress + probabilities.degradation)
if not target.has("life_remainders"):
target.life_remainders = {}
for channel in ["progress", "degradation"]:
var key: String = entity.id + ":" + channel
var expected: float = count * float(rule[channel]) + target.life_remainders.get(key, 0.0)
var expected: float = count * probabilities[channel] / probability_sum + target.life_remainders.get(key, 0.0)
target.life_remainders[key] = expected
pending[channel] = floorf(snappedf(expected, 1.0e-12))
var assigned: Dictionary = _allocate_whole(pending, count)
@@ -530,7 +561,7 @@ func _evolution_products(target: Dictionary, entity: OwniverseEntity, count: flo
outputs[id] = count * maxf(0.0, entity.output_and[id])
if not entity.output_or.is_empty():
# The parser stores the percentage after '%', e.g. 0.1 means 0.1 percent.
var chance: float = clampf(entity.output_or.get("chance", 0.0) / 100.0, 0.0, 1.0)
var chance: float = clampf(entity.output_or.get("chance", 0.0) / 100.0 * WorldParameters.effective(target, "Exp"), 0.0, 1.0)
if not target.has("evolution_remainders"):
target.evolution_remainders = {}
var expected: float = count * chance + target.evolution_remainders.get(entity.id, 0.0)
@@ -542,12 +573,11 @@ func _evolution_products(target: Dictionary, entity: OwniverseEntity, count: flo
# blast:N. Zero disables the event; never add both representations.
outputs.erase("blast")
if entity.blast > 0:
outputs["blast"] = count * entity.blast
outputs["blast"] = count * entity.blast * WorldParameters.effective(target, "Exp")
return outputs
# Derived from current concrete stock, including bound objects, never UI aliases.
# GI has no Unity force parameters; stellar effects use their neutral multiplier 1.
func stellar_environment(source: Dictionary) -> Dictionary:
var quantities: Dictionary = _physical_amounts(source)
# SMBH is a self-group in GI; its quantity stores mass, not object count.
@@ -618,7 +648,7 @@ func _produce_newborn_lifetime(target: Dictionary, id: String, count: float,
object_years -= maxf(0.0, added) * _expired_years(to_year, batch.start_year, batch.end_year)
object_years = maxf(0.0, object_years)
for resource in products:
var output: float = object_years * products[resource]
var output: float = object_years * products[resource] * WorldParameters.production(target, entities[id])
if resource in ["H", "S"]:
output = minf(output, target.energy)
var actual: float = _credit(target, resource, output)
@@ -1001,7 +1031,7 @@ func _produce_for_period(previous: Dictionary, target: Dictionary, quantities: D
var auto_share: float = clampf(entity.auto, 0.0, 1.0)
var products: Dictionary = _yearly_products(entity)
for product in products:
var amount: float = object_years * products[product]
var amount: float = object_years * products[product] * WorldParameters.production(previous, entity)
basic[product] = basic.get(product, 0.0) + amount * (1.0 - auto_share)
automatic[product] = automatic.get(product, 0.0) + amount * auto_share
# Basic and automatic output each retain their unproduced remainder.
+102
View File
@@ -0,0 +1,102 @@
extends RefCounted
const INITIAL_ENERGY = 1.0e14
const TRENDS = ["Antitheism", "Integration", "Xenocide", "Reconstruction", "Spirituality"]
const WEIGHTS = {
"m": [0, 2, 2, 0, 6], "l": [6, 1, 1, 2, 0],
"w": [2, 0, 6, 0, 2], "d": [0, 6, 0, 1, 3],
"c": [1, 3, 3, 3, 0], "b": [3, 0, 0, 6, 1],
}
static func defaults() -> Dictionary:
return {"Bur": 10.0, "Exp": 10.0, "Agg": 10.0,
"Federation": 1.0, "Enlightenment": 1.0, "Order": 1.0}
static func trend_defaults() -> Dictionary:
return {"Antitheism": 0.0, "Integration": 0.0, "Xenocide": 0.0,
"Reconstruction": 0.0, "Spirituality": 0.0}
static func value(source: Dictionary, key: String) -> float:
return source.get("world_parameters", {}).get(key, defaults().get(key, 1.0))
static func effective(source: Dictionary, key: String) -> float:
return pow(10.0, (clampf(value(source, key), 1.0, 19.0) - 10.0) / 9.0)
static func life_modifiers(source: Dictionary) -> Dictionary:
var aggression: float = effective(source, "Agg")
return {"progress": aggression,
"degradation": (1.0 - (1.0 - aggression) / 20.0) / value(source, "Order"),
"war": 1.0 - (1.0 - aggression) / 5.0,
"colonization": 1.0 - (1.0 - aggression) / 2.0}
static func production(source: Dictionary, entity) -> float:
if entity.event_group == "SNGRBB":
return effective(source, "Exp")
if entity.group_as == "Star":
var burn: float = effective(source, "Bur")
return sqrt(burn) if burn > 1.0 else burn
return 1.0
static func density_display(density: float) -> Dictionary:
if density <= 1.0e-5:
# Unity had log10(density) - 7 here; +7 maps 1e-7..1e-5 to 0..1.
return {"mode": "DensityRip", "position": clampf((log(maxf(density, 1e-7)) / log(10.0) + 7.0) / 2.0, 0.0, 1.0)}
if density >= 0.9:
return {"mode": "DensityCrunch", "position": clampf((density - 0.9) * 10.0, 0.0, 1.0)}
return {"mode": "Density", "position": clampf(log(density * 1.0e5) / log(9.0e4), 0.0, 1.0)}
## Integrate the population present at the start of the simulation interval.
## Thresholds use whole event counts; no per-year loop at high speeds.
static func advance(previous: Dictionary, target: Dictionary, rules: Dictionary, years: float) -> void:
if years <= 0.0: return
if not target.has("world_parameters"): target.world_parameters = defaults()
if not target.has("trends"): target.trends = trend_defaults()
var rates: Array = [0.0, 0.0, 0.0, 0.0, 0.0]
var stages: Array = []
for id in rules:
if str(rules[id].home) != "": stages.append(id)
stages.sort()
var weight: float = 1.0
for id in stages:
var population: Dictionary = previous.get("ethics", {}).get(id, {})
var count: float = 0.0
for n in population.values(): count += n
if count > 0.0:
for ethic in population:
for i in TRENDS.size():
rates[i] += population[ethic] / count * WEIGHTS[ethic][i] * weight
weight *= 2.0
for i in TRENDS.size():
var name: String = TRENDS[i]
var accumulated: float = target.trends[name] + rates[i] * years * 1.0e-9
var events: float = floorf(snappedf(accumulated, 1.0e-12))
target.trends[name] = maxf(0.0, accumulated - events)
if events <= 0.0: continue
var key: String = "trend:" + name
target.civilization_stats[key] = target.civilization_stats.get(key, 0.0) + events
if name == "Integration": _integration(target, events)
static func _integration(target: Dictionary, events: float) -> void:
# Equal-probability outcomes in Unity. Use a saved RNG and bounded aggregate
# sampling for large steps, so thousands of triggers do not stall a frame.
var rng := RandomNumberGenerator.new()
rng.seed = 76123
if target.civilization_stats.has("trend_rng"):
rng.state = int(target.civilization_stats.trend_rng)
var counts: Array = [0.0, 0.0, 0.0]
if events <= 256.0:
for i in int(events): counts[rng.randi_range(0, 2)] += 1.0
else:
counts[0] = clampf(roundf(rng.randfn(events / 3.0, sqrt(events * 2.0 / 9.0))), 0.0, events)
var rest: float = events - counts[0]
counts[1] = clampf(roundf(rng.randfn(rest / 2.0, sqrt(rest / 4.0))), 0.0, rest)
counts[2] = rest - counts[1]
target.civilization_stats.trend_rng = rng.state
var parameters: Dictionary = target.world_parameters
parameters.Agg = maxf(1.0, parameters.Agg - counts[0] - counts[1])
parameters.Federation += counts[0] * 0.1
parameters.Enlightenment += counts[1] * 0.2
parameters.Order += counts[2] * 0.1
for i in 3:
var key: String = ["Federation", "Enlightenment", "Order"][i]
target.civilization_stats[key] = target.civilization_stats.get(key, 0.0) + counts[i]
+1
View File
@@ -0,0 +1 @@
uid://brsauq1r3xn87
+1
View File
@@ -1110,6 +1110,7 @@ autostart = true
[connection signal="pressed" from="Stage/World/PageScroller/HBoxContainer/LifePage/LifeHeader/AggressionIndicator" to="Stage" method="_on_indicator_pressed" binds= ["Aggression", 4]]
[connection signal="pressed" from="Stage/World/PageScroller/HBoxContainer/LifePage/LifeHeader/TrendIndicator" to="Stage" method="_on_indicator_pressed" binds= ["Trends", 4]]
[connection signal="pressed" from="Stage/MainMenu/TopMenu/BackButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["World"]]
[connection signal="pressed" from="Stage/MainMenu/CreateOwniverse" to="Stage" method="create_new_universe"]
[connection signal="pressed" from="Stage/MainMenu/Achievements" to="Stage" method="_on_topmenu_button_pressed" binds= ["Achievements"]]
[connection signal="pressed" from="Stage/MainMenu/Credits" to="Stage" method="_on_topmenu_button_pressed" binds= ["Credits"]]
[connection signal="pressed" from="Stage/Report/TopMenu/BackButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["World"]]