Add world indicators and parameters to enhance universe simulation
- new universe creation function.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
@@ -0,0 +1 @@
|
||||
uid://brsauq1r3xn87
|
||||
@@ -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"]]
|
||||
|
||||
Reference in New Issue
Block a user