Compare commits
30 Commits
v0.6.22
...
1796829338
| Author | SHA1 | Date | |
|---|---|---|---|
| 1796829338 | |||
| f246efd6ad | |||
| f25b77b605 | |||
| a09e3df220 | |||
| 26347fca30 | |||
| 5ea65a7e9b | |||
| 5f3875bc14 | |||
| cc2700d507 | |||
| 5f5cc6f894 | |||
| 7de65f6145 | |||
| 85a13eb241 | |||
| dea744bc30 | |||
| c22b21a537 | |||
| b44f92aacf | |||
| 0162084e53 | |||
| 2f4b6a98d8 | |||
| d90f8693c9 | |||
| e870c96879 | |||
| e531e7d9b6 | |||
| 57cc023558 | |||
| 23929e3651 | |||
| 829aadca2d | |||
| 30486616f8 | |||
| d9a2679af4 | |||
| 4f57c37572 | |||
| e4a0dd2d13 | |||
| 0a10cff0fa | |||
| e4cf5bdad0 | |||
| 645751cd23 | |||
| b92a49821d |
@@ -1,13 +1,19 @@
|
||||
---
|
||||
name: Godot 4.4 Core Rules
|
||||
name: Godot 4.7 Core Rules
|
||||
priority: high
|
||||
---
|
||||
|
||||
Ты — senior Godot 4.4 GDScript разработчик.
|
||||
Ты — senior Godot 4.7 GDScript разработчик.
|
||||
|
||||
- Use Godot 4.7+ API.
|
||||
- Do not use Godot 3.x syntax or APIs.
|
||||
- Follow the existing project architecture and coding style.
|
||||
- Do not introduce unnecessary abstractions.
|
||||
- Do not modify unrelated files.
|
||||
|
||||
- Всегда используй Godot 4.4+ API.
|
||||
- Пиши строго типизированный код (`var health: int = 100`, `: Array[Node]` и т.д.).
|
||||
- Предпочитай Signals вместо проверки в _process.
|
||||
- Используй @export, @export_range, @export_enum, @onready.
|
||||
- Composition over deep inheritance.
|
||||
- Менеджеры и автолоады выноси в отдельные singleton'ы.
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- use spaces not tabs for indentation
|
||||
@@ -25,6 +25,7 @@ jobs:
|
||||
--headless \
|
||||
--path . \
|
||||
--editor \
|
||||
--import \
|
||||
--quit \
|
||||
--verbose
|
||||
|
||||
@@ -37,6 +38,7 @@ jobs:
|
||||
--headless \
|
||||
--path . \
|
||||
--editor \
|
||||
--import \
|
||||
--quit
|
||||
|
||||
godot \
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
.vscode/
|
||||
/android/
|
||||
*.import
|
||||
!data/*.txt.import
|
||||
|
||||
# MacOS specific ignores
|
||||
.DS_Store
|
||||
|
||||
+62
-11
@@ -1,6 +1,9 @@
|
||||
extends Node
|
||||
|
||||
const SPEED_LIMIT: int = 4
|
||||
const SPEED_LIMIT: int = 6
|
||||
|
||||
# Write a readable diagnostic copy next to the encrypted save. Never used for loading.
|
||||
var save_plaintext_copy: bool = true
|
||||
|
||||
var Settings: Dictionary = {
|
||||
"filename": "save-0001",
|
||||
@@ -16,6 +19,8 @@ var Locale = "en"
|
||||
var OwniverseEntities: Dictionary = {}
|
||||
var OwniverseItems: Dictionary = {}
|
||||
var Templates: Dictionary = {}
|
||||
var LifeRules: Dictionary = {}
|
||||
var CivilizationRules: Dictionary = {}
|
||||
|
||||
var batch_count: float = 40.0 # must be even not odd
|
||||
|
||||
@@ -26,20 +31,51 @@ func get_localized_item_description(item_id: String, item: String) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
# Russian templates include the class; legacy English keys omit it.
|
||||
func get_template(id: String, category: String, field: String) -> Dictionary:
|
||||
var rows: Dictionary = Templates.get(Locale, {})
|
||||
for key in [id + "." + category + "." + field, id + "." + field]:
|
||||
if rows.has(key):
|
||||
return rows[key]
|
||||
return {}
|
||||
|
||||
|
||||
func render_template(row: Dictionary, variables: Dictionary) -> String:
|
||||
var result: String = str(row.get("text", ""))
|
||||
for i in range(5):
|
||||
var variable: String = str(row.get("value" + str(i), "")).strip_edges()
|
||||
var value: String = str(variables.get(variable, "—")) if variable != "" else ""
|
||||
result = result.replace("%value" + str(i) + "%", value)
|
||||
return result.replace("<br>", "\n").replace("[p]", "\n")
|
||||
|
||||
|
||||
func get_description(item_id: String, items: Array = []) -> String:
|
||||
if !Templates[Locale].has(item_id):
|
||||
return ""
|
||||
var description: String = Templates[Locale][item_id]["text"]
|
||||
var value = 0
|
||||
for item in items:
|
||||
description.replace("value" + str(value), str(item))
|
||||
return description
|
||||
var row: Dictionary = Templates.get(Locale, {}).get(item_id, {})
|
||||
var variables: Dictionary = {}
|
||||
for i in mini(items.size(), 5):
|
||||
variables[str(row.get("value" + str(i), ""))] = items[i]
|
||||
return render_template(row, variables)
|
||||
|
||||
|
||||
func format_number(value: float, digits: int = 0) -> String:
|
||||
if value == 0: return ""
|
||||
if value < 10_000:
|
||||
return "%.*f" % [digits, value]
|
||||
# Keep ordinary quantities readable, grouping thousands with spaces.
|
||||
# Scientific notation is reserved for values whose fixed form would be
|
||||
# unwieldy in the UI.
|
||||
if absf(value) < 1_000_000_000.0:
|
||||
var fixed: String = "%.*f" % [digits, value]
|
||||
var parts: PackedStringArray = fixed.split(".", true)
|
||||
var integer: String = parts[0]
|
||||
var sign: String = ""
|
||||
if integer.begins_with("-") or integer.begins_with("+"):
|
||||
sign = integer.left(1)
|
||||
integer = integer.substr(1)
|
||||
var grouped: String = ""
|
||||
while integer.length() > 3:
|
||||
grouped = " " + integer.right(3) + grouped
|
||||
integer = integer.left(integer.length() - 3)
|
||||
grouped = sign + integer + grouped
|
||||
return grouped + ("." + parts[1] if parts.size() > 1 else "")
|
||||
|
||||
var exp_value = int(floor(log(abs(value)) / log(10.0)))
|
||||
var mantissa = value / pow(10.0, exp_value)
|
||||
@@ -48,13 +84,28 @@ func format_number(value: float, digits: int = 0) -> String:
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
OwniverseEntities = _read_from_csv("res://data/owniverse_entitites.txt")
|
||||
CivilizationRules.clear()
|
||||
for row: Dictionary in _read_from_csv("res://data/civilization_rules.txt").values():
|
||||
if not CivilizationRules.has(row.stage):
|
||||
CivilizationRules[row.stage] = {"home": row.home, "span": float(row.span), "stars": float(row.stars), "ethics": {},
|
||||
"colonizes": row.colonizes == "1", "fights": row.fights == "1", "diplomacy": row.diplomacy == "1", "worships": row.worships == "1"}
|
||||
CivilizationRules[row.stage].ethics[row.ethic] = row
|
||||
var stages: Array = CivilizationRules.keys()
|
||||
stages.sort()
|
||||
for i in stages.size():
|
||||
CivilizationRules[stages[i]].next = stages[i + 1] if i + 1 < stages.size() else "Ct"
|
||||
CivilizationRules[stages[i]].previous = stages[i - 1] if i > 0 else "C0.1"
|
||||
CivilizationRules[stages[i]].colony = stages[maxi(0, i - 2)]
|
||||
CivilizationRules.Ct = {"home": "", "span": 0.0, "stars": 0.0, "ethics": {}, "next": "Ct", "previous": "C4.4", "colonizes": false, "fights": false, "diplomacy": false, "worships": false}
|
||||
LifeRules = _read_from_csv("res://data/life_rules.txt")
|
||||
OwniverseEntities = _read_from_csv("res://data/owniverse_entities.txt")
|
||||
OwniverseItems = _read_from_csv("res://data/owniverse_items.txt")
|
||||
|
||||
Locales["en"] = _read_from_csv("res://data/locale.en.txt")
|
||||
Locales["ru"] = _read_from_csv("res://data/locale.ru.txt")
|
||||
|
||||
Templates["en"] = _read_from_csv("res://data/templates.en.txt")
|
||||
Templates["ru"] = _read_from_csv("res://data/templates.ru.txt")
|
||||
|
||||
|
||||
func _read_from_csv(file_path: String) -> Dictionary:
|
||||
|
||||
+279
-15
@@ -5,34 +5,298 @@ extends Node
|
||||
const SETTING_SECTION := "Settings"
|
||||
const SAVE_PATH = "user://settings.cfg"
|
||||
|
||||
const FORMAT_VERSION: int = 2
|
||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||
const STATE_VERSION: String = SimulationTime.STATE_VERSION
|
||||
var last_error: String = ""
|
||||
var blocked_saves: Dictionary = {}
|
||||
var save_directory: String = "user://"
|
||||
signal save_completed(filename: String, success: bool, error: String)
|
||||
signal saves_drained
|
||||
|
||||
# Queue ownership stays on the main thread. The worker only sees its snapshot.
|
||||
var _save_thread: Thread
|
||||
var _active_filename: String = ""
|
||||
var _pending_saves: Dictionary = {}
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _save_thread != null and not _save_thread.is_alive():
|
||||
_finish_save()
|
||||
|
||||
|
||||
func is_saving() -> bool:
|
||||
return _save_thread != null or not _pending_saves.is_empty()
|
||||
|
||||
|
||||
func _snapshot(filename: String, password: String, data: Dictionary) -> Dictionary:
|
||||
var payload: Dictionary = data.duplicate(true)
|
||||
payload.format_version = FORMAT_VERSION
|
||||
payload.app_version = str(ProjectSettings.get_setting("application/config/version", ""))
|
||||
return {"filename": filename, "path": save_directory.path_join(filename + ".adventure"), "password": password, "payload": payload,
|
||||
"plaintext": Global.save_plaintext_copy, "speed_limit": Global.SPEED_LIMIT,
|
||||
"stages": Global.CivilizationRules.keys()}
|
||||
|
||||
|
||||
## True means queued, not written. Completion is reported by save_completed.
|
||||
func request_save(filename: String, password: String, data: Dictionary) -> bool:
|
||||
if blocked_saves.has(filename):
|
||||
return _fail("Save blocked to preserve an unreadable save: " + filename)
|
||||
_pending_saves[filename] = _snapshot(filename, password, data)
|
||||
if _save_thread == null:
|
||||
_start_save()
|
||||
return true
|
||||
|
||||
|
||||
func _start_save() -> void:
|
||||
if _pending_saves.is_empty():
|
||||
return
|
||||
_active_filename = _pending_saves.keys()[0]
|
||||
var job: Dictionary = _pending_saves[_active_filename]
|
||||
_pending_saves.erase(_active_filename)
|
||||
_save_thread = Thread.new()
|
||||
var error: Error = _save_thread.start(_write_snapshot.bind(job))
|
||||
if error != OK:
|
||||
_save_thread = null
|
||||
_fail("Cannot start save worker: " + error_string(error))
|
||||
save_completed.emit(_active_filename, false, last_error)
|
||||
_start_save()
|
||||
if not is_saving(): saves_drained.emit()
|
||||
|
||||
|
||||
func _finish_save() -> void:
|
||||
var error: String = _save_thread.wait_to_finish()
|
||||
var filename: String = _active_filename
|
||||
_save_thread = null
|
||||
last_error = error
|
||||
if error != "": push_error(error)
|
||||
save_completed.emit(filename, error == "", error)
|
||||
if _save_thread == null: _start_save()
|
||||
if not is_saving(): saves_drained.emit()
|
||||
|
||||
|
||||
## Used for application suspension and teardown, when frames may stop arriving.
|
||||
func flush_saves() -> void:
|
||||
while is_saving():
|
||||
if _save_thread == null: _start_save()
|
||||
if _save_thread != null: _finish_save()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
flush_saves()
|
||||
|
||||
#region Save Load routines
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
last_error = message
|
||||
push_error(message)
|
||||
return false
|
||||
|
||||
func save_data(filename: String, password: String, data: Dictionary) -> void:
|
||||
filename = "user://" + filename + ".adventure"
|
||||
var file := FileAccess.open_encrypted_with_pass(filename, FileAccess.WRITE, password)
|
||||
|
||||
func save_data(filename: String, password: String, data: Dictionary) -> bool:
|
||||
# Synchronous compatibility API must never race an asynchronous writer.
|
||||
flush_saves()
|
||||
last_error = ""
|
||||
if blocked_saves.has(filename):
|
||||
return _fail("Save blocked to preserve an unreadable save: " + filename)
|
||||
var error: String = _write_snapshot(_snapshot(filename, password, data))
|
||||
return _fail(error) if error != "" else true
|
||||
|
||||
|
||||
static func _write_snapshot(job: Dictionary) -> String:
|
||||
var payload: Dictionary = job.payload
|
||||
var problem: String = _validate_payload(payload, job.speed_limit, job.stages)
|
||||
if problem != "":
|
||||
return "Invalid save: " + problem
|
||||
var path: String = job.path
|
||||
var temporary: String = path + ".tmp"
|
||||
var file := FileAccess.open_encrypted_with_pass(temporary, FileAccess.WRITE, job.password)
|
||||
if file == null:
|
||||
push_error("Can't write to the file: ", filename)
|
||||
return
|
||||
|
||||
file.store_var(data) # бинарно + быстрее + сериализация
|
||||
return "Cannot write encrypted save: " + temporary
|
||||
file.store_var(payload, false)
|
||||
file.close()
|
||||
# Check the actual encrypted file, including finalization, before replacing the old save.
|
||||
var verification := FileAccess.open_encrypted_with_pass(temporary, FileAccess.READ, job.password)
|
||||
if verification == null:
|
||||
return "Cannot verify encrypted save: " + temporary
|
||||
var restored: Variant = verification.get_var(false)
|
||||
verification.close()
|
||||
if not restored is Dictionary or restored != payload:
|
||||
return "Encrypted save verification failed: " + temporary
|
||||
if DirAccess.rename_absolute(temporary, path) != OK:
|
||||
return "Cannot replace encrypted save: " + path
|
||||
if job.plaintext:
|
||||
var plain_path: String = path + ".json"
|
||||
file = FileAccess.open(plain_path + ".tmp", FileAccess.WRITE)
|
||||
if file == null:
|
||||
return "Encrypted save written, but cannot write diagnostic copy: " + plain_path
|
||||
file.store_string(JSON.stringify(payload, " ", true, true) + "\n")
|
||||
file.flush()
|
||||
var write_error: Error = file.get_error()
|
||||
file.close()
|
||||
if write_error != OK or DirAccess.rename_absolute(plain_path + ".tmp", plain_path) != OK:
|
||||
return "Encrypted save written, but diagnostic copy failed: " + plain_path
|
||||
return ""
|
||||
|
||||
|
||||
func load_data(filename: String, password: String) -> Dictionary:
|
||||
filename = "user://" + filename + ".adventure"
|
||||
if not FileAccess.file_exists(filename):
|
||||
flush_saves()
|
||||
last_error = ""
|
||||
# Plaintext is diagnostic only: never use it as a fallback.
|
||||
var path: String = save_directory.path_join(filename + ".adventure")
|
||||
if not FileAccess.file_exists(path):
|
||||
return {}
|
||||
|
||||
var file := FileAccess.open_encrypted_with_pass(filename, FileAccess.READ, password)
|
||||
var file := FileAccess.open_encrypted_with_pass(path, FileAccess.READ, password)
|
||||
if file == null:
|
||||
push_error(filename, " couldn't be decrypted.")
|
||||
blocked_saves[filename] = true
|
||||
_fail("Cannot decrypt save: " + path)
|
||||
return {}
|
||||
|
||||
var data: Variant = file.get_var()
|
||||
var data: Variant = file.get_var(false)
|
||||
file.close()
|
||||
# Unsupported formats are archived, never converted or overwritten.
|
||||
# Returning no data lets the stage start a new, saveable session.
|
||||
if data is Dictionary and data.get("format_version") != FORMAT_VERSION:
|
||||
var archive: String = path + ".incompatible.bak"
|
||||
var index: int = 0
|
||||
while FileAccess.file_exists(archive):
|
||||
index += 1
|
||||
archive = path + ".incompatible.bak." + str(index)
|
||||
var error: Error = DirAccess.rename_absolute(path, archive)
|
||||
if error != OK:
|
||||
blocked_saves[filename] = true
|
||||
_fail("Cannot archive incompatible save: " + error_string(error))
|
||||
return {}
|
||||
blocked_saves.erase(filename)
|
||||
push_warning("Incompatible save preserved at " + archive + "; starting a new game.")
|
||||
return {}
|
||||
var problem: String = validate_data(data)
|
||||
if problem != "":
|
||||
blocked_saves[filename] = true
|
||||
_fail("Invalid save " + path + ": " + problem)
|
||||
return {}
|
||||
blocked_saves.erase(filename)
|
||||
return data
|
||||
|
||||
|
||||
static func _number(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool:
|
||||
if not (value is int or value is float) or not is_finite(float(value)):
|
||||
return false
|
||||
return (not whole or float(value) == floorf(float(value))) and (not nonnegative or value >= 0)
|
||||
|
||||
|
||||
static func _number_map(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool:
|
||||
if not value is Dictionary:
|
||||
return false
|
||||
for key in value:
|
||||
if not (key is String or key is StringName) or not _number(value[key], whole, nonnegative):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _plain_tree(value: Variant) -> bool:
|
||||
if value is Dictionary:
|
||||
for key in value:
|
||||
if not (key is String or key is StringName) or not _plain_tree(value[key]):
|
||||
return false
|
||||
return true
|
||||
if value is Array:
|
||||
for item in value:
|
||||
if not _plain_tree(item):
|
||||
return false
|
||||
return true
|
||||
return value == null or value is String or value is StringName or value is bool or _number(value, false, false)
|
||||
|
||||
|
||||
## Return a diagnostic path on failure; an empty string means the structure is valid.
|
||||
func validate_data(data: Variant) -> String:
|
||||
return _validate_payload(data, Global.SPEED_LIMIT, Global.CivilizationRules.keys())
|
||||
|
||||
|
||||
static func _validate_payload(data: Variant, speed_limit: int, stages: Array) -> String:
|
||||
if not data is Dictionary or not _plain_tree(data):
|
||||
return "save must contain plain finite values"
|
||||
if data.get("format_version") != FORMAT_VERSION or not data.get("app_version") is String:
|
||||
return "unsupported save format"
|
||||
if not data.get("ui") is Dictionary or not data.ui.get("current_page") is int or data.ui.current_page < 0:
|
||||
return "invalid ui.current_page"
|
||||
if not data.get("state") is Dictionary:
|
||||
return "state must be a Dictionary"
|
||||
var saved: Dictionary = data.state
|
||||
var required := [
|
||||
"version", "time", "speed", "speed_in_year", "energy", "batches", "batch_origins",
|
||||
"binds", "amount", "distribution", "auto_resources", "auto_star_deficits",
|
||||
"production_remainders", "resource_remainders", "evolution_remainders", "life_remainders",
|
||||
"planet_remainders", "blast_remainders", "destruction_remainders", "civilization_remainders",
|
||||
"civilization_stats", "civilization_homes", "ethics", "stats",
|
||||
]
|
||||
if saved.size() != required.size():
|
||||
return "unsupported state structure"
|
||||
for field in required:
|
||||
if not saved.has(field):
|
||||
return "missing state." + field
|
||||
if saved.version != STATE_VERSION:
|
||||
return "unsupported state.version"
|
||||
if not saved.time is Dictionary or not _number(saved.time.get("datetime")) or not _number(saved.time.get("year")):
|
||||
return "invalid state.time"
|
||||
if not is_finite(float(saved.time.datetime) * 10.0) or saved.time.year != SimulationTime.processed_year(saved.time.datetime):
|
||||
return "invalid processed time"
|
||||
if not _number(saved.speed, true) or saved.speed < 1 or saved.speed > speed_limit:
|
||||
return "invalid state.speed"
|
||||
if not _number(saved.speed_in_year) or saved.speed_in_year != pow(10.0, saved.speed - 1):
|
||||
return "invalid state.speed_in_year"
|
||||
if not _number(saved.energy, true) or not _number_map(saved.amount, true):
|
||||
return "invalid state resources"
|
||||
if not saved.stats is Dictionary or not _number_map(saved.stats.get("total_amount"), true):
|
||||
return "invalid state.stats.total_amount"
|
||||
if saved.amount.keys() != saved.stats.total_amount.keys():
|
||||
return "amount and total_amount keys differ"
|
||||
if not saved.binds is Dictionary or not saved.batches is Dictionary:
|
||||
return "state.binds and state.batches must be Dictionaries"
|
||||
for owner_id in saved.binds:
|
||||
if not saved.amount.has(owner_id) or not _number_map(saved.binds[owner_id], true):
|
||||
return "invalid state.binds." + owner_id
|
||||
for id in saved.binds[owner_id]:
|
||||
if not saved.amount.has(id):
|
||||
return "unknown bound resource " + id
|
||||
for id in saved.batches:
|
||||
if not saved.amount.has(id) or not saved.batches[id] is Array:
|
||||
return "invalid state.batches." + id
|
||||
for batch in saved.batches[id]:
|
||||
if not batch is Dictionary or not _number(batch.get("amount"), true) or not _number(batch.get("start_year")) or not _number(batch.get("end_year")):
|
||||
return "invalid batch in " + id
|
||||
if batch.end_year < batch.start_year:
|
||||
return "reversed batch interval in " + id
|
||||
if batch.has("created_from") != batch.has("created_to"):
|
||||
return "incomplete batch creation interval in " + id
|
||||
if batch.has("created_from") and (not _number(batch.created_from) or not _number(batch.created_to) or batch.created_to <= batch.created_from or batch.start_year < batch.created_from):
|
||||
return "invalid batch creation interval in " + id
|
||||
if batch.has("processed_until") and (not _number(batch.processed_until) or batch.processed_until < batch.start_year or batch.processed_until > batch.end_year):
|
||||
return "invalid batch.processed_until in " + id
|
||||
if batch.has("withdraw_remainder") and (not _number(batch.withdraw_remainder) or batch.withdraw_remainder > batch.amount):
|
||||
return "invalid batch.withdraw_remainder in " + id
|
||||
for field in ["batch_origins", "distribution", "production_remainders", "evolution_remainders", "planet_remainders", "blast_remainders", "destruction_remainders", "life_remainders", "civilization_remainders", "civilization_stats"]:
|
||||
if not _number_map(saved[field], false, false):
|
||||
return "invalid state." + field
|
||||
if not _number_map(saved.auto_resources, true) or not _number_map(saved.auto_star_deficits, false, false):
|
||||
return "invalid automatic production state"
|
||||
if not saved.resource_remainders is Dictionary or saved.resource_remainders.keys().size() != 2 or not saved.resource_remainders.has("basic") or not saved.resource_remainders.has("automatic"):
|
||||
return "invalid state.resource_remainders"
|
||||
for channel in ["basic", "automatic"]:
|
||||
if not _number_map(saved.resource_remainders[channel], false, false):
|
||||
return "invalid state.resource_remainders." + channel
|
||||
if not saved.ethics is Dictionary or not saved.civilization_homes is Dictionary:
|
||||
return "invalid civilization state"
|
||||
for id in saved.ethics:
|
||||
if id not in stages or not _number_map(saved.ethics[id], true):
|
||||
return "invalid state.ethics." + str(id)
|
||||
for ethic in saved.ethics[id]:
|
||||
if ethic not in ["m", "l", "w", "d", "c", "b"]:
|
||||
return "invalid ethic " + str(ethic)
|
||||
for id in saved.civilization_homes:
|
||||
if not _number_map(saved.civilization_homes[id], true):
|
||||
return "invalid civilization homes"
|
||||
return ""
|
||||
|
||||
return data if data is Dictionary else {}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
id|stage|span|home|stars|colonizes|fights|diplomacy|worships|ethic|progress|degradation|colonization|war|diplomacy_rate|worship|fall
|
||||
C0.2:m|C0.2|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.1:1
|
||||
C0.2:b|C0.2|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.1:1
|
||||
C0.2:d|C0.2|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.1:1
|
||||
C0.2:l|C0.2|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.1:1
|
||||
C0.2:c|C0.2|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.1:1
|
||||
C0.2:w|C0.2|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.1:1
|
||||
C0.3:m|C0.3|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.2:1
|
||||
C0.3:b|C0.3|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.2:1
|
||||
C0.3:d|C0.3|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.2:1
|
||||
C0.3:l|C0.3|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.2:1
|
||||
C0.3:c|C0.3|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.2:1
|
||||
C0.3:w|C0.3|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.2:1
|
||||
C0.4:m|C0.4|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.3:1
|
||||
C0.4:b|C0.4|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.3:1
|
||||
C0.4:d|C0.4|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.3:1
|
||||
C0.4:l|C0.4|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.3:1
|
||||
C0.4:c|C0.4|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.3:1
|
||||
C0.4:w|C0.4|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.3:1
|
||||
C0.5:m|C0.5|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.4:1
|
||||
C0.5:b|C0.5|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.4:1
|
||||
C0.5:d|C0.5|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.4:1
|
||||
C0.5:l|C0.5|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.4:1
|
||||
C0.5:c|C0.5|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.4:1
|
||||
C0.5:w|C0.5|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.4:1
|
||||
C0.6:m|C0.6|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.5:1
|
||||
C0.6:b|C0.6|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.5:1
|
||||
C0.6:d|C0.6|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.5:1
|
||||
C0.6:l|C0.6|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.5:1
|
||||
C0.6:c|C0.6|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.5:1
|
||||
C0.6:w|C0.6|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.5:1
|
||||
C0.7:m|C0.7|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.6:1
|
||||
C0.7:b|C0.7|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.6:1
|
||||
C0.7:d|C0.7|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.6:1
|
||||
C0.7:l|C0.7|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.6:1
|
||||
C0.7:c|C0.7|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.6:1
|
||||
C0.7:w|C0.7|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.6:1
|
||||
C0.8:m|C0.8|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.7:1
|
||||
C0.8:b|C0.8|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.7:1
|
||||
C0.8:d|C0.8|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.7:1
|
||||
C0.8:l|C0.8|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.7:1
|
||||
C0.8:c|C0.8|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.7:1
|
||||
C0.8:w|C0.8|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.7:1
|
||||
C0.9:m|C0.9|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.8:512^C0.7:256^EON5:21^EON1:6
|
||||
C0.9:b|C0.9|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.8:512^C0.7:256^EON5:43^EON1:9
|
||||
C0.9:d|C0.9|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.8:512^C0.7:256^EON5:7^EON1:3
|
||||
C0.9:l|C0.9|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.8:512^C0.7:256^EON5:60^EON1:11
|
||||
C0.9:c|C0.9|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.8:512^C0.7:256^EON5:252^EON1:19
|
||||
C0.9:w|C0.9|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.8:512^C0.7:256^EON5:260^EON1:26
|
||||
C1.0:m|C1.0|5E+03||||||1|m|0.009|0.01|0.0000625|0.001024||1.6|C0.9:1024^EON5:21^EON1:6
|
||||
C1.0:b|C1.0|5E+03||||||1|b|0.019921875|0.02|0.00003125|0.0025||1|C0.9:1024^EON5:43^EON1:9
|
||||
C1.0:d|C1.0|5E+03||||||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C0.9:1024^EON5:7^EON1:3
|
||||
C1.0:l|C1.0|5E+03||||||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C0.9:1024^EON5:60^EON1:11
|
||||
C1.0:c|C1.0|5E+03||||||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C0.9:1024^EON5:152^EON1:19
|
||||
C1.0:w|C1.0|5E+03||||||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C0.9:1024^EON5:260^EON1:26
|
||||
C1.1:m|C1.1|5E+03||||||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.0:1024^C0.9:256^EON5:21^EON1:6
|
||||
C1.1:b|C1.1|5E+03||||||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.0:1024^C0.9:256^EON5:43^EON1:9
|
||||
C1.1:d|C1.1|5E+03||||||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.0:1024^C0.9:256^EON5:7^EON1:3
|
||||
C1.1:l|C1.1|5E+03||||||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.0:1024^C0.9:256^EON5:60^EON1:11
|
||||
C1.1:c|C1.1|5E+03||||||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.0:1024^C0.9:256^EON5:152^EON1:19
|
||||
C1.1:w|C1.1|5E+03||||||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.0:1024^C0.9:256^EON5:260^EON1:26
|
||||
C1.2:m|C1.2|5E+03||||||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.1:256^C1.0:1024^C0.9:256^EON5:21^EON1:6
|
||||
C1.2:b|C1.2|5E+03||||||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.1:256^C1.0:1024^C0.9:256^EON5:43^EON1:9
|
||||
C1.2:d|C1.2|5E+03||||||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.1:256^C1.0:1024^C0.9:256^EON5:7^EON1:3
|
||||
C1.2:l|C1.2|5E+03||||||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.1:256^C1.0:1024^C0.9:256^EON5:60^EON1:11
|
||||
C1.2:c|C1.2|5E+03||||||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.1:256^C1.0:1024^C0.9:256^EON5:152^EON1:19
|
||||
C1.2:w|C1.2|5E+03||||||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.1:256^C1.0:1024^C0.9:256^EON5:260^EON1:26
|
||||
C1.3:m|C1.3|5E+03||||||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.2:256^C1.0:1024^C0.9:64^EON5:13^EON1:13
|
||||
C1.3:b|C1.3|5E+03||||||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.2:256^C1.0:1024^C0.9:64^EON5:22^EON1:22
|
||||
C1.3:d|C1.3|5E+03||||||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.2:256^C1.0:1024^C0.9:64^EON5:5^EON1:5
|
||||
C1.3:l|C1.3|5E+03||||||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.2:256^C1.0:1024^C0.9:64^EON5:30^EON1:30
|
||||
C1.3:c|C1.3|5E+03||||||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.2:256^C1.0:1024^C0.9:64^EON5:64^EON1:64
|
||||
C1.3:w|C1.3|5E+03||||||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.2:256^C1.0:1024^C0.9:64^EON5:100^EON1:100
|
||||
C1.4:m|C1.4|5E+03||||||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.3:384^C1.0:896^EON5:6^EON1:21
|
||||
C1.4:b|C1.4|5E+03||||||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.3:384^C1.0:896^EON5:9^EON1:43
|
||||
C1.4:d|C1.4|5E+03||||||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.3:384^C1.0:896^EON5:3^EON1:7
|
||||
C1.4:l|C1.4|5E+03||||||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.3:384^C1.0:896^EON5:11^EON1:60
|
||||
C1.4:c|C1.4|5E+03||||||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.3:384^C1.0:896^EON5:19^EON1:152
|
||||
C1.4:w|C1.4|5E+03||||||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.3:384^C1.0:896^EON5:26^EON1:260
|
||||
C1.5:m|C1.5|5E+03||||||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.4:512^C1.0:768^EON5:6^EON1:21
|
||||
C1.5:b|C1.5|5E+03||||||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.4:512^C1.0:768^EON5:9^EON1:43
|
||||
C1.5:d|C1.5|5E+03||||||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.4:512^C1.0:768^EON5:3^EON1:7
|
||||
C1.5:l|C1.5|5E+03||||||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.4:512^C1.0:768^EON5:11^EON1:60
|
||||
C1.5:c|C1.5|5E+03||||||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.4:512^C1.0:768^EON5:19^EON1:152
|
||||
C1.5:w|C1.5|5E+03||||||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.4:512^C1.0:768^EON5:26^EON1:260
|
||||
C1.6:m|C1.6|5E+03|||1|||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.5:768^C1.0:512^EON5:6^RP:21
|
||||
C1.6:b|C1.6|5E+03|||1|||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.5:768^C1.0:512^EON5:9^RP:43
|
||||
C1.6:d|C1.6|5E+03|||1|||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.5:768^C1.0:512^EON5:3^RP:7
|
||||
C1.6:l|C1.6|5E+03|||1|||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.5:768^C1.0:512^EON5:11^RP:60
|
||||
C1.6:c|C1.6|5E+03|||1|||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.5:768^C1.0:512^EON5:19^RP:152
|
||||
C1.6:w|C1.6|5E+03|||1|||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.5:768^C1.0:512^EON5:26^RP:260
|
||||
C1.7:m|C1.7|5E+03|||1|||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.6:896^C1.0:384^EON5:6^RP:21
|
||||
C1.7:b|C1.7|5E+03|||1|||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.6:896^C1.0:384^EON5:9^RP:43
|
||||
C1.7:d|C1.7|5E+03|||1|||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.6:896^C1.0:384^EON5:3^RP:7
|
||||
C1.7:l|C1.7|5E+03|||1|||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.6:896^C1.0:384^EON5:11^RP:60
|
||||
C1.7:c|C1.7|5E+03|||1|||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.6:896^C1.0:384^EON5:19^RP:152
|
||||
C1.7:w|C1.7|5E+03|||1|||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.6:896^C1.0:384^EON5:26^RP:260
|
||||
C1.8:m|C1.8|5E+03|||1|||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.7:1024^C1.0:256^EON5:6^RP:21
|
||||
C1.8:b|C1.8|5E+03|||1|||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.7:1024^C1.0:256^EON5:9^RP:43
|
||||
C1.8:d|C1.8|5E+03|||1|||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.7:1024^C1.0:256^EON5:3^RP:7
|
||||
C1.8:l|C1.8|5E+03|||1|||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.7:1024^C1.0:256^EON5:11^RP:60
|
||||
C1.8:c|C1.8|5E+03|||1|||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.7:1024^C1.0:256^EON5:19^RP:152
|
||||
C1.8:w|C1.8|5E+03|||1|||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.7:1024^C1.0:256^EON5:26^RP:260
|
||||
C1.9:m|C1.9|5E+03|||1|||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.8:1024^C1.0:128^EON5:6^RP:21
|
||||
C1.9:b|C1.9|5E+03|||1|||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.8:1024^C1.0:128^EON5:9^RP:43
|
||||
C1.9:d|C1.9|5E+03|||1|||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.8:1024^C1.0:128^EON5:3^RP:7
|
||||
C1.9:l|C1.9|5E+03|||1|||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.8:1024^C1.0:128^EON5:11^RP:60
|
||||
C1.9:c|C1.9|5E+03|||1|||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.8:1024^C1.0:128^EON5:19^RP:152
|
||||
C1.9:w|C1.9|5E+03|||1|||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.8:1024^C1.0:128^EON5:26^RP:260
|
||||
C2.0:m|C2.0|5E+03|DwStar|1E+00|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C1.9:1024^C1.0:128^RP:28
|
||||
C2.0:b|C2.0|5E+03|DwStar|1E+00|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C1.9:1024^C1.0:128^EON5:9^RP:59
|
||||
C2.0:d|C2.0|5E+03|DwStar|1E+00|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C1.9:1024^C1.0:128^EON5:3^RP:8
|
||||
C2.0:l|C2.0|5E+03|DwStar|1E+00|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C1.9:1024^C1.0:128^EON5:11^RP:85
|
||||
C2.0:c|C2.0|5E+03|DwStar|1E+00|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C1.9:1024^C1.0:128^EON5:19^RP:235
|
||||
C2.0:w|C2.0|5E+03|DwStar|1E+00|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C1.9:1024^C1.0:128^EON5:26^RP:419
|
||||
C2.1:m|C2.1|5E+03|DwStar|1E+01|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.0:1
|
||||
C2.1:b|C2.1|5E+03|DwStar|1E+01|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.0:1
|
||||
C2.1:d|C2.1|5E+03|DwStar|1E+01|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.0:1
|
||||
C2.1:l|C2.1|5E+03|DwStar|1E+01|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.0:1
|
||||
C2.1:c|C2.1|5E+03|DwStar|1E+01|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.0:1
|
||||
C2.1:w|C2.1|5E+03|DwStar|1E+01|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.0:1
|
||||
C2.2:m|C2.2|1E+04|DwStar|1E+02|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.1:1024^C2.0:6
|
||||
C2.2:b|C2.2|1E+04|DwStar|1E+02|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.1:1024^C2.0:9
|
||||
C2.2:d|C2.2|1E+04|DwStar|1E+02|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.1:1024^C2.0:3
|
||||
C2.2:l|C2.2|1E+04|DwStar|1E+02|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.1:1024^C2.0:11
|
||||
C2.2:c|C2.2|1E+04|DwStar|1E+02|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.1:1024^C2.0:19
|
||||
C2.2:w|C2.2|1E+04|DwStar|1E+02|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.1:1024^C2.0:26
|
||||
C2.3:m|C2.3|2E+04|DwStar|1E+03|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.2:1024^C2.1:6
|
||||
C2.3:b|C2.3|2E+04|DwStar|1E+03|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.2:1024^C2.1:9
|
||||
C2.3:d|C2.3|2E+04|DwStar|1E+03|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.2:1024^C2.1:3
|
||||
C2.3:l|C2.3|2E+04|DwStar|1E+03|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.2:1024^C2.1:11
|
||||
C2.3:c|C2.3|2E+04|DwStar|1E+03|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.2:1024^C2.1:19
|
||||
C2.3:w|C2.3|2E+04|DwStar|1E+03|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.2:1024^C2.1:26
|
||||
C2.4:m|C2.4|2E+04|DwStar|1E+04|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.3:1024^C2.2:6
|
||||
C2.4:b|C2.4|2E+04|DwStar|1E+04|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.3:1024^C2.2:9
|
||||
C2.4:d|C2.4|2E+04|DwStar|1E+04|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.3:1024^C2.2:3
|
||||
C2.4:l|C2.4|2E+04|DwStar|1E+04|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.3:1024^C2.2:11
|
||||
C2.4:c|C2.4|2E+04|DwStar|1E+04|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.3:1024^C2.2:19
|
||||
C2.4:w|C2.4|2E+04|DwStar|1E+04|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.3:1024^C2.2:26
|
||||
C2.5:m|C2.5|3E+04|DwStar|1E+05|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.4:1024^C2.3:6
|
||||
C2.5:b|C2.5|3E+04|DwStar|1E+05|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.4:1024^C2.3:9
|
||||
C2.5:d|C2.5|3E+04|DwStar|1E+05|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.4:1024^C2.3:3
|
||||
C2.5:l|C2.5|3E+04|DwStar|1E+05|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.4:1024^C2.3:11
|
||||
C2.5:c|C2.5|3E+04|DwStar|1E+05|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.4:1024^C2.3:19
|
||||
C2.5:w|C2.5|3E+04|DwStar|1E+05|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.4:1024^C2.3:26
|
||||
C2.6:m|C2.6|3E+04|DwStar|1E+06|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.5:1024^C2.4:6
|
||||
C2.6:b|C2.6|3E+04|DwStar|1E+06|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.5:1024^C2.4:9
|
||||
C2.6:d|C2.6|3E+04|DwStar|1E+06|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.5:1024^C2.4:3
|
||||
C2.6:l|C2.6|3E+04|DwStar|1E+06|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.5:1024^C2.4:11
|
||||
C2.6:c|C2.6|3E+04|DwStar|1E+06|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.5:1024^C2.4:19
|
||||
C2.6:w|C2.6|3E+04|DwStar|1E+06|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.5:1024^C2.4:26
|
||||
C2.7:m|C2.7|4E+04|GiStar|1E+07|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.6:1024^C2.5:6
|
||||
C2.7:b|C2.7|4E+04|GiStar|1E+07|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.6:1024^C2.5:9
|
||||
C2.7:d|C2.7|4E+04|GiStar|1E+07|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.6:1024^C2.5:3
|
||||
C2.7:l|C2.7|4E+04|GiStar|1E+07|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.6:1024^C2.5:11
|
||||
C2.7:c|C2.7|4E+04|GiStar|1E+07|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.6:1024^C2.5:19
|
||||
C2.7:w|C2.7|4E+04|GiStar|1E+07|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.6:1024^C2.5:26
|
||||
C2.8:m|C2.8|4E+04|GiStar|1E+08|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.7:1024^C2.6:6
|
||||
C2.8:b|C2.8|4E+04|GiStar|1E+08|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.7:1024^C2.6:9
|
||||
C2.8:d|C2.8|4E+04|GiStar|1E+08|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.7:1024^C2.6:3
|
||||
C2.8:l|C2.8|4E+04|GiStar|1E+08|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.7:1024^C2.6:11
|
||||
C2.8:c|C2.8|4E+04|GiStar|1E+08|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.7:1024^C2.6:19
|
||||
C2.8:w|C2.8|4E+04|GiStar|1E+08|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.7:1024^C2.6:26
|
||||
C2.9:m|C2.9|5E+04|GiStar|1E+09|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.8:1024^C2.7:6
|
||||
C2.9:b|C2.9|5E+04|GiStar|1E+09|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.8:1024^C2.7:9
|
||||
C2.9:d|C2.9|5E+04|GiStar|1E+09|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.8:1024^C2.7:3
|
||||
C2.9:l|C2.9|5E+04|GiStar|1E+09|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.8:1024^C2.7:11
|
||||
C2.9:c|C2.9|5E+04|GiStar|1E+09|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.8:1024^C2.7:19
|
||||
C2.9:w|C2.9|5E+04|GiStar|1E+09|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.8:1024^C2.7:26
|
||||
C3.0:m|C3.0|1E+05|GiStar|1E+10|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C2.9:1024^C2.8:6
|
||||
C3.0:b|C3.0|1E+05|GiStar|1E+10|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C2.9:1024^C2.8:9
|
||||
C3.0:d|C3.0|1E+05|GiStar|1E+10|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.9:1024^C2.8:3
|
||||
C3.0:l|C3.0|1E+05|GiStar|1E+10|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.9:1024^C2.8:11
|
||||
C3.0:c|C3.0|1E+05|GiStar|1E+10|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C2.9:1024^C2.8:19
|
||||
C3.0:w|C3.0|1E+05|GiStar|1E+10|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C2.9:1024^C2.8:26
|
||||
C3.1:m|C3.1|1E+05|GiStar|1E+11|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.0:1024^C2.9:6
|
||||
C3.1:b|C3.1|1E+05|GiStar|1E+11|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.0:1024^C2.9:9
|
||||
C3.1:d|C3.1|1E+05|GiStar|1E+11|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.0:1024^C2.9:3
|
||||
C3.1:l|C3.1|1E+05|GiStar|1E+11|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.0:1024^C2.9:11
|
||||
C3.1:c|C3.1|1E+05|GiStar|1E+11|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.0:1024^C2.9:19
|
||||
C3.1:w|C3.1|1E+05|GiStar|1E+11|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.0:1024^C2.9:26
|
||||
C3.2:m|C3.2|2E+05|WD|1E+12|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.1:1024^C3.0:6
|
||||
C3.2:b|C3.2|2E+05|WD|1E+12|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.1:1024^C3.0:9
|
||||
C3.2:d|C3.2|2E+05|WD|1E+12|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.1:1024^C3.0:3
|
||||
C3.2:l|C3.2|2E+05|WD|1E+12|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.1:1024^C3.0:11
|
||||
C3.2:c|C3.2|2E+05|WD|1E+12|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.1:1024^C3.0:19
|
||||
C3.2:w|C3.2|2E+05|WD|1E+12|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.1:1024^C3.0:26
|
||||
C3.3:m|C3.3|3E+05|WD|1E+13|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.2:1024^C3.1:6
|
||||
C3.3:b|C3.3|3E+05|WD|1E+13|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.2:1024^C3.1:9
|
||||
C3.3:d|C3.3|3E+05|WD|1E+13|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.2:1024^C3.1:3
|
||||
C3.3:l|C3.3|3E+05|WD|1E+13|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.2:1024^C3.1:11
|
||||
C3.3:c|C3.3|3E+05|WD|1E+13|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.2:1024^C3.1:19
|
||||
C3.3:w|C3.3|3E+05|WD|1E+13|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.2:1024^C3.1:26
|
||||
C3.4:m|C3.4|4E+05|WD|1E+14|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.3:1024^C3.2:6
|
||||
C3.4:b|C3.4|4E+05|WD|1E+14|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.3:1024^C3.2:9
|
||||
C3.4:d|C3.4|4E+05|WD|1E+14|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.3:1024^C3.2:3
|
||||
C3.4:l|C3.4|4E+05|WD|1E+14|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.3:1024^C3.2:11
|
||||
C3.4:c|C3.4|4E+05|WD|1E+14|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.3:1024^C3.2:19
|
||||
C3.4:w|C3.4|4E+05|WD|1E+14|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.3:1024^C3.2:26
|
||||
C3.5:m|C3.5|5E+05|NS|1E+15|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.4:1024^C3.3:6
|
||||
C3.5:b|C3.5|5E+05|NS|1E+15|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.4:1024^C3.3:9
|
||||
C3.5:d|C3.5|5E+05|NS|1E+15|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.4:1024^C3.3:3
|
||||
C3.5:l|C3.5|5E+05|NS|1E+15|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.4:1024^C3.3:11
|
||||
C3.5:c|C3.5|5E+05|NS|1E+15|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.4:1024^C3.3:19
|
||||
C3.5:w|C3.5|5E+05|NS|1E+15|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.4:1024^C3.3:26
|
||||
C3.6:m|C3.6|6E+05|NS|1E+16|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.5:1024^C3.4:6
|
||||
C3.6:b|C3.6|6E+05|NS|1E+16|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.5:1024^C3.4:9
|
||||
C3.6:d|C3.6|6E+05|NS|1E+16|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.5:1024^C3.4:3
|
||||
C3.6:l|C3.6|6E+05|NS|1E+16|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.5:1024^C3.4:11
|
||||
C3.6:c|C3.6|6E+05|NS|1E+16|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.5:1024^C3.4:19
|
||||
C3.6:w|C3.6|6E+05|NS|1E+16|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.5:1024^C3.4:26
|
||||
C3.7:m|C3.7|7E+05|MGT|1E+17|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.6:1024^C3.5:6
|
||||
C3.7:b|C3.7|7E+05|MGT|1E+17|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.6:1024^C3.5:9
|
||||
C3.7:d|C3.7|7E+05|MGT|1E+17|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.6:1024^C3.5:3
|
||||
C3.7:l|C3.7|7E+05|MGT|1E+17|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.6:1024^C3.5:11
|
||||
C3.7:c|C3.7|7E+05|MGT|1E+17|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.6:1024^C3.5:19
|
||||
C3.7:w|C3.7|7E+05|MGT|1E+17|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.6:1024^C3.5:26
|
||||
C3.8:m|C3.8|8E+05|MGT|1E+18|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.7:1024^C3.6:6
|
||||
C3.8:b|C3.8|8E+05|MGT|1E+18|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.7:1024^C3.6:9
|
||||
C3.8:d|C3.8|8E+05|MGT|1E+18|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.7:1024^C3.6:3
|
||||
C3.8:l|C3.8|8E+05|MGT|1E+18|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.7:1024^C3.6:11
|
||||
C3.8:c|C3.8|8E+05|MGT|1E+18|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.7:1024^C3.6:19
|
||||
C3.8:w|C3.8|8E+05|MGT|1E+18|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.7:1024^C3.6:26
|
||||
C3.9:m|C3.9|9E+05|BH|1E+19|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.8:1024^C3.7:6
|
||||
C3.9:b|C3.9|9E+05|BH|1E+19|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.8:1024^C3.7:9
|
||||
C3.9:d|C3.9|9E+05|BH|1E+19|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.8:1024^C3.7:3
|
||||
C3.9:l|C3.9|9E+05|BH|1E+19|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.8:1024^C3.7:11
|
||||
C3.9:c|C3.9|9E+05|BH|1E+19|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.8:1024^C3.7:19
|
||||
C3.9:w|C3.9|9E+05|BH|1E+19|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.8:1024^C3.7:26
|
||||
C4.0:m|C4.0|2E+06|BH|1E+20|1|1|1|1|m|0.00675|0.01|0.0005|0.00128|1|1.6|C3.9:1024^C3.8:6
|
||||
C4.0:b|C4.0|2E+06|BH|1E+20|1|1|1|1|b|0.02390625|0.015625|0.00003125|0.0025|0.64|1|C3.9:1024^C3.8:9
|
||||
C4.0:d|C4.0|2E+06|BH|1E+20|1|1|1|1|d|0.0144|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.9:1024^C3.8:3
|
||||
C4.0:l|C4.0|2E+06|BH|1E+20|1|1|1|1|l|0.0648|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.9:1024^C3.8:11
|
||||
C4.0:c|C4.0|2E+06|BH|1E+20|1|1|1|1|c|0.027|0.04166666667|0.004096|0.006103515625|1|0.8|C3.9:1024^C3.8:19
|
||||
C4.0:w|C4.0|2E+06|BH|1E+20|1|1|1|1|w|0.03375|0.025|0.000256|0.03072|0.25|1.25|C3.9:1024^C3.8:26
|
||||
C4.1:m|C4.1|2E+06|BH|1E+21|1|1|1|1|m|0.00675|0.01|0.0005|0.00128|1|1.6|C4.0:1024^C3.9:6
|
||||
C4.1:b|C4.1|2E+06|BH|1E+21|1|1|1|1|b|0.02390625|0.015625|0.00003125|0.0025|0.64|1|C4.0:1024^C3.9:9
|
||||
C4.1:d|C4.1|2E+06|BH|1E+21|1|1|1|1|d|0.0144|0.01923076923|0.0009765625|0.0003125|2.56|1|C4.0:1024^C3.9:3
|
||||
C4.1:l|C4.1|2E+06|BH|1E+21|1|1|1|1|l|0.0648|0.04464285714|0.0005|0.0025|1.5625|0.5|C4.0:1024^C3.9:11
|
||||
C4.1:c|C4.1|2E+06|BH|1E+21|1|1|1|1|c|0.027|0.04166666667|0.004096|0.006103515625|1|0.8|C4.0:1024^C3.9:19
|
||||
C4.1:w|C4.1|2E+06|BH|1E+21|1|1|1|1|w|0.03375|0.025|0.000256|0.03072|0.25|1.25|C4.0:1024^C3.9:26
|
||||
C4.2:m|C4.2|4E+06|SMBH|1E+22|1|1|1|1|m|0.00675|0.01|0.0005|0.00128|1|1.6|C4.1:1024^C4.0:6
|
||||
C4.2:b|C4.2|4E+06|SMBH|1E+22|1|1|1|1|b|0.02390625|0.015625|0.00003125|0.0025|0.64|1|C4.1:1024^C4.0:9
|
||||
C4.2:d|C4.2|4E+06|SMBH|1E+22|1|1|1|1|d|0.0144|0.01923076923|0.0009765625|0.0003125|2.56|1|C4.1:1024^C4.0:3
|
||||
C4.2:l|C4.2|4E+06|SMBH|1E+22|1|1|1|1|l|0.0648|0.04464285714|0.0005|0.0025|1.5625|0.5|C4.1:1024^C4.0:11
|
||||
C4.2:c|C4.2|4E+06|SMBH|1E+22|1|1|1|1|c|0.027|0.04166666667|0.004096|0.006103515625|1|0.8|C4.1:1024^C4.0:19
|
||||
C4.2:w|C4.2|4E+06|SMBH|1E+22|1|1|1|1|w|0.03375|0.025|0.000256|0.03072|0.25|1.25|C4.1:1024^C4.0:26
|
||||
C4.3:m|C4.3|6E+06|SMBH|1E+23|1|1|1|1|m|0.00675|0.01|0.0005|0.00128|1|1.6|C4.2:1024^C4.1:6
|
||||
C4.3:b|C4.3|6E+06|SMBH|1E+23|1|1|1|1|b|0.02390625|0.015625|0.00003125|0.0025|0.64|1|C4.2:1024^C4.1:9
|
||||
C4.3:d|C4.3|6E+06|SMBH|1E+23|1|1|1|1|d|0.0144|0.01923076923|0.0009765625|0.0003125|2.56|1|C4.2:1024^C4.1:3
|
||||
C4.3:l|C4.3|6E+06|SMBH|1E+23|1|1|1|1|l|0.0648|0.04464285714|0.0005|0.0025|1.5625|0.5|C4.2:1024^C4.1:11
|
||||
C4.3:c|C4.3|6E+06|SMBH|1E+23|1|1|1|1|c|0.027|0.04166666667|0.004096|0.006103515625|1|0.8|C4.2:1024^C4.1:19
|
||||
C4.3:w|C4.3|6E+06|SMBH|1E+23|1|1|1|1|w|0.03375|0.025|0.000256|0.03072|0.25|1.25|C4.2:1024^C4.1:26
|
||||
C4.4:m|C4.4|8E+06|SMBH|1E+24|1|1|1|1|m|0.00675|0.01|0.0005|0.00128|1|1.6|C4.3:1024^C4.2:6
|
||||
C4.4:b|C4.4|8E+06|SMBH|1E+24|1|1|1|1|b|0.02390625|0.015625|0.00003125|0.0025|0.64|1|C4.3:1024^C4.2:9
|
||||
C4.4:d|C4.4|8E+06|SMBH|1E+24|1|1|1|1|d|0.0144|0.01923076923|0.0009765625|0.0003125|2.56|1|C4.3:1024^C4.2:3
|
||||
C4.4:l|C4.4|8E+06|SMBH|1E+24|1|1|1|1|l|0.0648|0.04464285714|0.0005|0.0025|1.5625|0.5|C4.3:1024^C4.2:11
|
||||
C4.4:c|C4.4|8E+06|SMBH|1E+24|1|1|1|1|c|0.027|0.04166666667|0.004096|0.006103515625|1|0.8|C4.3:1024^C4.2:19
|
||||
C4.4:w|C4.4|8E+06|SMBH|1E+24|1|1|1|1|w|0.03375|0.025|0.000256|0.03072|0.25|1.25|C4.3:1024^C4.2:26
|
||||
@@ -0,0 +1,9 @@
|
||||
id|span|progress|degradation|next|fall
|
||||
HP|5E+06|0.00025|0.0025|EON1|RP:1
|
||||
EON1|1E+07|0.00025|0.0025|EON2|HP:100^RP:1
|
||||
EON2|1E+07|0.00025|0.0025|EON3|EON1:1
|
||||
EON3|1E+06|0.00025|0.0025|EON4|EON2:1000^EON1:1
|
||||
EON4|1E+06|0.00025|0.0025|EON5|EON3:1000^EON1:1
|
||||
EON5|5E+05|0.00025|0.0025|C0.0|EON4:2000^EON1:1
|
||||
C0.0|2E+03|0.0175|0.025|C0.1|EON5:1
|
||||
C0.1|2E+03|0.0175|0.025|C0.2|C0.0:1
|
||||
@@ -0,0 +1,3 @@
|
||||
[remap]
|
||||
|
||||
importer="keep"
|
||||
@@ -0,0 +1,3 @@
|
||||
[remap]
|
||||
|
||||
importer="keep"
|
||||
@@ -1,6 +1,6 @@
|
||||
id|is_visible|unlock|cost|bind|mass|space|lifespan|auto|gamma|blast|life_product|voids|output|destroy_level|degradation|planets|count_as|group_as|event_group|blastability|space_bonus|civ_bonus|claim
|
||||
S|1|S:0|S:0||||||||||||||||S||||
|
||||
H|1|H:0|H:0||||||||||||||||H||||
|
||||
S|1|S:0|E:1||||||||||||||||S||||
|
||||
H|1|H:0|E:1||||||||||||||||H||||
|
||||
He|1|He:1|||||||||||||||||||||
|
||||
Met|1|Met:1|||||||||||||||||||||
|
||||
BD|1|BD:1|||||||||||||||||||||
|
||||
@@ -51,25 +51,25 @@ STN|1|G:1|H:200&G:1||211|300|30000000|0.5|||H:3000||H:211&S:320|||||CL|CL||||
|
||||
MC|1|G:1&S:1300|H:2000&G:1||2011|4000|300000000||||H:90000.||H:2011&S:4020|||||CL|CL||||
|
||||
ASTN|1|MC:1&S:60000|H:200000&MC:1||202011|200000|100000000|0.65|||H:2000000||H:202011&S:204020|||||CL|CL||||
|
||||
SCD|1|Star:300||Star:1000|||10000000|||||||||||SC|SC||0.1|4|C2.3
|
||||
SCS|1|SCD:1&Star:2000||Star:10000&BH:10|||1000000000|||||||||||SC|SC||0.2|44350|C2.4
|
||||
SCM|1|SCS:1&Star:20000||Star:100000&BH:100|||3000000000|||||||||||SC|SC||0.3|44289|C2.5
|
||||
SCL|1|SCM:1&Star:200000||Star:1000000&BH:1000|||10000000000|||||||||||SC|SC||0.5|44230|C2.6
|
||||
SCS|1|SCD:1&Star:2000||Star:10000&BH:10|||1000000000|||||||||||SC|SC||0.2|3.6|C2.4
|
||||
SCM|1|SCS:1&Star:20000||Star:100000&BH:100|||3000000000|||||||||||SC|SC||0.3|3.4|C2.5
|
||||
SCL|1|SCM:1&Star:200000||Star:1000000&BH:1000|||10000000000|||||||||||SC|SC||0.5|3.2|C2.6
|
||||
SCG|1|SCL:1&Star:2000000||Star:10000000&BH:10000|||30000000000|||||||||||SC|SC||0.8|3|C2.7
|
||||
GED|1|SMBH:1&SC:1|SMBH:10000|Star:1000000000&BH:1000000&SC:100|||12000000000|||||||||||GAL|GALE||1|44440|C2.9
|
||||
GES|1|GED:1&Star:2000000000|SMBH:100000|Star:10000000000&BH:10000000&SC:1000|||13000000000|||||||||||GAL|GALE||1.5|44409|C3.0
|
||||
GEM|1|GES:1&Star:20000000000|SMBH:1000000|Star:100000000000&BH:100000000&SC:10000|||15000000000|||||||||||GAL|GALE||2|44378|C3.1
|
||||
GEL|1|GEM:1&Star:200000000000|SMBH:10000000|Star:1000000000000&BH:1000000000&SC:100000|||17000000000|||||||||||GAL|GALE||3|44348|C3.2
|
||||
GEG|1|GEL:1&Star:2000000000000|SMBH:100000000|Star:10000000000000&BH:10000000000&SC:1000000|||20000000000|||||||||||GAL|GALE||4.5|44317|C3.3
|
||||
GSD|1|GED:1|STN:10000&ASTN:10|GED:1||-5240200|2000000000|0.8|||H:2400000000||H:4112110|||||GAL|GALS|||44318|C2.9
|
||||
GSS|1|GES:1|STN:100000&ASTN:100|GES:1||-52402000|3000000000|0.8|||H:36000000000||H:41121100|||||GAL|GALS|||44257|C3.0
|
||||
GSM|1|GEM:1|STN:1000000&ASTN:1000|GEM:1||-524020000|4000000000|0.8|||H:480000000000||H:411211000|||||GAL|GALS|||44229|C3.1
|
||||
GSL|1|GEL:1|STN:10000000&ASTN:10000|GEL:1||-5240200000|6000000000|0.8|||H:7200000000000||H:4112110000|||||GAL|GALS|||44198|C3.2
|
||||
GED|1|SMBH:1&SC:1|SMBH:10000|Star:1000000000&BH:1000000&SC:100|||12000000000|||||||||||GAL|GALE||1|1.9|C2.9
|
||||
GES|1|GED:1&Star:2000000000|SMBH:100000|Star:10000000000&BH:10000000&SC:1000|||13000000000|||||||||||GAL|GALE||1.5|1.8|C3.0
|
||||
GEM|1|GES:1&Star:20000000000|SMBH:1000000|Star:100000000000&BH:100000000&SC:10000|||15000000000|||||||||||GAL|GALE||2|1.7|C3.1
|
||||
GEL|1|GEM:1&Star:200000000000|SMBH:10000000|Star:1000000000000&BH:1000000000&SC:100000|||17000000000|||||||||||GAL|GALE||3|1.6|C3.2
|
||||
GEG|1|GEL:1&Star:2000000000000|SMBH:100000000|Star:10000000000000&BH:10000000000&SC:1000000|||20000000000|||||||||||GAL|GALE||4.5|1.5|C3.3
|
||||
GSD|1|GED:1|STN:10000&ASTN:10|GED:1||-5240200|2000000000|0.8|||H:2400000000||H:4112110|||||GAL|GALS|||2.5|C2.9
|
||||
GSS|1|GES:1|STN:100000&ASTN:100|GES:1||-52402000|3000000000|0.8|||H:36000000000||H:41121100|||||GAL|GALS|||2.3|C3.0
|
||||
GSM|1|GEM:1|STN:1000000&ASTN:1000|GEM:1||-524020000|4000000000|0.8|||H:480000000000||H:411211000|||||GAL|GALS|||2.2|C3.1
|
||||
GSL|1|GEL:1|STN:10000000&ASTN:10000|GEL:1||-5240200000|6000000000|0.8|||H:7200000000000||H:4112110000|||||GAL|GALS|||2.1|C3.2
|
||||
GSG|1|GEG:1|STN:100000000&ASTN:100000|GEG:1||-52402000000|9000000000|0.8|||H:108000000000000||H:41121100000|||||GAL|GALS|||2|C3.3
|
||||
LGG|1|GAL:20||GAL:50|||10000000000|||||||||||LS|LS|||44317|C3.4
|
||||
LSC|1|LGG:1&GAL:100||LGG:200|||20000000000|||||||||||LS|LS|||44287|C3.6
|
||||
LGF|1|LSC:1&LGG:400||LSC:100|||30000000000|||||5||||||LS|LS|||44256|C3.8
|
||||
LGW|1|LGF:1&LSC:200||LGF:1000|||60000000000|||||10000||||||LS|LS|||44228|C4.1
|
||||
LUB|1|LGW:1&LGF:2000||LGW:1000|||100000000000|||||1000000000||||||LS|LS|||44197|C4.4
|
||||
LGG|1|GAL:20||GAL:50|||10000000000|||||||||||LS|LS|||1.5|C3.4
|
||||
LSC|1|LGG:1&GAL:100||LGG:200|||20000000000|||||||||||LS|LS|||1.4|C3.6
|
||||
LGF|1|LSC:1&LGG:400||LSC:100|||30000000000|||||5||||||LS|LS|||1.3|C3.8
|
||||
LGW|1|LGF:1&LSC:200||LGF:1000|||60000000000|||||10000||||||LS|LS|||1.2|C4.1
|
||||
LUB|1|LGW:1&LGF:2000||LGW:1000|||100000000000|||||1000000000||||||LS|LS|||1.1|C4.4
|
||||
GG|1|GG:1||||||||||||||||Planet|BPlanet||||
|
||||
AB|1|AB:1||||||||||||||||Planet|BPlanet||||
|
||||
RP|1|RP:1||||||||||||||||Planet|BPlanet||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[remap]
|
||||
|
||||
importer="keep"
|
||||
@@ -0,0 +1,3 @@
|
||||
[remap]
|
||||
|
||||
importer="keep"
|
||||
@@ -725,7 +725,7 @@ Aggression.Description|Evolution and competition.[p]%value0%: %value1%|scale|amo
|
||||
Trends.Description|Actions of advanced civilizations.[p]Antitheism: %value0%[p]Integration: %value1%[p]Xenocide: %value2%[p]Reconstruction: %value3%[p]Spirituality: %value4%[p]|antitheism|integration|xenocide|reconstruction|spirituality
|
||||
S.Description|%value0%[p]Voids: %value1%[p]Needed for placing objects.|amount|voids|||
|
||||
H.Description|%value0%[p]Used to create clouds and stars.|amount||||
|
||||
He.Description|%value0%[p]Saves hydrogen.%value1%|amount he / h||||
|
||||
He.Description|%value0%[p]Saves hydrogen.%value1%|amount|he / h|||
|
||||
Met.Description|%value0%[p]Needed to create planets.[p]Metallicity: %value1%|amount|metallicity|||
|
||||
N.Description|%value0%[p]Produces hydrogen.[p]%value2%[p]Baseline lifespan: %value3%|amount||colored costs|lifespan|
|
||||
G.Description|%value0%[p]Produces more hydrogen.[p]%value2%[p]Baseline lifespan: %value3%|amount||colored costs|lifespan|
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[remap]
|
||||
|
||||
importer="keep"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
[remap]
|
||||
|
||||
importer="skip"
|
||||
+14
-8
@@ -1,14 +1,17 @@
|
||||
[runnable_presets]
|
||||
|
||||
"Windows Desktop"="Windows Desktop"
|
||||
|
||||
[preset.0]
|
||||
|
||||
name="Windows Desktop"
|
||||
platform="Windows Desktop"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="../../Documents/Owad/Owniverse.I/Builds/owniverse.0.6.13-129.exe"
|
||||
export_path="../../../Documents/Owad/Owniverse.I/Builds/owniverse.0.7.0-146.console.exe"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
@@ -27,7 +30,7 @@ script_export_mode=2
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
binary_format/embed_pck=false
|
||||
binary_format/embed_pck=true
|
||||
texture_format/s3tc_bptc=true
|
||||
texture_format/etc2_astc=false
|
||||
shader_baker/enabled=false
|
||||
@@ -74,7 +77,6 @@ Remove-Item -Recurse -Force '{temp_dir}'"
|
||||
|
||||
name="Android"
|
||||
platform="Android"
|
||||
runnable=false
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
@@ -110,7 +112,7 @@ architectures/armeabi-v7a=false
|
||||
architectures/arm64-v8a=true
|
||||
architectures/x86=false
|
||||
architectures/x86_64=false
|
||||
version/code=138
|
||||
version/code=146
|
||||
version/name=""
|
||||
package/unique_name="com.KIDGameProduction.Owniverse"
|
||||
package/name="Owniverse"
|
||||
@@ -136,11 +138,12 @@ screen/support_normal=true
|
||||
screen/support_large=true
|
||||
screen/support_xlarge=true
|
||||
screen/background_color=Color(0, 0, 0, 1)
|
||||
splash_screen/disable_godot_boot_splash=false
|
||||
splash_screen/icon=""
|
||||
splash_screen/branding_image=""
|
||||
splash_screen/background_color=Color(0, 0, 0, 1)
|
||||
user_data_backup/allow=false
|
||||
command_line/extra_args=""
|
||||
apk_expansion/enable=false
|
||||
apk_expansion/SALT=""
|
||||
apk_expansion/public_key=""
|
||||
permissions/custom_permissions=PackedStringArray()
|
||||
permissions/access_checkin_properties=false
|
||||
permissions/access_coarse_location=false
|
||||
@@ -295,3 +298,6 @@ permissions/write_sms=false
|
||||
permissions/write_social_stream=false
|
||||
permissions/write_sync_settings=false
|
||||
permissions/write_user_dictionary=false
|
||||
apk_expansion/enable=false
|
||||
apk_expansion/SALT=""
|
||||
apk_expansion/public_key=""
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
+3
-3
@@ -8,14 +8,14 @@
|
||||
|
||||
config_version=5
|
||||
|
||||
[animation]
|
||||
[accessibility]
|
||||
|
||||
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
||||
general/updates_per_second=10
|
||||
|
||||
[application]
|
||||
|
||||
config/name="Owniverse"
|
||||
config/version="0.6.22"
|
||||
config/version="0.7.0"
|
||||
run/main_scene="uid://bv384dpmvjv8o"
|
||||
config/features=PackedStringArray("4.7", "Mobile")
|
||||
boot_splash/show_image=false
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# Эволюция звёзд Owniverse.GI
|
||||
|
||||
Механики перенесены из Owniverse.U с согласованными изменениями GI: постепенные партии, детерминированные дробные остатки, уязвимость цивилизаций до C1.2 и передача выживших планет преемнику. Баланс таблицы GI сохраняется.
|
||||
|
||||
## Порядок расчёта
|
||||
|
||||
1. Накопить игровое время; обработать новые десятые доли года.
|
||||
2. Рассчитать производство существующих объектов за интервал с учётом их постепенного исчезновения; выполнить автоматическое создание звёзд и их планет.
|
||||
3. Для очередного поколения определить долю заканчивающих жизнь звёзд. Применить локальное разрушение их планет до списания партий.
|
||||
4. Снять целые объекты из партий, рассчитать основные и альтернативные выходы с накоплением дробей.
|
||||
5. Передать выжившие планеты полученным звёздам/остаткам. Если преемников несколько, распределить по их фактическим количествам; если преемника нет — освободить. Остальные связанные объекты используют существующие правила GI.
|
||||
6. Создать партии преемников. Учесть производство новорождённых за оставшуюся часть длинного шага, включая излучение короткоживущих сверхновых.
|
||||
7. Применить суммарный бласт поколения; повторять обработку поколений, пока в интервале остаются переходы.
|
||||
8. Пересчитать группы и опубликовать состояние.
|
||||
|
||||
## Автоматическое создание бурых карликов
|
||||
|
||||
На горячие бурые карлики `HBD` выделяется 0,1% **нового фактически произведённого автоматического водорода** за расчётный интервал. По текущей таблице автоматическую долю имеют STN (`auto = 0.5`) и ASTN (`auto = 0.65`). Обычный водород, ручные покупки и неиспользованный запас прошлых интервалов повторно в эту долю не входят.
|
||||
|
||||
При стоимости `c = 0.1 H` квота объектов увеличивается на `ΔQ = ΔH_auto × 0.001 / c`. Она хранится между циклами и в сохранении в `auto_star_deficits.HBD`. Доступность ограничивается автоматическим запасом и уже оплаченным топливом: `Q ≤ (auto_resources.H + HBD_fuel) / c`.
|
||||
|
||||
Карлики появляются по одному. Для `q = max(0, Q)` создаётся `floor(q)` объектов и ещё один с вероятностью `q - floor(q)`, с ограничением по доступному топливу. Созданное количество вычитается из Q; отрицательный остаток компенсирует досрочное появление на следующих шагах. Повторного случайного округления без нового автоматического водорода нет. Это исключение из общего детерминированного округления GI. Состояние генератора `auto_star_deficits.HBD_random` сохраняется, поэтому загрузка продолжает ту же последовательность.
|
||||
|
||||
Водород списывается целыми единицами авансом: `charge = ceil(max(0, count × c - HBD_fuel))`. Неиспользованный остаток `HBD_fuel += charge - count × c` сохраняется в `auto_star_deficits.HBD_fuel` и больше не входит в свободный запас или бюджет обычных звёзд. Например, 500 автоматических H позволяют создать 5 HBD, списав 1 H и оставив 0,5 H оплаченного топлива. Следующие 5 HBD используют этот остаток без повторного списания. Численность всегда целая, но не обязана быть кратной 10. При отсутствии обычных звёзд их доля остаётся в запасе, а производство HBD всё равно работает.
|
||||
|
||||
Новые HBD получают обычные жизненные партии. По таблице их срок — 5 миллиардов лет, затем они постепенно превращаются в холодные `CBD`. На экране оба типа суммируются в `BD`. Ручного рецепта у общего счётчика BD нет. Миграция сохранений не требуется: квота использует существующий словарь, отсутствующее значение равно нулю.
|
||||
|
||||
## Постепенные партии
|
||||
|
||||
Окно рождения шириной `w = lifespan / 40` при `lifespan > 10`, иначе `w = lifespan`. Окно превращения сдвинуто на `lifespan`. При частичном списании:
|
||||
|
||||
```
|
||||
expected = remainder + (remaining_count - remainder) * elapsed / remaining_window
|
||||
converted = floor(expected)
|
||||
remainder = expected - converted
|
||||
```
|
||||
|
||||
Конец окна списывает остаток. Переход не уничтожает всю партию в начале окна. Начала и длительности сохраняют модель GI, включая годовую длительность событий SN/GRB.
|
||||
|
||||
## Планеты
|
||||
|
||||
При ручном и автоматическом создании звёзд:
|
||||
|
||||
```
|
||||
expected = stars_created * planet_amount * planet_chance * metallicity + remainder
|
||||
planets_created = floor(expected)
|
||||
remainder = expected - planets_created
|
||||
```
|
||||
|
||||
Остаток независим для каждой пары звезда/планета и сохраняется. Планеты сразу связаны со звездой. Эволюционный переход сам по себе не создаёт новую планетную систему. Шансы планет в таблице — доли, а проценты альтернативных выходов звёзд делятся на 100.
|
||||
|
||||
## Среда: адаптация формул Unity к состоянию GI
|
||||
|
||||
GI не хранит накопленные счётчики первичной материи Unity. Поэтому `stellar_environment()` вычисляет среду из текущего физического состава, включая связанные объекты, без суммирующих групп интерфейса. Это приближение для баланса GI, а не побитовое воспроизведение Unity.
|
||||
|
||||
- `M` — свободные и связанные H/He/Met плюс табличная масса конкретных объектов и масса SMBH.
|
||||
- `V` — физическое S плюс табличный объём объектов и `voids * 1e22`.
|
||||
- `Z = max(0, 0.4 * ln(100 * free_Met / M + 0.11) + 0.88)`; при `M = 0` металличность нулевая.
|
||||
- `B = 65536 * f(M)`, где `f(0)=1`, `f(M)=M^0.1` при `0<M<1e10`, иначе `10`.
|
||||
- `density = 1.23456e-7 * sqrt(B + M) / max(1, V)^(1/6)`.
|
||||
- `blast_factor = clamp(free_S / V, 0, 1)`; при нулевом объёме `1`.
|
||||
|
||||
Система сил Unity и планетные бонусы в GI пока не перенесены; здесь используются нейтральные множители 1. Металличность для производства фиксируется до расходования ресурсов на звёзды. Среда бласта фиксируется для поколения до добавления его продуктов.
|
||||
|
||||
## Разрушение и бласты
|
||||
|
||||
Локальное воздействие: `ratio = dying_stars / physical_stars`, без множителя плотности.
|
||||
|
||||
Бласт: `hits = floor(power * blast_factor + saved_remainder)`. Распределение экспозиции между типами звёзд, имеющими планетные системы в таблице, пропорционально их физическому количеству. Для типа: `ratio = hits * star_share / physical_stars * density`.
|
||||
|
||||
Для каждого связанного типа планет накапливается `held_planets * min(1, ratio)`. Уничтожается целая часть, не больше существующего количества. Остатки локального воздействия и бластов раздельны. Это позволяет слабым воздействиям работать при постепенной эволюции.
|
||||
|
||||
Уровень 1 превращает только `blastability=1` в RP (HP, EON, цивилизации до C1.2). C1.3 и выше защищены от этого уровня. Уровень 2 уничтожает все планеты, включая защищённые цивилизации, без создания RP. Уничтоженные объекты удаляются из жизненных партий, чтобы не возникать заново.
|
||||
|
||||
`blast` в выходе — событие, а не предмет. Поле `blast` сущности повторно не суммируется с выходом `blast:N`.
|
||||
|
||||
## Границы этапа
|
||||
|
||||
Рассчитываются агрегаты по типам, а не отдельные звёздные системы; конкретная принадлежность каждой планеты отдельной звезде не моделируется. Распределения целых объектов детерминированы. Порядок внутри длинного шага — по поколениям, поэтому нелинейное воздействие бластов и среды не обязано совпадать с серией более коротких шагов. Эволюция HP и эонов подключена ниже; эволюция цивилизаций и бонусы структур описаны ниже. Глобальная система сил остаётся отдельной механикой.
|
||||
|
||||
Проверки: `tests/test_time.gd`, `tests/test_stellar_rules.gd`. Сохранения игрока тестами не затрагиваются.
|
||||
|
||||
## Первичная жизнь: HP → EON1 → … → EON5 → C0.0
|
||||
|
||||
Правила хранятся в `data/life_rules.txt`, исходные сроки и коэффициенты взяты из Unity `civs.txt`. В каждой обработанной части партии:
|
||||
|
||||
- прогресс: `count * progress + carry_progress`;
|
||||
- деградация: `count * degradation + carry_degradation`;
|
||||
- остальная жизнь остаётся на прежней стадии и получает новую партию для следующей попытки.
|
||||
|
||||
Прогресс и деградация округляются вниз с сохранением остатков. Если оба накопленных исхода конкурируют за последнюю планету, распределяются только существующие объекты, а неиспользованный остаток переносится на следующую попытку. Деградация распределяется по весам `fall`; знаковые ошибки распределения также сохраняются.
|
||||
|
||||
Для всех шести стадий базовые доли за попытку — `progress=0.00025`, `degradation=0.0025`. Интервалы HP/EON1/EON2/EON3/EON4/EON5: 5 млн, 10 млн, 10 млн, 1 млн, 1 млн, 500 тыс. лет. Используются постепенные окна GI шириной `span / 40`. Случайные множители Unity не применяются; агрессия, порядок и просвещение пока имеют нейтральное влияние.
|
||||
|
||||
HP деградирует в RP. У эонов возможен откат к предыдущим стадиям по таблице. RP не развивается сам. C0.0 и C0.1 также развиваются: период 2000 лет, прогресс 0.0175, деградация 0.025. Переходы C0.0 → C0.1 → C0.2, откат C0.0 → EON5 и C0.1 → C0.0. Развитие C0.2 и выше подключено через этические правила. Конфликт civ_bonus устранён повторным импортом: бонусы структур совпадают с Unity. Для существующих этических цивилизаций пользователь выбрал равномерное распределение между шестью этиками с детерминированным целым остатком.
|
||||
|
||||
Переходы сохраняют количество планет и их хозяев, включая передачу преемнику звезды и разрушение бластом. Повторная попытка той же стадии не увеличивает статистику созданных объектов. Партии новых планет регистрируются при создании; недостающие партии старых HP/эонов — однократно при загрузке с текущим возрастом вселенной, без выдумывания их прошлого возраста. Состояние содержит `life_version=2` (версия 1 также загружается) и `life_remainders`.
|
||||
|
||||
Миграция версии жизни 1 → 2 добавляет только недостающие партии C0.0/C0.1, сохраняя уже существующие партии HP и эонов.
|
||||
|
||||
## Этические и космические цивилизации
|
||||
|
||||
`civilization_rules.txt` содержит 43 стадии по шесть этик из Unity. Они загружаются в `Global.CivilizationRules` и участвуют в расчёте партий. После C4.4 следует Ct: конечная стадия без новых жизненных партий.
|
||||
|
||||
Состояние `ethics[stage][ethic]` хранит целые количества. Для существующих цивилизаций без этик количество распределяется поровну в порядке m, l, w, d, c, b; остаток распределяется по одному в этом порядке. Существующее распределение сохраняется. При внешних потерях оно пропорционально сокращается.
|
||||
|
||||
Цивилизации с непустым `home` (C2.0–C4.4) освобождаются от прежних привязок без изменения общего количества. Общий механизм переходов больше не переносит планетную привязку на такого преемника. Структуры назначаются отдельно в `civilization_homes`; при возвращении к планетным стадиям цивилизации размещаются у имеющихся карликовых звёзд.
|
||||
|
||||
Правило среды согласовано: число космических цивилизаций ограничено звёздным ресурсом. Для стадии `i` расход ёмкости — `population[i] * max(1, Stars[i])`. Каждое семейство `Home` имеет общий бюджет для всех использующих его стадий; звёзды внутри структур также учитываются, но повторно не суммируются. SMBH учитывается по суммарной массе в эквиваленте звёзд, согласно решению пользователя.
|
||||
|
||||
Переход переиспользует резерв исходной стадии. При нехватке ёмкости неподдержанные переходы остаются на исходной стадии. Лимит применяется к общему механизму выходов партий и ручному созданию; расчёт прогресса C0.2+ использует тот же бюджет.
|
||||
|
||||
При загрузке и после эволюционных поколений избыток деградирует последовательно до поддерживаемой стадии. Приоритет получают существующие более развитые цивилизации; остаток бюджета достаётся младшим. Деградация переносит количество и этики, сокращает старые партии и регистрирует новые, не уничтожая цивилизации. C2.0 при избытке становится C1.9 и распределяется по имеющимся звёздам-карликам. Если карликов нет, C1.9 остаётся свободной — отсутствующая звезда не создаётся. Планетные стадии до C2.0 не расходуют отдельную звёздную ёмкость: у одной звезды могут быть несколько планет.
|
||||
|
||||
Резерв ёмкости логический: звёзды не расходуются и продолжают производить ресурсы. Общий бюджет пересчитывается из физических количеств, отдельная копия бронирований в сохранении не хранится.
|
||||
|
||||
|
||||
### Расчёт поколения C0.2–C4.4
|
||||
|
||||
1. Из истёкшей части партии пропорционально выбираются представители шести этик. Для каждой этики читаются `progress` и `degradation`; целые исходы получаются с сохранением дробных остатков. Неудавшийся из-за лимита переход возвращается на исходную стадию.
|
||||
2. Прогресс ведёт на следующую стадию; деградация распределяется по весам `fall` конкретной этики. Повторные попытки не увеличивают статистику созданных цивилизаций.
|
||||
3. При флаге `fights` рассчитываются войны. Веса исходов — 1:2:3:3, доли прогресса победителей — 0:0.25:0.5:0.75; первый исход деградирует атакующих. Потери противников — 2:1:1.5:2 на войну. Два последних исхода дают 10 и 30 единиц побочного ущерба предыдущей стадии. Выбор участников ограничен живой популяцией, включая ещё не сработавшие партии; их партии сокращаются вместе с населением.
|
||||
4. Для обычного дрейфа веса своей/противоположной/каждой другой этики равны 196/0/1. Дипломатия умножает исходные численности на `diplomacy_rate`, затем нормирует к прежней численности. Флаг дипломатии действует на остающихся на стадии. При переходе в Ct этика сохраняется без дрейфа.
|
||||
5. При флаге `colonizes` создаются колонии на две стадии ниже отправителя. Цели — существующие GG, AB, RP, HP; колонизация заменяет цель и сохраняет её звёздную привязку. Веса дрейфа колонистов — 960/4/9. Исходная цивилизация не расходуется. Новая партия начинается в текущий игровой момент.
|
||||
6. После применения переходов пересчитываются этики, поклонение (`Σ population × worship`), бласты и ограничения среды. Число операций зависит от числа типов и партий, а не от числа цивилизаций.
|
||||
|
||||
Окна исходных партий сохраняют время переходов; несколько частей одного типа обрабатываются агрегированно между минимальным и максимальным временем. Побочный ущерб неистёкшим партиям применяется в текущий момент. Все остатки округления сохраняются в `civilization_remainders`, счётчики — в `civilization_stats`.
|
||||
|
||||
### Структуры и периоды
|
||||
|
||||
Прямой `claim` задаёт структуру стадии и норму 1; каждая промежуточная стадия умножает норму на 10 до следующего `claim`. Начиная с C3.0 требуется соответствующий запас структур, общий для использующих его стадий. При потере запаса избыток деградирует. Этот бюджет действует одновременно со звёздным.
|
||||
|
||||
Размещение выбирает структуры в порядке убывания `civ_bonus`, начиная с соответствующего стадии `claim`. Прямое размещение ограничено числом структур; остальные разрешённые структуры при наличии дают общий бонус, как в Unity. Период новых партий равен `span / средневзвешенный civ_bonus`, для неразмещённой части множитель равен 1. Уже назначенные сроки партий не переписываются при изменении бонуса.
|
||||
|
||||
`life_version = 3` добавляет часы существующим C0.2+ с момента загрузки; старые часы HP, эонов и начальных цивилизаций сохраняются. Ct не получает часы. Миграция выполняется однократно. Глобальные силы и их модификаторы не входят в этот перенос: используются нейтральные множители 1.
|
||||
+233
-58
@@ -4,7 +4,14 @@ const OWNIVERSE_ITEM = preload("uid://d3xjfxwe3w16v")
|
||||
|
||||
const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png"
|
||||
|
||||
const ItemDescription = preload("res://world/owniverse/item_description.gd")
|
||||
var description_targets: Dictionary = {}
|
||||
var description_samples: Dictionary = {}
|
||||
var description_rates: Dictionary = {}
|
||||
var description_elapsed: float = 0.0
|
||||
|
||||
const PAGE_COUNT = 5
|
||||
const RESOURCE_PAGE = 1
|
||||
const PAGE_WIDTH = 1080
|
||||
const DRAG_TOLERANCE = 360
|
||||
const SWIPE_DURATION: float = 0.4
|
||||
@@ -71,52 +78,94 @@ var g_counters: Dictionary[String, Counter] = {
|
||||
|
||||
var g_active_items: Dictionary = {}
|
||||
|
||||
var pending_page: int = -1
|
||||
|
||||
var owniverse:Owniverse = Owniverse.new()
|
||||
var owniverse_items: Dictionary[String, OwniverseItem] = {}
|
||||
var locked_items: Array = []
|
||||
|
||||
var item_list_to_update: Dictionary = {}
|
||||
|
||||
func set_datetime(value: float):
|
||||
date_time.text = str(snapped(value, 0.1))
|
||||
# Simulation time advances in tenths of a year; displaying five decimal
|
||||
# places only adds zeroes and suggests precision the simulation does not
|
||||
# have.
|
||||
date_time.text = Global.format_number(value, 1) if value != 0.0 else "0.0"
|
||||
|
||||
#region Item manipulations: update and visibility
|
||||
|
||||
|
||||
func add_to_update_list(item_id: String, amount: int) -> void:
|
||||
if item_list_to_update.has(item_id):
|
||||
item_list_to_update[item_id] += amount
|
||||
else:
|
||||
item_list_to_update[item_id] = amount
|
||||
|
||||
|
||||
func set_owniverse_item_value() -> void:
|
||||
for item_id in owniverse.items_to_update:
|
||||
if owniverse_items.has(item_id):
|
||||
owniverse_items[item_id].set_label(owniverse.items_to_update[item_id])
|
||||
owniverse.items_to_update = {}
|
||||
owniverse.items_to_update.clear()
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
func _unlock_items():
|
||||
if locked_items.size() == 0: return
|
||||
var _new_locked_items = []
|
||||
var display_totals: Dictionary = owniverse.get_item_amounts(true)
|
||||
for _item_id in locked_items:
|
||||
#print(_item_id ' | ', _entity.unlock)
|
||||
var _flag_to_unlock = true
|
||||
for _elem in owniverse.entities[_item_id].unlock:
|
||||
if maxf(owniverse.state.stats.total_amount.get(_elem, 0.0), display_totals.get(_elem, 0.0)) < owniverse.entities[_item_id].unlock[_elem]:
|
||||
_flag_to_unlock = false
|
||||
break
|
||||
|
||||
if _flag_to_unlock:
|
||||
var item_id: String = owniverse.entities[_item_id].count_as
|
||||
if item_id == "":
|
||||
item_id = _item_id
|
||||
if owniverse_items.has(item_id):
|
||||
owniverse_items[item_id].set_visible(true)
|
||||
else:
|
||||
_new_locked_items.append(_item_id)
|
||||
locked_items = _new_locked_items
|
||||
|
||||
|
||||
func _on_indicator_pressed(indicator_name: String, page: int = 0):
|
||||
_set_description(indicator_name, page)
|
||||
|
||||
|
||||
func _set_item_description(item: OwniverseItem) -> void:
|
||||
var description: String = "[b]" \
|
||||
+ Global.get_localized_item_description(item.item_name, "label") \
|
||||
+"[/b]" \
|
||||
+"\n" \
|
||||
+ Global.get_localized_item_description(item.item_name, "description")
|
||||
var desc_page = clamp(item.item_page, 0, 4)
|
||||
g_item_descriptions[desc_page].text = description
|
||||
var category: String = "Entity" if owniverse.entities.has(item.item_id) else "Force"
|
||||
var id: String = item.item_id if category == "Entity" else item.item_name.capitalize()
|
||||
var page: int = clampi(item.item_page, 0, 4)
|
||||
if description_targets.get(page, {}).get("id", "") != id:
|
||||
description_samples.erase(id)
|
||||
description_rates.erase(id)
|
||||
description_targets[page] = {"id": id, "category": category}
|
||||
_refresh_descriptions()
|
||||
|
||||
|
||||
func _set_description(item_name: String, page: int) -> void:
|
||||
var description = Global.get_description(item_name + ".Name")
|
||||
description += "\n"
|
||||
description += Global.get_description(item_name + ".Description")
|
||||
g_item_descriptions[page].text = description
|
||||
description_targets[page] = {"id": item_name, "category": "Indicator"}
|
||||
_refresh_descriptions()
|
||||
|
||||
|
||||
func _refresh_descriptions() -> void:
|
||||
for page in description_targets:
|
||||
var target: Dictionary = description_targets[page]
|
||||
var label: RichTextLabel = g_item_descriptions[page]
|
||||
label.bbcode_enabled = true
|
||||
label.text = ItemDescription.render(owniverse, Global, target.id, target.category, description_rates.get(target.id, 0.0))
|
||||
|
||||
|
||||
func _update_descriptions(delta: float) -> void:
|
||||
description_elapsed += delta
|
||||
if description_elapsed < 0.25:
|
||||
return
|
||||
for page in description_targets:
|
||||
var id: String = description_targets[page].id
|
||||
var total: float = ItemDescription.quantities(owniverse, id).total
|
||||
description_rates[id] = (total - description_samples[id]) / description_elapsed if description_samples.has(id) else 0.0
|
||||
description_samples[id] = total
|
||||
description_elapsed = 0.0
|
||||
_refresh_descriptions()
|
||||
|
||||
|
||||
func _control_page(item: OwniverseItem) -> void:
|
||||
@@ -169,10 +218,7 @@ func item_activated(item: OwniverseItem) -> void:
|
||||
_set_item_description(item)
|
||||
|
||||
if item.item_id == "S" or item.item_id == "H":
|
||||
owniverse.produce(item.item_id, 100_000)
|
||||
|
||||
func _unlock_item(item_id: String):
|
||||
owniverse_items[item_id].set_visible(true)
|
||||
var _value = owniverse.produce_hs(item.item_id, 100_000)
|
||||
|
||||
|
||||
func _check_counters(delta: float) -> Array:
|
||||
@@ -191,7 +237,7 @@ func _check_counters(delta: float) -> Array:
|
||||
return actions
|
||||
|
||||
|
||||
#region Swipe
|
||||
#region Swipe and page
|
||||
|
||||
func _do_actions(actions: Array) -> void:
|
||||
for action: String in actions:
|
||||
@@ -226,41 +272,26 @@ func _input(event: InputEvent) -> void:
|
||||
g_counters.swipe.value = 0
|
||||
elif event is InputEventScreenTouch:
|
||||
pass
|
||||
#endregion
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
owniverse.init_enitities()
|
||||
owniverse.load_data()
|
||||
_init_owniverse_items()
|
||||
|
||||
owniverse.unlock_item.connect(_unlock_item)
|
||||
g_counters.swipe.sec = 0.2
|
||||
|
||||
for pa in page_actions:
|
||||
page_actions[pa].action_pressed.connect(_page_action_pressed)
|
||||
page_actions[pa].page_id = pa
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
|
||||
var year_delta: float = owniverse.add_time_delta(delta)
|
||||
set_datetime(year_delta)
|
||||
owniverse.cycle(year_delta)
|
||||
|
||||
var actions = _check_counters(delta)
|
||||
_do_actions(actions)
|
||||
|
||||
_set_active_page_actions()
|
||||
|
||||
func _set_active_page_actions() -> void:
|
||||
for pa in page_actions:
|
||||
if !g_active_items.has(pa): continue
|
||||
|
||||
var item_id: String = g_active_items[pa].item_id
|
||||
var amount = owniverse.get_available_to_produce(item_id)
|
||||
page_actions[pa].set_actions(item_id, amount)
|
||||
var entity: OwniverseEntity = owniverse.entities[item_id]
|
||||
|
||||
#var amount = owniverse.get_available_to_produce(item_id)
|
||||
|
||||
#var amount = owniverse.get_available_to_produce(entity)
|
||||
var amount = owniverse.calculate_max_producible(entity)
|
||||
#print(item_id, " : ", amount)
|
||||
if item_id == "SMBH":
|
||||
var preview: Callable = func(requested: float) -> float:
|
||||
return owniverse.calculate_max_producible(entity, requested)
|
||||
page_actions[pa].set_actions(item_id, amount, preview)
|
||||
else:
|
||||
page_actions[pa].set_actions(item_id, amount)
|
||||
|
||||
|
||||
func _page_action_pressed(page_id: int, action_amount: float) -> void:
|
||||
@@ -273,8 +304,49 @@ func _page_action_pressed(page_id: int, action_amount: float) -> void:
|
||||
owniverse.produce(item_id, action_amount)
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var inited_result = init_enitities()
|
||||
locked_items = inited_result.locked_items
|
||||
load_data()
|
||||
_init_owniverse_items()
|
||||
owniverse.refresh_item_amounts()
|
||||
_change_speed(0)
|
||||
set_datetime(owniverse.state.time.year)
|
||||
set_owniverse_item_value()
|
||||
_unlock_items()
|
||||
|
||||
g_counters.swipe.sec = 0.2
|
||||
|
||||
for pa in page_actions:
|
||||
page_actions[pa].action_pressed.connect(_page_action_pressed)
|
||||
page_actions[pa].page_id = pa
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
|
||||
owniverse.evolute(delta)
|
||||
_update_descriptions(delta)
|
||||
set_owniverse_item_value()
|
||||
set_datetime(owniverse.state.time.year)
|
||||
|
||||
#owniverse.cycle(delta)
|
||||
|
||||
#owniverse.cycle_legacy(year_delta)
|
||||
|
||||
var actions = _check_counters(delta)
|
||||
_do_actions(actions)
|
||||
|
||||
_set_active_page_actions()
|
||||
|
||||
|
||||
func _change_speed(delta: int) -> void:
|
||||
var speed: int = owniverse.change_speed(delta)
|
||||
|
||||
owniverse.speed = owniverse.speed + delta
|
||||
|
||||
var speed: int = owniverse.speed
|
||||
var ind: int = 1
|
||||
for icon in g_speed_icons:
|
||||
if ind <= speed:
|
||||
@@ -293,14 +365,117 @@ func _change_speed(delta: int) -> void:
|
||||
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed + 1))
|
||||
|
||||
|
||||
func _on_topmenu_button_pressed(ButtonName) -> void:
|
||||
print(ButtonName)
|
||||
func _on_topmenu_button_pressed(_ButtonName) -> void:
|
||||
print("Debug → ")
|
||||
#, owniverse.state)
|
||||
for id in owniverse.state.batches:
|
||||
var s = owniverse.state.batches[id].size()
|
||||
if s != 0:
|
||||
print(id, " → ", s)
|
||||
#, owniverse.state.batches[id])
|
||||
if id == 'HG':
|
||||
print(owniverse.state.batches[id])
|
||||
|
||||
#var s: float = 1E15
|
||||
#var z = 1.0
|
||||
#for i in range(30):
|
||||
#print(i, " | ", z, " | ", " | ", s - z)
|
||||
#z *= 10
|
||||
print("-------")
|
||||
|
||||
|
||||
func _on_save_data() -> void:
|
||||
owniverse.save_data()
|
||||
func _on_unlock_and_save_timeout() -> void:
|
||||
save_data()
|
||||
_unlock_items()
|
||||
|
||||
|
||||
#region Init save load routines
|
||||
|
||||
|
||||
func load_data() -> bool:
|
||||
var game_data: Dictionary = SaveManager.load_data("owniverse", "qwerty")
|
||||
if game_data.is_empty():
|
||||
if SaveManager.last_error == "":
|
||||
pending_page = RESOURCE_PAGE
|
||||
_restore_page.call_deferred()
|
||||
return false
|
||||
var restored: Dictionary = game_data.state
|
||||
# Current definitions are initialized first; reject incompatible saves before publishing.
|
||||
for id in owniverse.state.amount:
|
||||
if not restored.amount.has(id) or not restored.stats.total_amount.has(id):
|
||||
SaveManager.blocked_saves["owniverse"] = true
|
||||
push_error("Save is missing the current entity: " + id)
|
||||
return false
|
||||
var complete: Dictionary = owniverse.state.duplicate(true)
|
||||
complete.merge(restored, true)
|
||||
owniverse.state = complete
|
||||
owniverse.new_state = complete
|
||||
owniverse.initialize_life_batches()
|
||||
owniverse.refresh_item_amounts()
|
||||
pending_page = clampi(int(game_data.get("ui", {}).get("current_page", 0)), 0, PAGE_COUNT - 1)
|
||||
_restore_page.call_deferred()
|
||||
return true
|
||||
|
||||
|
||||
func _restore_page() -> void:
|
||||
# Wait until containers have calculated the scrollable width.
|
||||
await get_tree().process_frame
|
||||
if pending_page >= 0:
|
||||
stage_page_scroller.scroll_horizontal = pending_page * PAGE_WIDTH
|
||||
pending_page = -1
|
||||
|
||||
|
||||
func save_data() -> void:
|
||||
var page: int = pending_page
|
||||
if page < 0:
|
||||
page = clampi(roundi(float(stage_page_scroller.scroll_horizontal) / PAGE_WIDTH), 0, PAGE_COUNT - 1)
|
||||
SaveManager.save_data("owniverse", "qwerty", {
|
||||
"state": owniverse.state,
|
||||
"ui": {"current_page": page},
|
||||
})
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if is_node_ready() and (what == NOTIFICATION_WM_CLOSE_REQUEST or what == NOTIFICATION_APPLICATION_PAUSED):
|
||||
save_data()
|
||||
|
||||
|
||||
func init_enitities() -> Dictionary:
|
||||
var _result := {
|
||||
'locked_items': [],
|
||||
}
|
||||
for entity_id in Global.OwniverseEntities:
|
||||
var entity = OwniverseEntity.new(Global.OwniverseEntities[entity_id])
|
||||
owniverse.entities[entity_id] = entity
|
||||
|
||||
owniverse.state.stats.total_amount[entity_id] = 0
|
||||
owniverse.state.amount[entity_id] = 0
|
||||
if entity.lifespan > 0:
|
||||
owniverse.state.batches[entity.id] = []
|
||||
owniverse.add_entity_to_batch(entity_id, 0, 0)
|
||||
if entity.group_as != "":
|
||||
owniverse.state.amount[entity.group_as] = 0
|
||||
|
||||
owniverse.state.stats.total_amount[entity.group_as] = 0
|
||||
|
||||
if entity.unlock != {}:
|
||||
owniverse.locked_entities.append(entity)
|
||||
_result.locked_items.append(entity.id)
|
||||
|
||||
if entity.product_per_year_type != "":
|
||||
owniverse.auto_production.append(entity)
|
||||
|
||||
# groups
|
||||
if entity.group_as != "":
|
||||
if !owniverse.entity_groups.has(entity.group_as):
|
||||
owniverse.entity_groups[entity.group_as] = []
|
||||
owniverse.entity_groups[entity.group_as].append(entity_id)
|
||||
|
||||
if entity.event_group != "":
|
||||
if !owniverse.entity_event_groups.has(entity.event_group):
|
||||
owniverse.entity_event_groups[entity.event_group] = []
|
||||
owniverse.entity_event_groups[entity.event_group].append(entity_id)
|
||||
|
||||
return _result
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# Проверки поэтапного переноса из Owniverse.U
|
||||
|
||||
`test_stellar_rules.gd` также проверяет автоматическое создание HBD: долю 0,1% нового автоматического водорода, появление по одному со случайным округлением, сохранение оплаченного остатка топлива, отсутствие повторного начисления на старый запас, продолжение случайной последовательности после сериализации и переход HBD → CBD с сохранением счётчика BD. Формулы описаны в `../simulation_rules.md`.
|
||||
|
||||
## Фоновое сохранение и частота обновления интерфейса
|
||||
|
||||
```sh
|
||||
godot --headless --path Owniverse.GI --script res://tests/test_async_save.gd
|
||||
godot --headless --path Owniverse.GI --script res://tests/test_ui_updates.gd
|
||||
```
|
||||
|
||||
Сохранение: основной поток создаёт глубокий снимок состояния и параметров. Рабочий поток проверяет данные, записывает зашифрованный временный файл, проверяет чтением и заменяет основное сохранение; диагностический JSON создаётся из того же снимка. Одновременно работает один писатель. Для каждого имени файла хранится только последний ожидающий запрос. `request_save()` подтверждает постановку в очередь; результат записи сообщает сигнал `save_completed`. При закрытии игра ждёт `saves_drained`, при приостановке приложения и уничтожении менеджера очередь завершается через `flush_saves()`. Копирование снимка остаётся в основном потоке.
|
||||
|
||||
Тест сохранений пишет только в уникальную временную папку `/tmp`, затем удаляет собственные файлы. Проверяются независимость снимка, работа кадров при записи, объединение запросов, диагностическая копия, сохранность предыдущего файла при ошибках и завершение очереди при уничтожении менеджера. Сообщения `Invalid save`, `Save blocked` и `Cannot write` в этом тесте ожидаемы.
|
||||
|
||||
Счётчики, время и кнопки производства обновляются раз в 0,1 секунды. Изменения симуляции помечают суммы для пересчёта; сами суммы вычисляются при обновлении интерфейса. Текст счётчиков применяется только к видимым элементам и только при изменении строки. Переход между страницами, выбор элемента и производство вызывают немедленное обновление. Описания сохраняют прежний интервал 0,25 секунды. Тест интерфейса использует игровую сцену с отключёнными загрузкой, сохранением и автоматической симуляцией, не обращаясь к сохранению игрока.
|
||||
|
||||
## Время: шаг 0,1 игрового года
|
||||
|
||||
```sh
|
||||
godot --headless --path Owniverse.GI --script res://tests/test_time.gd
|
||||
```
|
||||
|
||||
Команда выполняется из общей папки проектов. Тесты используют искусственные состояния; сохранение игрока не читается и не перезаписывается.
|
||||
|
||||
Проверяются границы десятых долей года, производство ресурсов на этих границах, накопление дробного остатка, скорости 1–10 000 лет/сек, продолжение после загрузки и недопустимые интервалы.
|
||||
|
||||
Схема состояния `0.1.1` хранит:
|
||||
|
||||
- `time.datetime` — накопленный возраст, включая необработанную дробную часть;
|
||||
- `time.year` — последний рассчитанный возраст с точностью до десятой доли года.
|
||||
|
||||
Сохранения используют формат 2 и актуальную структуру состояния. Проверяются целостность данных, фоновая запись и повторная загрузка; поддержка и миграция прежних форматов отсутствуют.
|
||||
|
||||
Файл с несовместимым номером формата при загрузке переносится в `*.adventure.incompatible.bak` (при совпадении имени добавляется числовой суффикс). Игра начинает новую сессию и может сохранять её в формате 2. Прогресс несовместимого файла не переносится, исходные байты сохраняются в архиве. Ошибка архивирования, расшифровки или повреждение данных текущего формата по-прежнему блокируют перезапись.
|
||||
|
||||
## Согласованные правила эволюции
|
||||
|
||||
Сохраняются постепенные интервальные окна GI: начало преобразования — через `lifespan`, длительность окна — `lifespan / 40` при `lifespan > 10`, иначе `lifespan`.
|
||||
|
||||
Вероятностные исходы используют накопление дробного остатка GI, без случайного округления. Этот же подход выбран для будущего создания планет. Шансы планет записаны долями (`0.01` = 1%), а альтернативные исходы звёзд — процентами (`%0.1` = 0.1%). Парсер планет сохраняет дроби и поддерживает один элемент без `&`.
|
||||
|
||||
Уязвимость к бластам читается из `blastability` таблицы GI: HP, EON и цивилизации до C1.2 включительно; C1.3 и выше неуязвимы. Это ограничение относится к бластам уровня 1; локальное уничтожение планет уровня 2 в Unity охватывает все планеты.
|
||||
|
||||
```sh
|
||||
godot --headless --path Owniverse.GI --script res://tests/test_stellar_rules.gd
|
||||
```
|
||||
|
||||
Проверяются парсер планет, граница уязвимости, накопление редких исходов, сериализация остатков и постепенное преобразование реальной партии RD в TWD.
|
||||
|
||||
Создание планет подключено к ручному и автоматическому производству звёзд. Бласты обрабатываются как события, а не ресурс. Выжившие планеты переходят преемнику звезды пропорционально числу реально полученных преемников; при отсутствии преемника освобождаются. Локальное разрушение выполняется перед передачей.
|
||||
|
||||
Дополнительно проверяются частичная передача, альтернативные преемники, уровни разрушения 1/2, накопление слабых бластов, сохранения, ручное/автоматическое создание планет и несколько поколений за длинный шаг. Правила и формулы описаны в `../simulation_rules.md`.
|
||||
|
||||
## Описания выбранных объектов
|
||||
|
||||
```sh
|
||||
godot --headless --path Owniverse.GI --script res://tests/test_descriptions.gd
|
||||
```
|
||||
|
||||
Источник текста — `Global.Templates[Global.Locale]`. Заголовок берётся из `Name`, основной текст — из `Description`. Поддерживаются ключи с классом (`RD.Entity.Description`) и старые ключи без класса (`RD.Description`).
|
||||
|
||||
`%valueN%` заменяется значением переменной, имя которой записано в поле `valueN` этой строки. Поддерживаются `amount`, `colored costs`, `lifespan`, `voids`, `he / h`, `metallicity`, показатели энергии, плотности и скорости времени. `amount1`–`amount4` сил пока отображаются как `—`; неизвестные непустые обозначения тоже дают `—`, пустые поля — пустую строку. `[p]` превращается в разделение абзацев, цветовые BBCode-теги сохраняются.
|
||||
|
||||
Доступные требования зелёные, недоступные красные. Для объектов со связанными экземплярами выводится доступное количество и серым общее; для планет — общее количество. Скорость изменения вычисляется по реальному времени и обновляется раз в 0,25 секунды. Доли He/H вычисляются из текущих свободных запасов; при пустой смеси показывается `—`. Металличность и плотность используют расчёт среды GI. Счётчики исходной материи и этики Unity в текущей GI отсутствуют и здесь не добавляются.
|
||||
|
||||
## Первичная жизнь
|
||||
|
||||
```sh
|
||||
godot --headless --path Owniverse.GI --script res://tests/test_life.gd
|
||||
```
|
||||
|
||||
Проверяются абиогенез, деградация, повторные попытки без создания дополнительных планет, появление C0.0 на прежней звезде, остатки вероятностей, сохранение/продолжение, однократная инициализация старых планет и отсутствие воскрешения после бластов.
|
||||
|
||||
Проверка загрузки этических правил, распределений и отделения космических цивилизаций:
|
||||
|
||||
```sh
|
||||
godot --headless --path Owniverse.GI --script res://tests/test_civilization_state.gd
|
||||
```
|
||||
|
||||
Тест цивилизаций дополнительно проверяет общий звёздный бюджет, повторное использование резерва при переходе, звёзды в структурах, массовый эквивалент SMBH, блокировку неподдержанного прогресса, каскад деградации и повторное применение ограничений без потерь.
|
||||
|
||||
|
||||
## Полная эволюция цивилизаций
|
||||
|
||||
```sh
|
||||
godot --headless --path Owniverse.GI --script res://tests/test_civilization_evolution.gd
|
||||
```
|
||||
|
||||
Проверяются реальные коэффициенты C0.2, войны и побочный ущерб C2.0, замена планет колониями с сохранением хозяина, дипломатическое влияние, бонус периода от структур и деградация при их отсутствии. Ускоренная модель проходит все стадии C0.2 → C4.4 → Ct. Проверки контролируют сохранение населения, целочисленность этик, валидацию сохранения и детерминированное продолжение после сериализации.
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
extends "res://stage/stage.gd"
|
||||
|
||||
# Keep scene wiring and display logic, without accessing the player's save.
|
||||
func load_data() -> bool:
|
||||
return false
|
||||
|
||||
func save_data() -> void:
|
||||
pass
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
pass
|
||||
|
||||
func _notification(_what: int) -> void:
|
||||
pass
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
uid://c8ewikdhlvnex
|
||||
@@ -0,0 +1,47 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures: int = 0
|
||||
var checks: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(value: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not value:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func run() -> void:
|
||||
var actions = load("res://ui/actions/actions.tscn").instantiate()
|
||||
root.add_child(actions)
|
||||
for maximum in [0.0, 0.1, 0.99, 1.0, 1.9, 10.0, 100.0, 123.0, 1000.0, 10000.0, 1e9, 1.1e9, 1e150]:
|
||||
actions.set_actions("test", maximum)
|
||||
var labels: Array = []
|
||||
var values: Array = []
|
||||
for i in 5:
|
||||
var value: float = actions.action_values[i]
|
||||
var text: String = actions.action_labels[i].text
|
||||
check(value == floorf(value) and value <= floorf(maximum), "Whole affordable request")
|
||||
if value > 0.0:
|
||||
check(not values.has(value) and not labels.has(text), "Unique requests and displayed labels")
|
||||
values.append(value)
|
||||
labels.append(text)
|
||||
else:
|
||||
check(text == "" and actions.action_labels[i].get_parent().get_node("TextureButton").disabled, "Unused button blank and disabled")
|
||||
if maximum >= 1.0: check(values.back() == floorf(maximum), "Maximum retained")
|
||||
actions.set_actions("test", 100.0)
|
||||
check(actions.action_values.values() == [1.0, 10.0, 100.0, 0.0, 0.0], "Exact power of ten not duplicated")
|
||||
actions.set_actions("SMBH", 25.0, func(_request): return 25.0)
|
||||
check(actions.action_values.values() == [25.0, 0.0, 0.0, 0.0, 0.0], "Identical SMBH previews collapse to one button")
|
||||
actions.set_actions("step", 123.0, func(request): return floorf(request / 5.0) * 5.0)
|
||||
check(actions.action_values.values() == [10.0, 100.0, 123.0, 0.0, 0.0], "Unproducible small requests omitted")
|
||||
check(actions.action_labels[2].text == "120", "Recipe preview displayed while original request retained")
|
||||
var emitted: Array = []
|
||||
actions.action_pressed.connect(func(_page, amount): emitted.append(amount))
|
||||
actions._on_action_button_pressed(2)
|
||||
actions._on_action_button_pressed(4)
|
||||
check(emitted == [123.0], "Only active button emits its matching request")
|
||||
actions.queue_free()
|
||||
print("Action values: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dilbcphuku6k1
|
||||
@@ -0,0 +1,100 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures: int = 0
|
||||
var checks: int = 0
|
||||
var completions: Array = []
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(value: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not value:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func run() -> void:
|
||||
var manager = load("res://autoload/save_manager.gd").new()
|
||||
var directory: String = "/tmp/owniverse-async-%s-%s" % [OS.get_process_id(), Time.get_ticks_usec()]
|
||||
DirAccess.make_dir_recursive_absolute(directory)
|
||||
manager.save_directory = directory
|
||||
root.add_child(manager)
|
||||
manager.save_completed.connect(func(filename, success, error): completions.append([filename, success, error]))
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
var data: Dictionary = {"state": world.state, "ui": {"current_page": 1}}
|
||||
var global: Node = root.get_node("Global")
|
||||
var old_plaintext: bool = global.save_plaintext_copy
|
||||
global.save_plaintext_copy = true
|
||||
check(manager.request_save("snapshot", "test-password", data), "Snapshot queued")
|
||||
data.state.energy = 99.0
|
||||
data.ui.current_page = 2
|
||||
var frames: int = 0
|
||||
var deadline: int = Time.get_ticks_msec() + 10000
|
||||
while manager.is_saving() and Time.get_ticks_msec() < deadline:
|
||||
frames += 1
|
||||
await process_frame
|
||||
check(not manager.is_saving() and frames > 0, "Frames continue while save is pending")
|
||||
var saved: Dictionary = manager.load_data("snapshot", "test-password")
|
||||
check(saved.state.energy == 1e14 and saved.ui.current_page == 1, "Worker owns a deep snapshot")
|
||||
var plain: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("snapshot.adventure.json")))
|
||||
check(plain.state.energy == 1e14, "Diagnostic JSON uses same snapshot")
|
||||
check(completions.size() == 1 and completions[0][1], "Completion reports successful write")
|
||||
|
||||
completions.clear()
|
||||
data.state.energy = 1.0
|
||||
manager.request_save("latest", "test-password", data)
|
||||
data.state.energy = 2.0
|
||||
manager.request_save("latest", "test-password", data)
|
||||
data.state.energy = 3.0
|
||||
manager.request_save("latest", "test-password", data)
|
||||
data.state.energy = 4.0
|
||||
manager.flush_saves()
|
||||
check(completions.size() == 2, "Repeated requests coalesce behind active write")
|
||||
check(manager.load_data("latest", "test-password").state.energy == 3.0, "Last queued snapshot wins")
|
||||
|
||||
var incompatible_path: String = directory.path_join("incompatible.adventure")
|
||||
var incompatible = FileAccess.open_encrypted_with_pass(incompatible_path, FileAccess.WRITE, "test-password")
|
||||
incompatible.store_var({"format_version": -1, "state": {"unknown": true}}, false)
|
||||
incompatible.close()
|
||||
var original: PackedByteArray = FileAccess.get_file_as_bytes(incompatible_path)
|
||||
var previous_archive = FileAccess.open(incompatible_path + ".incompatible.bak", FileAccess.WRITE)
|
||||
previous_archive.store_string("keep previous backup")
|
||||
previous_archive.close()
|
||||
manager.blocked_saves.incompatible = true
|
||||
check(manager.load_data("incompatible", "test-password").is_empty() and manager.last_error == "", "Unsupported format permits a fresh session")
|
||||
check(not manager.blocked_saves.has("incompatible") and not FileAccess.file_exists(incompatible_path), "Incompatible file no longer blocks saving")
|
||||
check(FileAccess.get_file_as_bytes(incompatible_path + ".incompatible.bak.1") == original, "Unsupported save archived without losing bytes")
|
||||
check(FileAccess.get_file_as_string(incompatible_path + ".incompatible.bak") == "keep previous backup", "Earlier archive preserved")
|
||||
check(manager.request_save("incompatible", "test-password", data), "New session can queue a save")
|
||||
manager.flush_saves()
|
||||
check(manager.load_data("incompatible", "test-password").state == data.state, "New format saves and reloads after incompatible file")
|
||||
|
||||
# Expected failures below must leave the last good encrypted file intact.
|
||||
data.state.energy = -1.0
|
||||
manager.request_save("latest", "test-password", data)
|
||||
manager.flush_saves()
|
||||
check(not completions.back()[1] and manager.last_error.contains("Invalid save"), "Worker validation error reaches main thread")
|
||||
check(manager.load_data("latest", "test-password").state.energy == 3.0, "Invalid snapshot preserves previous save")
|
||||
manager.blocked_saves.latest = true
|
||||
check(not manager.request_save("latest", "test-password", data), "Unreadable save protection also applies to queue")
|
||||
# A valid snapshot exercises an actual I/O failure, not validation.
|
||||
data.state.energy = 5.0
|
||||
manager.request_save("missing/failure", "test-password", data)
|
||||
manager.flush_saves()
|
||||
check(manager.last_error.contains("Cannot write"), "I/O error reported without hanging queue")
|
||||
|
||||
manager.request_save("shutdown", "test-password", data)
|
||||
data.state.energy = 6.0
|
||||
manager.request_save("shutdown", "test-password", data)
|
||||
manager.free()
|
||||
var reader = load("res://autoload/save_manager.gd").new()
|
||||
reader.save_directory = directory
|
||||
check(reader.load_data("shutdown", "test-password").state.energy == 6.0, "Teardown drains final pending snapshot")
|
||||
reader.free()
|
||||
world.free()
|
||||
global.save_plaintext_copy = old_plaintext
|
||||
for filename in DirAccess.get_files_at(directory):
|
||||
DirAccess.remove_absolute(directory.path_join(filename))
|
||||
DirAccess.remove_absolute(directory)
|
||||
print("Async save: %d checks, %d failures (validation/block/I/O errors above are intentional)" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dsn0ih3yfrry2
|
||||
@@ -0,0 +1,57 @@
|
||||
extends SceneTree
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
var global: Node = root.get_node("Global")
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.entities["N"].life_product = {}
|
||||
world.entities["N"].product_per_year_type = "H"
|
||||
world.entities["N"].product_per_year_amount = 10.0
|
||||
world.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
for entity in world.entities.values():
|
||||
entity.free()
|
||||
world.free()
|
||||
|
||||
func run() -> void:
|
||||
var world = fixture()
|
||||
world.state.batch_origins.LGRB = 10357034.80000000074505806
|
||||
var boundary: float = 41215289.79999999701976776
|
||||
for interval in [[boundary - 2.5, boundary + 2.25], [boundary, boundary + 2.0]]:
|
||||
world.state.batches.LGRB = []
|
||||
world._append_batch(world.state, "LGRB", 100.0, interval[0], interval[1])
|
||||
var total: float = 0.0
|
||||
var last: float = -INF
|
||||
for batch: Dictionary in world.state.batches.LGRB:
|
||||
total += batch.amount
|
||||
check(batch.created_from > last, "Batch windows advance monotonically")
|
||||
check(batch.created_to > batch.created_from, "Positive window duration")
|
||||
check(batch.amount >= 0.0, "No negative batch amounts")
|
||||
last = batch.created_from
|
||||
check(total == 100.0, "Boundary correction preserves all objects")
|
||||
check(world.state.batches.LGRB.size() <= 7, "Finite number of windows")
|
||||
dispose(world)
|
||||
print("Batch boundaries: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dq86k8bto42xl
|
||||
@@ -0,0 +1,164 @@
|
||||
extends SceneTree
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
var global: Node = root.get_node("Global")
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.entities["N"].life_product = {}
|
||||
world.entities["N"].product_per_year_type = "H"
|
||||
world.entities["N"].product_per_year_amount = 10.0
|
||||
world.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
for entity in world.entities.values():
|
||||
entity.free()
|
||||
world.free()
|
||||
|
||||
func seed_population(world, id: String, count: float, time: float = 1.0) -> void:
|
||||
world.state.amount.N = 0.0
|
||||
world.state.amount[id] = count
|
||||
world.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}]
|
||||
world.initialize_life_batches()
|
||||
|
||||
func sum_ethics(world, id: String) -> float:
|
||||
var result: float = 0.0
|
||||
for n in world.state.ethics.get(id, {}).values(): result += n
|
||||
return result
|
||||
|
||||
func invariants(world) -> void:
|
||||
var physical: Dictionary = world._physical_amounts(world.state)
|
||||
for id in world.state.ethics:
|
||||
check(sum_ethics(world, id) == physical.get(id, 0.0), "Ethics equal physical population for " + id)
|
||||
for n in world.state.ethics[id].values():
|
||||
if n < 0.0 or n != floorf(n): check(false, "Invalid ethic amount")
|
||||
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Civilization state validates")
|
||||
|
||||
func run() -> void:
|
||||
var global: Node = root.get_node("Global")
|
||||
var baseline: Dictionary = global.CivilizationRules.duplicate(true)
|
||||
var world = fixture()
|
||||
seed_population(world, "C0.2", 60000.0)
|
||||
world._evolve_batches(world.state, 1.0)
|
||||
check(world.state.amount["C0.3"] > 0.0, "C0.2 actually progresses")
|
||||
check(world.state.amount["C0.1"] > 0.0, "C0.2 actually degrades")
|
||||
check(world.state.civilization_stats.get("wars", 0.0) == 0.0, "Table disables early wars")
|
||||
check(world.state.amount["C0.2"] + world.state.amount["C0.3"] + world.state.amount["C0.1"] == 60000.0, "Ordinary evolution conserves population")
|
||||
check(world.state.stats.total_amount["C0.2"] == 0.0, "Survivors do not count as new civilizations")
|
||||
check(world.state.civilization_stats.worship > 0.0, "Worship derived from surviving ethics")
|
||||
invariants(world)
|
||||
var restored = fixture()
|
||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||
world._evolve_batches(world.state, 2100.0)
|
||||
restored._evolve_batches(restored.state, 2100.0)
|
||||
check(world.state == restored.state, "Save resumes deterministic ethical evolution")
|
||||
invariants(world)
|
||||
dispose(restored)
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1e8
|
||||
seed_population(world, "C2.0", 60000.0)
|
||||
world.state.amount["C1.9"] = 10000.0
|
||||
world.initialize_life_batches()
|
||||
world._evolve_batches(world.state, 1.0)
|
||||
check(world.state.civilization_stats.wars > 0.0, "Wars execute when enabled")
|
||||
check(world.state.civilization_stats.collateral > 0.0, "Wars affect previous-stage populations")
|
||||
check(world.state.amount["C2.1"] > 0.0, "Winners and peaceful progress reach next stage")
|
||||
invariants(world)
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1.0
|
||||
world.state.binds.RD = {"HP": 5.0, "GG": 5.0, "AB": 5.0, "RP": 5.0}
|
||||
seed_population(world, "C1.6", 60000.0)
|
||||
for row: Dictionary in global.CivilizationRules["C1.6"].ethics.values(): row.degradation = "0"
|
||||
world._evolve_batches(world.state, 1.0)
|
||||
var after: Dictionary = world._physical_amounts(world.state)
|
||||
check(world.state.civilization_stats.colonies == 20.0, "Colonization limited to real targets")
|
||||
check(after.HP + after.GG + after.AB + after.RP == 0.0, "Colonized objects consumed once")
|
||||
check(world.state.binds.RD.get("C1.4", 0.0) == 20.0, "Colonies retain target host and use two-stages-earlier level")
|
||||
var totals: float = 0.0
|
||||
for id in after:
|
||||
if world.entities[id].group_as == "Planet": totals += after[id]
|
||||
check(totals == 60020.0, "Colonization replaces planets, does not create extra worlds")
|
||||
invariants(world)
|
||||
dispose(world)
|
||||
|
||||
global.CivilizationRules = baseline.duplicate(true)
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1e25
|
||||
world.state.amount.BG = 1e25
|
||||
world.state.amount.TWD = 1e25
|
||||
world.state.amount.SNS = 1e25
|
||||
world.state.amount.MGT = 1e25
|
||||
world.state.amount.BH25 = 1e25
|
||||
world.state.amount.SMBH = 1e26
|
||||
for id in world.entities:
|
||||
if world.entities[id].claim != "": world.state.amount[id] = 1e12
|
||||
# Accelerated deterministic fixture traverses every enabled stage, including Ct.
|
||||
for id in global.CivilizationRules:
|
||||
if id == "Ct": continue
|
||||
global.CivilizationRules[id].fights = false
|
||||
global.CivilizationRules[id].colonizes = false
|
||||
global.CivilizationRules[id].diplomacy = false
|
||||
for row: Dictionary in global.CivilizationRules[id].ethics.values():
|
||||
row.progress = "1"
|
||||
row.degradation = "0"
|
||||
world.entities[id].lifespan = 1.0
|
||||
world.entities[id].lifespan_delta = 1.0
|
||||
global.CivilizationRules[id].span = 1.0
|
||||
seed_population(world, "C0.2", 6.0)
|
||||
world._evolve_batches(world.state, 1000.0)
|
||||
check(world.state.amount.Ct == 6.0, "Complete C0.2 -> C4.4 -> Ct chain runs")
|
||||
check(not world.state.batches.has("Ct"), "Transcendence is terminal")
|
||||
invariants(world)
|
||||
dispose(world)
|
||||
global.CivilizationRules = baseline
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 10000.0
|
||||
world.state.amount.SCS = 1.0
|
||||
world.state.amount["C2.4"] = 1.0
|
||||
world.initialize_life_batches()
|
||||
var span: float = world.state.batches["C2.4"][0].start_year
|
||||
check(is_equal_approx(span, 20000.0 / 3.6), "Structure bonus shortens newly registered lifespan")
|
||||
dispose(world)
|
||||
var evolution = load("res://world/owniverse/civilization_evolution.gd")
|
||||
var balanced: Dictionary = {"m": 10000.0, "l": 10000.0, "w": 10000.0, "d": 10000.0, "c": 10000.0, "b": 10000.0}
|
||||
var neutral: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics)
|
||||
var diplomatic: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics, true)
|
||||
check(neutral == balanced, "Ordinary drift preserves balanced ethics")
|
||||
check(diplomatic.d > diplomatic.w, "Diplomacy uses per-ethic influence")
|
||||
check(evolution.total(diplomatic) == 60000.0, "Diplomacy conserves population")
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1e15
|
||||
world.state.amount.BG = 1e15
|
||||
world.state.amount["C3.0"] = 6.0
|
||||
world.initialize_life_batches()
|
||||
check(world.state.amount["C3.0"] == 0.0 and world.state.amount["C2.9"] == 6.0, "Missing mandatory structures degrade existing civilizations")
|
||||
invariants(world)
|
||||
dispose(world)
|
||||
print("Civilization evolution: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://u4ujq17gu2l5
|
||||
@@ -0,0 +1,134 @@
|
||||
extends SceneTree
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
var global: Node = root.get_node("Global")
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.entities["N"].life_product = {}
|
||||
world.entities["N"].product_per_year_type = "H"
|
||||
world.entities["N"].product_per_year_amount = 10.0
|
||||
world.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
for entity in world.entities.values():
|
||||
entity.free()
|
||||
world.free()
|
||||
|
||||
func run() -> void:
|
||||
var world = fixture()
|
||||
var global: Node = root.get_node("Global")
|
||||
check(global.CivilizationRules.size() == 44, "Load all ethical civilization stages")
|
||||
for row: Dictionary in global.CivilizationRules.values():
|
||||
check(row.ethics.size() == 6 or row.span == 0.0, "Every ethical stage has six rules")
|
||||
world.state.amount.RD = 12.0
|
||||
world.state.amount["C0.2"] = 8.0
|
||||
world.state.binds.RD = {"C2.0": 12.0, "C1.9": 3.0}
|
||||
world.initialize_life_batches()
|
||||
check(world.state.amount["C2.0"] == 12.0 and world.state.binds.RD["C2.0"] == 0.0, "Old space civilizations detach without loss")
|
||||
check(world.state.binds.RD["C1.9"] == 3.0, "Planetary civilizations retain their host")
|
||||
check(world.state.ethics["C0.2"] == {"m": 2.0, "l": 2.0, "w": 1.0, "d": 1.0, "c": 1.0, "b": 1.0}, "Migration uses equal ethics and deterministic remainder")
|
||||
check(world.state.ethics["C2.0"].values().all(func(n): return n == 2.0), "Bound civilization ethics count physical population")
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
world.initialize_life_batches()
|
||||
check(world.state == before, "Repeated initialization preserves ethics")
|
||||
world.state.ethics["C0.2"] = {"m": 8.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0}
|
||||
world.initialize_life_batches()
|
||||
check(world.state.ethics["C0.2"].m == 8.0, "Existing unequal distribution is retained")
|
||||
world.state.amount["C0.2"] = 3.0
|
||||
world.initialize_life_batches()
|
||||
check(world.state.ethics["C0.2"].m == 3.0, "Losses reduce ethics without inventing population")
|
||||
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Ethics state validates")
|
||||
var restored = fixture()
|
||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||
restored.initialize_life_batches()
|
||||
check(restored.state == world.state, "Save roundtrip preserves ethics and ownership")
|
||||
dispose(restored)
|
||||
dispose(world)
|
||||
world = fixture()
|
||||
world.state.amount.RD = 10.0
|
||||
for row: Dictionary in global.CivilizationRules["C1.9"].ethics.values():
|
||||
row.progress = "1"
|
||||
row.degradation = "0"
|
||||
row.colonization = "0"
|
||||
world.state.binds.RD = {"C1.9": 10.0}
|
||||
world.state.batches["C1.9"] = [{"amount": 10.0, "start_year": 1.0, "end_year": 1.0}]
|
||||
world._evolve_batches(world.state, 1.0)
|
||||
check(world.state.amount["C2.0"] == 10.0 and world.state.binds.RD.get("C2.0", 0.0) == 0.0, "Space successor does not inherit planetary binding")
|
||||
dispose(world)
|
||||
var capacity = load("res://world/owniverse/civilization_state.gd")
|
||||
world = fixture()
|
||||
world.state.amount.RD = 10.0
|
||||
world.state.amount["C2.0"] = 8.0
|
||||
var budget: Dictionary = capacity.stellar_budget(world, world.state, global.CivilizationRules)
|
||||
check(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5.0) == 2.0, "Growth consumes only remaining stellar capacity")
|
||||
check(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5.0) == 0.0, "A reservation cannot be spent twice")
|
||||
world.state.amount["C2.0"] = 1.0
|
||||
budget = capacity.stellar_budget(world, world.state, global.CivilizationRules)
|
||||
check(capacity.admit_transition(budget, global.CivilizationRules, "C2.0", "C2.1", 1.0) == 1.0, "A transition reuses the source reservation")
|
||||
check(budget.free.DwStar == 0.0, "C2.1 reserves ten dwarf stars")
|
||||
world.state.amount["C2.0"] = 0.0
|
||||
world.state.binds.SC = {"RD": 10.0}
|
||||
budget = capacity.stellar_budget(world, world.state, global.CivilizationRules)
|
||||
check(budget.capacity.DwStar == 20.0, "Stars inside structures are counted once")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 12.0
|
||||
world.state.amount["C2.1"] = 2.0
|
||||
world.state.amount["C2.0"] = 5.0
|
||||
world.initialize_life_batches()
|
||||
check(world.state.amount["C2.1"] == 1.0 and world.state.amount["C2.0"] == 2.0, "Shared budget supports advanced civilizations first")
|
||||
check(world.state.binds.RD["C1.9"] == 4.0, "Excess cascades to supported planetary stage")
|
||||
var snapshot: Dictionary = world.state.duplicate(true)
|
||||
world.initialize_life_batches()
|
||||
check(world.state == snapshot, "Capacity enforcement is idempotent")
|
||||
world.state.amount.RD = 0.0
|
||||
world.initialize_life_batches()
|
||||
check(world.state.amount["C2.1"] == 0.0 and world.state.amount["C2.0"] == 0.0, "Vanished stars force all unsupported space civilizations down")
|
||||
check(world._physical_amounts(world.state)["C1.9"] == 7.0, "Forced degradation conserves civilizations")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.LGW = 20.0
|
||||
world.state.amount.SMBH = 2.0e22
|
||||
world.state.amount["C4.2"] = 3.0
|
||||
world.initialize_life_batches()
|
||||
check(world.state.amount["C4.2"] == 2.0, "SMBH capacity uses stellar mass equivalent")
|
||||
check(world._physical_amounts(world.state)["C1.9"] == 1.0, "Unsupported civilization degrades through missing habitat families")
|
||||
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Capacity-limited state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 2.0
|
||||
for row: Dictionary in global.CivilizationRules["C1.9"].ethics.values():
|
||||
row.progress = "1"
|
||||
row.degradation = "0"
|
||||
row.colonization = "0"
|
||||
world.state.binds.RD = {"C1.9": 5.0}
|
||||
world.state.batches["C1.9"] = [{"amount": 5.0, "start_year": 1.0, "end_year": 1.0}]
|
||||
world._evolve_batches(world.state, 1.0)
|
||||
check(world.state.amount["C2.0"] == 2.0 and world.state.binds.RD["C1.9"] == 3.0, "Unfunded transition retains the original planetary civilization")
|
||||
dispose(world)
|
||||
print("Civilization state: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://nvgiilyqitw
|
||||
@@ -0,0 +1,96 @@
|
||||
extends SceneTree
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
var global: Node = root.get_node("Global")
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.entities["N"].life_product = {}
|
||||
world.entities["N"].product_per_year_type = "H"
|
||||
world.entities["N"].product_per_year_amount = 10.0
|
||||
world.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
for entity in world.entities.values():
|
||||
entity.free()
|
||||
world.free()
|
||||
|
||||
func run() -> void:
|
||||
var global: Node = root.get_node("Global")
|
||||
var description = load("res://world/owniverse/item_description.gd")
|
||||
var world = fixture()
|
||||
world.state.amount.H = 8.0
|
||||
world.state.amount.He = 2.0
|
||||
for locale in ["en", "ru"]:
|
||||
global.Locale = locale
|
||||
var text: String = description.render(world, global, "RD")
|
||||
check(text.contains("[b]") and not text.contains("%value"), "Localized template resolved: " + locale)
|
||||
check(text.contains("#40cc40"), "Affordable cost green: " + locale)
|
||||
check(text.contains("4.0000E+10"), "Lifespan substituted: " + locale)
|
||||
text = description.render(world, global, "He")
|
||||
check(text.contains("He: 20.00% / H: 80.00%") and not text.contains("%value"), "Helium metadata and mixture: " + locale)
|
||||
text = description.render(world, global, "Shovelin", "Force")
|
||||
check(text.count("]—[/color]") == 4, "Missing force levels use dash: " + locale)
|
||||
text = description.render(world, global, "YD")
|
||||
check(text.contains("#ff6060"), "Insufficient cost red: " + locale)
|
||||
var reordered: Dictionary = {"text": "%value0% / %value1%", "value0": "lifespan", "value1": "amount"}
|
||||
check(global.render_template(reordered, {"amount": "12", "lifespan": "99"}) == "99 / 12", "Metadata chooses variables, not fixed argument positions")
|
||||
world.state.amount.RD = 2.0
|
||||
world.state.binds.G = {"RD": 3.0, "HP": 7.0}
|
||||
var values: Dictionary = description.variables(world, global, "RD", -2.0)
|
||||
check(values.amount.contains("2") and values.amount.contains("(5)") and values.amount.contains("#888888"), "Free and total counts with gray binding total")
|
||||
check(values.amount.contains("#ff6060") and values.amount.contains("−2.0"), "Real-time decrease red")
|
||||
values = description.variables(world, global, "HP")
|
||||
check(values.amount == "7", "Planet description includes bound planets")
|
||||
world.state.amount.EON1 = 2.0
|
||||
world.state.binds.G.EON2 = 3.0
|
||||
var saved_state: Dictionary = world.state.duplicate(true)
|
||||
var counters: Dictionary = world.get_item_amounts()
|
||||
check(counters.HP == 7.0 and counters.LP == 5.0 and counters.Planet == 12.0, "Planet counters include bound objects and aggregate eons")
|
||||
check(counters.RD == 2.0, "Star counters retain available quantity")
|
||||
check(world.state == saved_state, "Reading counters does not mutate simulation")
|
||||
check(world.get_item_amounts(true).HP == 0.0, "Produced counters remain independent of current population")
|
||||
values = description.variables(world, global, "RD", 2.0)
|
||||
check(values.amount.contains("#40cc40") and values.amount.contains("+2.0"), "Increase green")
|
||||
var stage_script = load("res://stage/stage.gd")
|
||||
check(stage_script.can_instantiate(), "Stage integration compiles")
|
||||
var stage = stage_script.new()
|
||||
stage.owniverse.free()
|
||||
stage.owniverse = world
|
||||
var label: RichTextLabel = RichTextLabel.new()
|
||||
stage.g_item_descriptions = [label]
|
||||
stage.description_targets[0] = {"id": "RD", "category": "Entity"}
|
||||
stage._refresh_descriptions()
|
||||
check(label.bbcode_enabled and label.text.contains("Красный карлик"), "Stage displays localized rich text")
|
||||
stage._update_descriptions(0.25)
|
||||
world.state.amount.RD += 1.0
|
||||
stage._update_descriptions(0.25)
|
||||
check(label.text.contains("+4.0"), "Selected description refreshes with rate per real second")
|
||||
global.Locale = "en"
|
||||
stage._update_descriptions(0.25)
|
||||
check(label.text.contains("Red Dwarf"), "Selected description follows locale changes")
|
||||
label.free()
|
||||
stage.free()
|
||||
dispose(world)
|
||||
print("Descriptions: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b1efrwyimgle7
|
||||
@@ -0,0 +1,58 @@
|
||||
extends SceneTree
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
var global: Node = root.get_node("Global")
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.entities["N"].life_product = {}
|
||||
world.entities["N"].product_per_year_type = "H"
|
||||
world.entities["N"].product_per_year_amount = 10.0
|
||||
world.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
for entity in world.entities.values():
|
||||
entity.free()
|
||||
world.free()
|
||||
|
||||
func run() -> void:
|
||||
var world = fixture()
|
||||
for id in world.entities:
|
||||
var expected: Array = []
|
||||
for key in world.entities:
|
||||
if world.entities[key].group_as == id:
|
||||
expected.append(key)
|
||||
check(world._is_group(id) == (not expected.is_empty()), "Group classification: " + id)
|
||||
check(world._members(id) == (expected if not expected.is_empty() else [id]), "Group members: " + id)
|
||||
var members: Array = world._members("Star")
|
||||
members.clear()
|
||||
check(not world._members("Star").is_empty(), "Returned members are independent")
|
||||
world.entities.N.group_as = "TestGroup"
|
||||
world.rebuild_group_index()
|
||||
check(world._members("TestGroup") == ["N"], "Explicit definition reload")
|
||||
world.entities.N.free()
|
||||
world.entities.erase("N")
|
||||
check(not world._is_group("TestGroup"), "Removed definition invalidates index")
|
||||
print("Group index: %d checks, %d failures" % [checks, failures])
|
||||
dispose(world)
|
||||
quit(1 if failures else 0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bykrkembtacbw
|
||||
@@ -0,0 +1,123 @@
|
||||
extends SceneTree
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
var global: Node = root.get_node("Global")
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.entities["N"].life_product = {}
|
||||
world.entities["N"].product_per_year_type = "H"
|
||||
world.entities["N"].product_per_year_amount = 10.0
|
||||
world.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
for entity in world.entities.values():
|
||||
entity.free()
|
||||
world.free()
|
||||
|
||||
func run() -> void:
|
||||
var world = fixture()
|
||||
world.state.amount.N = 0.0
|
||||
world.state.amount.HP = 40000.0
|
||||
world.initialize_life_batches()
|
||||
check(world.state.batches.HP.size() == 1, "Old HP receives life cohort")
|
||||
check(world.state.batches.HP[0].start_year == 5e6, "HP uses source span")
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
world.initialize_life_batches()
|
||||
check(world.state == before, "Initialization is idempotent")
|
||||
world._evolve_batches(world.state, 5e6 + 5e6 / 40.0)
|
||||
check(world.state.amount.EON1 == 10.0, "HP abiogenesis follows source probability")
|
||||
check(world.state.amount.RP == 100.0, "HP degradation produces RP")
|
||||
check(world.state.amount.HP == 39890.0, "Unchanged planets retry, not disappear")
|
||||
check(world.state.stats.total_amount.HP == 0.0, "Retry does not inflate creation statistics")
|
||||
check(world.state.batches.HP.size() > 0, "Survivors get next attempt")
|
||||
var physical: Dictionary = world._physical_amounts(world.state)
|
||||
check(physical.HP + physical.EON1 + physical.RP == 40000.0, "Life conserves planets")
|
||||
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Life save validates")
|
||||
var restored = fixture()
|
||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||
world._evolve_batches(world.state, 11e6)
|
||||
restored._evolve_batches(restored.state, 11e6)
|
||||
check(world.state == restored.state, "Life continuation preserves cohorts and carries")
|
||||
dispose(restored)
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1.0
|
||||
world.state.binds.RD = {"EON5": 40000.0}
|
||||
world.initialize_life_batches()
|
||||
world._evolve_batches(world.state, 5e5 + 5e5 / 40.0)
|
||||
check(world.state.binds.RD["C0.0"] == 10.0, "Intelligence emerges at the same host star")
|
||||
check(world.state.amount["C0.0"] == 0.0, "Life transition keeps planet bound")
|
||||
check(world.state.batches.has("C0.0"), "New C0.0 starts civilization evolution")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
var progress: float = 0.0
|
||||
var degraded: float = 0.0
|
||||
var valid_outcomes: bool = true
|
||||
for i in 4001:
|
||||
var products: Dictionary = world._life_products(world.state, world.entities.HP, 1.0)
|
||||
var total: float = 0.0
|
||||
for value in products.values():
|
||||
total += value
|
||||
valid_outcomes = valid_outcomes and value >= 0.0 and value == floorf(value)
|
||||
valid_outcomes = valid_outcomes and total == 1.0
|
||||
progress += products.get("EON1", 0.0)
|
||||
degraded += products.get("RP", 0.0)
|
||||
check(valid_outcomes, "Every single-planet attempt has whole outcomes and conserves population")
|
||||
check(progress == 1.0 and degraded == 10.0, "Rare outcomes accumulate deterministically")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.time = {"datetime": 123.4, "year": 123.4}
|
||||
world.state.binds.RD = {"HP": 2.0}
|
||||
world.initialize_life_batches()
|
||||
check(world.state.batches.HP[0].created_from == 123.4, "Old bound worlds begin life at load time")
|
||||
world._destroy_planets(world.state, "RD", 1.0, 1)
|
||||
world._evolve_batches(world.state, 1e7)
|
||||
check(world._physical_amounts(world.state).HP == 0.0 and world.state.binds.RD.RP == 2.0, "Destroyed life does not resurrect from cohort")
|
||||
dispose(world)
|
||||
world = fixture()
|
||||
world.state.time = {"datetime": 123.4, "year": 123.4}
|
||||
world.state.amount["C0.0"] = 40000.0
|
||||
world.initialize_life_batches()
|
||||
check(world.state.batches["C0.0"][0].created_from == 123.4, "Civilization cohorts begin at the current time")
|
||||
before = world.state.duplicate(true)
|
||||
world.initialize_life_batches()
|
||||
check(world.state == before, "Life batch initialization is idempotent")
|
||||
world._evolve_batches(world.state, 2173.4)
|
||||
check(world.state.amount["C0.1"] == 700.0 and world.state.amount.EON5 == 1000.0, "C0.0 progresses and degrades by Unity rates")
|
||||
check(world.state.amount["C0.0"] == 38300.0, "C0.0 remainder stays alive")
|
||||
dispose(world)
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1.0
|
||||
world.state.binds.RD = {"C0.1": 40000.0}
|
||||
world.initialize_life_batches()
|
||||
world._evolve_batches(world.state, 2050.0)
|
||||
check(world.state.binds.RD["C0.2"] == 700.0, "C0.1 reaches ethical civilization without losing host")
|
||||
check(world.state.binds.RD["C0.0"] == 1000.0, "C0.1 degradation follows Unity")
|
||||
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Extended life save validates")
|
||||
dispose(world)
|
||||
print("Life: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dghmnvnl7ks1b
|
||||
@@ -0,0 +1,58 @@
|
||||
extends SceneTree
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
var global: Node = root.get_node("Global")
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.entities["N"].life_product = {}
|
||||
world.entities["N"].product_per_year_type = "H"
|
||||
world.entities["N"].product_per_year_amount = 10.0
|
||||
world.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
for entity in world.entities.values():
|
||||
entity.free()
|
||||
world.free()
|
||||
|
||||
func run() -> void:
|
||||
var world = fixture()
|
||||
world.initialize_life_batches()
|
||||
world.entities.N.cost = {"H": 2.0}
|
||||
world.entities.N.bind = {"S": 3.0}
|
||||
world.state.amount.H = 10.0
|
||||
world.state.amount.S = 0.0
|
||||
world.state.auto_resources = {"H": 10.0}
|
||||
world.state.batches.H = [{"amount": 10.0, "start_year": 1.0, "end_year": 2.0}]
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
check(not world._try_production(world.state, world.entities.N, 1.0, true), "Binding failure rejects production")
|
||||
check(world.state == before, "Failure preserves costs, batches and budget")
|
||||
world.state.amount.S = 10.0
|
||||
var old_batches: Array = world.state.batches.H
|
||||
check(world._try_production(world.state, world.entities.N, 2.0, true), "Funded production succeeds")
|
||||
check(world.state.amount.H == 6.0 and world.state.amount.S == 4.0, "Exact cost and binding")
|
||||
check(world.state.auto_resources.H == 6.0, "Exact automatic budget")
|
||||
check(old_batches[0].amount == 10.0 and world.state.batches.H[0].amount == 6.0, "Consumed batches isolated")
|
||||
print("Transaction: %d checks, %d failures" % [checks, failures])
|
||||
dispose(world)
|
||||
quit(1 if failures else 0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://byjixspismsnh
|
||||
@@ -0,0 +1,251 @@
|
||||
extends SceneTree
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
var global: Node = root.get_node("Global")
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.entities["N"].life_product = {}
|
||||
world.entities["N"].product_per_year_type = "H"
|
||||
world.entities["N"].product_per_year_amount = 10.0
|
||||
world.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
for entity in world.entities.values():
|
||||
entity.free()
|
||||
world.free()
|
||||
|
||||
func run() -> void:
|
||||
var world = fixture()
|
||||
var rd = world.entities.RD
|
||||
check(rd.planets.HP.chance == 0.01, "Preserve fractional planet probability")
|
||||
check(rd.planets.GG.amount == 2.0 and rd.planets.GG.chance == 0.08, "Read count and chance separately")
|
||||
check(rd._parse_planet_string("HP:1:0.01") == {"HP": {"amount": 1.0, "chance": 0.01}}, "Accept a single planet term")
|
||||
check(rd._parse_planet_string("").is_empty(), "Empty planet definitions stay empty")
|
||||
for id in ["HP", "EON1", "C0.9", "C1.2"]:
|
||||
check(world.entities[id].blastability, "Blast vulnerable: " + id)
|
||||
for id in ["RP", "GG", "AB", "C1.3", "C1.4", "C1.5"]:
|
||||
check(not world.entities[id].blastability, "Blast protected: " + id)
|
||||
|
||||
# Actual GI table: HRG -> SN, with 1% GRB; no random rounding.
|
||||
var totals: Dictionary = {}
|
||||
for i in 100:
|
||||
var products: Dictionary = world._evolution_products(world.state, world.entities.HRG, 1.0)
|
||||
for id in products:
|
||||
totals[id] = totals.get(id, 0.0) + products[id]
|
||||
check(totals.get("SN") == 99.0 and totals.get("GRB") == 1.0, "GI rare outcomes accumulate deterministically")
|
||||
var restored = fixture()
|
||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||
check(world._evolution_products(world.state, world.entities.HRG, 37.0) == restored._evolution_products(restored.state, restored.entities.HRG, 37.0), "Evolution carry survives serialization")
|
||||
dispose(restored)
|
||||
|
||||
# Real RD lifespan, four stars spread over the existing lifespan/40 window.
|
||||
world.state.amount.N = 0.0
|
||||
world.state.amount.RD = 4.0
|
||||
world._append_batch(world.state, "RD", 4.0, 0.0, 0.0)
|
||||
var lifespan: float = rd.lifespan
|
||||
var width: float = rd.lifespan_delta
|
||||
world._evolve_batches(world.state, lifespan)
|
||||
check(world.state.amount.RD == 4.0, "Window start does not kill the whole batch")
|
||||
world._evolve_batches(world.state, lifespan + width / 4.0)
|
||||
check(world.state.amount.RD == 3.0 and world.state.amount.TWD == 1.0, "One quarter of a batch evolves")
|
||||
world._evolve_batches(world.state, lifespan + width / 2.0)
|
||||
check(world.state.amount.RD == 2.0 and world.state.amount.TWD == 2.0, "Half the batch evolves")
|
||||
world._evolve_batches(world.state, lifespan + width)
|
||||
check(world.state.amount.RD == 0.0 and world.state.amount.TWD == 4.0, "Window end completes evolution")
|
||||
check(world.state.batches.RD.is_empty(), "Completed batch removed")
|
||||
dispose(world)
|
||||
# Creation accumulates independently for each star/planet pair.
|
||||
world = fixture()
|
||||
world.state.amount.RD = 100.0
|
||||
for i in 100:
|
||||
world._create_planets(world.state, "RD", 1.0, 0.0, 0.0, 1.0)
|
||||
check(world.state.binds.RD.HP == 1.0, "100 RD at metallicity 1 produce one HP")
|
||||
check(world.state.binds.RD.RP == 21.0, "Rocky planet probability retains fractions")
|
||||
check(world.state.binds.RD.GG == 16.0 and world.state.binds.RD.AB == 120.0, "Planet multiplicity applied")
|
||||
check(world.state.amount.HP == 0.0 and world.state.stats.total_amount.HP == 1.0, "Created planets are bound, counted once")
|
||||
var saves: Node = root.get_node("SaveManager")
|
||||
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Planet carry save validates")
|
||||
restored = fixture()
|
||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||
world._create_planets(world.state, "RD", 37.0, 0.0, 0.0, 0.7)
|
||||
restored._create_planets(restored.state, "RD", 37.0, 0.0, 0.0, 0.7)
|
||||
check(world.state == restored.state, "Planet carry continues after save")
|
||||
dispose(restored)
|
||||
dispose(world)
|
||||
|
||||
# A partially dying dwarf sterilizes HP; survivors and RP move to its heir.
|
||||
world = fixture()
|
||||
world.state.amount.RD = 4.0
|
||||
world.state.binds.RD = {"HP": 8.0, "RP": 4.0, "C1.2": 4.0, "C1.3": 4.0, "GG": 4.0}
|
||||
world._append_batch(world.state, "RD", 4.0, 0.0, 0.0)
|
||||
world._evolve_batches(world.state, lifespan + width / 2.0)
|
||||
check(world.state.amount.RD == 2.0 and world.state.amount.TWD == 2.0, "Partial star transition with planets")
|
||||
check(world.state.binds.RD.HP + world.state.binds.TWD.HP == 4.0, "Half of HP sterilized")
|
||||
check(world.state.binds.RD["C1.3"] == 2.0 and world.state.binds.TWD["C1.3"] == 2.0, "Protected civilization splits between parent and heir")
|
||||
check(world.state.binds.RD.RP + world.state.binds.TWD.RP == 10.0, "Sterilized planets become RP without loss")
|
||||
check(world.state.amount.RP == 0.0 and world.state.amount["C1.3"] == 0.0, "Inherited planets never become free")
|
||||
world._evolve_batches(world.state, lifespan + width)
|
||||
check(world.state.binds.RD["C1.3"] == 0.0 and world.state.binds.TWD["C1.3"] == 4.0, "No planets left at extinct parent type")
|
||||
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Inherited state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.WS = 2.0
|
||||
world.state.binds.WS = {"HP": 4.0, "C1.3": 4.0, "RP": 4.0, "GG": 4.0}
|
||||
world._append_batch(world.state, "WS", 2.0, 0.0, 0.0)
|
||||
world._evolve_batches(world.state, world.entities.WS.lifespan + world.entities.WS.lifespan_delta)
|
||||
var physical: Dictionary = world._physical_amounts(world.state)
|
||||
check(physical.HP == 0.0 and physical["C1.3"] == 0.0 and physical.RP == 0.0 and physical.GG == 0.0, "Level 2 removes all planets, including protected civilization")
|
||||
check(world.state.amount.LRG == 2.0, "Giant evolves after local destruction")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 10.0
|
||||
world.state.binds.RD = {"HP": 10.0, "C1.2": 10.0, "C1.3": 10.0, "RP": 0.0}
|
||||
world._apply_blast(world.state, 5.0, {"blast": 1.0, "density": 1.0})
|
||||
check(world.state.binds.RD.HP == 5.0 and world.state.binds.RD["C1.2"] == 5.0, "Blast damages vulnerable worlds proportionally")
|
||||
check(world.state.binds.RD["C1.3"] == 10.0 and world.state.binds.RD.RP == 10.0, "Blast protects C1.3 and creates RP")
|
||||
world._apply_blast(world.state, 1e6, {"blast": 1.0, "density": 1.0})
|
||||
check(world.state.binds.RD.HP == 0.0 and world.state.binds.RD.RP == 20.0, "Overwhelming blast is capped at available planets")
|
||||
check(not world.state.amount.has("blast"), "Blast is not a resource")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1.0
|
||||
world.state.binds.RD = {"HP": 1.0}
|
||||
for i in 10:
|
||||
world._apply_blast(world.state, 1.0, {"blast": 1.0, "density": 0.1})
|
||||
check(world.state.binds.RD.HP == 0.0 and world.state.binds.RD.RP == 1.0, "Small blast effects accumulate across cycles")
|
||||
dispose(world)
|
||||
|
||||
# Actual supernova output: one SN gives a neutron star, resources, and an event.
|
||||
world = fixture()
|
||||
world.state.amount.SN = 1.0
|
||||
world._append_batch(world.state, "SN", 1.0, 0.0, 0.0)
|
||||
world._evolve_batches(world.state, 2.0)
|
||||
check(world.state.amount.SN == 0.0 and world.state.amount.LNS == 1.0, "Supernova produces a remnant")
|
||||
check(world.state.amount.H == 8.0 and world.state.amount.He == 50.0 and world.state.amount.Met == 11.0, "Supernova returns table resources")
|
||||
check(not world.state.amount.has("blast"), "Supernova dispatches blast output")
|
||||
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Blast state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.H = 1000.0
|
||||
world.state.amount.Met = 100.0
|
||||
check(world.produce("RD", 100.0) == 100.0, "Manual star creation succeeds")
|
||||
check(world.state.binds.RD.HP >= 1.0, "Manual creation dispatches planet formation")
|
||||
check(world.state.amount.H == 900.0, "Planet creation does not duplicate or spend stellar fuel")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.N = 0.0
|
||||
world.state.amount.RD = 1.0
|
||||
world.state.amount.H = 100.0
|
||||
world.state.amount.Met = 100.0
|
||||
world.state.auto_resources.H = 100.0
|
||||
world.evolute(0.1)
|
||||
check(world.state.amount.RD == 101.0 and world.state.amount.H == 0.0, "Automatic formation uses its resource budget")
|
||||
check(world.state.binds.RD.HP >= 1.0, "Automatic formation also creates planets")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.entities.N.auto = 1.0
|
||||
# 1000 newly produced automatic H reserve 1 H, enough for 10 HBD.
|
||||
world.evolute(100.0)
|
||||
check(world.state.amount.HBD == 10.0, "Brown dwarfs form without existing ordinary stars")
|
||||
check(world.state.amount.H == 999.0 and world.state.auto_resources.H == 999.0, "Brown dwarfs consume exactly their reserved hydrogen")
|
||||
check(world.get_item_amounts().BD == 10.0 and world.state.batches.HBD.size() > 0, "Brown dwarfs appear in counter and evolution batches")
|
||||
world.entities.N.product_per_year_amount = 0.0
|
||||
world.evolute(100.0)
|
||||
check(world.state.amount.HBD == 10.0, "Stored automatic budget is not taxed repeatedly")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.entities.N.auto = 1.0
|
||||
world.state.amount.RD = 1.0
|
||||
world.evolute(50.0)
|
||||
check(world.state.amount.HBD == 5.0 and is_equal_approx(world.state.auto_star_deficits.HBD_fuel, 0.5), "Individual dwarfs retain prepaid fractional fuel")
|
||||
check(world.state.amount.H == 0.0, "Prepaid fuel is not available to ordinary stars")
|
||||
var resumed = fixture()
|
||||
resumed.entities.N.auto = 1.0
|
||||
resumed.state = bytes_to_var(var_to_bytes(world.state))
|
||||
world.evolute(50.0)
|
||||
resumed.evolute(50.0)
|
||||
check(world.state.amount.HBD == 10.0, "Accumulated quota eventually creates a whole batch")
|
||||
check(world.state == resumed.state, "Brown dwarf quota survives serialization deterministically")
|
||||
check(world.state.amount.RD == 1000.0 and world.state.amount.H == 0.0, "Shared automatic hydrogen is spent once")
|
||||
world.state.batches.HBD = [{"amount": 10.0, "start_year": 101.0, "end_year": 101.0}]
|
||||
world._evolve_batches(world.state, 101.0)
|
||||
check(world.state.amount.HBD == 0.0 and world.state.amount.CBD == 10.0 and world.get_item_amounts().BD == 10.0, "Cooling preserves aggregate brown dwarf count")
|
||||
dispose(resumed)
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
var saw_individual: bool = false
|
||||
for i in range(250):
|
||||
world.state.amount.H += 1.0
|
||||
world.state.auto_resources.H = world.state.auto_resources.get("H", 0.0) + 1.0
|
||||
var before: float = world.state.amount.HBD
|
||||
world._produce_brown_dwarfs(world.state, 1.0, float(i), float(i + 1))
|
||||
var born: float = world.state.amount.HBD - before
|
||||
check(born == 0.0 or born == 1.0, "Small fuel increments produce individual dwarfs")
|
||||
saw_individual = saw_individual or born == 1.0
|
||||
check(saw_individual, "Random rounding does not wait for ten dwarfs")
|
||||
resumed = fixture()
|
||||
resumed.state = bytes_to_var(var_to_bytes(world.state))
|
||||
for i in range(250, 500):
|
||||
for simulation in [world, resumed]:
|
||||
simulation.state.amount.H += 1.0
|
||||
simulation.state.auto_resources.H += 1.0
|
||||
simulation._produce_brown_dwarfs(simulation.state, 1.0, float(i), float(i + 1))
|
||||
check(world.state == resumed.state, "Random sequence and prepaid fuel survive saving")
|
||||
check(is_equal_approx(world.state.amount.H + world.state.amount.HBD * 0.1 + world.state.auto_star_deficits.HBD_fuel, 500.0), "Individual production conserves fuel including prepaid reserve")
|
||||
check(is_equal_approx(world.state.amount.HBD + world.state.auto_star_deficits.HBD, 5.0), "Signed remainder preserves the 0.1 percent allocation")
|
||||
var paused_count: float = world.state.amount.HBD
|
||||
for i in 10: world._produce_brown_dwarfs(world.state, 0.0, 500.0, 501.0)
|
||||
check(world.state.amount.HBD == paused_count, "No repeated random attempts without new fuel")
|
||||
dispose(resumed)
|
||||
dispose(world)
|
||||
|
||||
# Two simultaneous successors share surviving planets in the same 99:1 ratio.
|
||||
world = fixture()
|
||||
world.entities.HRG.destroy_level = 0
|
||||
world.state.amount.HRG = 100.0
|
||||
world.state.binds.HRG = {"C1.3": 100.0}
|
||||
world.state.batches.HRG = [{"start_year": 0.1, "end_year": 0.1, "amount": 100.0}]
|
||||
world._evolve_batches(world.state, 0.1)
|
||||
check(world.state.binds.SN["C1.3"] == 99.0 and world.state.binds.GRB["C1.3"] == 1.0, "Planets split among actual alternative successors")
|
||||
check(world.state.binds.HRG["C1.3"] == 0.0, "Alternative evolution leaves no orphan bindings")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.N = 0.0
|
||||
world.state.amount.HRG = 1.0
|
||||
world.state.batches.HRG = [{"start_year": 0.1, "end_year": 0.1, "amount": 1.0}]
|
||||
world.evolute(3.0)
|
||||
check(world.state.amount.LNS == 1.0 and world.state.amount.SN == 0.0, "Long step follows multiple stellar generations")
|
||||
check(world.state.amount.S >= 99.0, "Short-lived newborn supernova emits S before disappearing")
|
||||
dispose(world)
|
||||
|
||||
print("Stellar rules: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ccegf5mell4ph
|
||||
@@ -0,0 +1,91 @@
|
||||
extends SceneTree
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
var global: Node = root.get_node("Global")
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.entities["N"].life_product = {}
|
||||
world.entities["N"].product_per_year_type = "H"
|
||||
world.entities["N"].product_per_year_amount = 10.0
|
||||
world.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
for entity in world.entities.values():
|
||||
entity.free()
|
||||
world.free()
|
||||
|
||||
func run() -> void:
|
||||
var saves: Node = root.get_node("SaveManager")
|
||||
var world = fixture()
|
||||
world.evolute(0.09)
|
||||
check(world.state.time.year == 0.0, "Do not process before a tenth boundary")
|
||||
check(world.state.amount.H == 0.0, "No premature production")
|
||||
world.evolute(0.01)
|
||||
check(world.state.time.year == 0.1, "Process the first tenth")
|
||||
check(world.state.amount.H == 1.0, "Production runs at 0.1 year")
|
||||
world.evolute(0.2)
|
||||
check(world.state.time.year == 0.3 and world.state.amount.H == 3.0, "Do not reprocess previous tenths")
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
for invalid in [0.0, -1.0, NAN, INF, 1e308]:
|
||||
world.evolute(invalid)
|
||||
check(world.state == before, "Invalid/overflow interval rejected")
|
||||
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Fractional save is valid")
|
||||
var invalid_save: Dictionary = world.state.duplicate(true)
|
||||
invalid_save.time.year = 0.25
|
||||
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}) != "", "Reject inconsistent fractional clock")
|
||||
dispose(world)
|
||||
|
||||
for fps in [30, 60, 144]:
|
||||
world = fixture()
|
||||
for frame in fps:
|
||||
world.evolute(1.0 / fps)
|
||||
check(world.state.time.year == 1.0, "One second at %d fps" % fps)
|
||||
check(world.state.amount.H == 10.0, "Production across frames at %d fps" % fps)
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.evolute(0.05)
|
||||
world.speed = 2
|
||||
world.evolute(0.005)
|
||||
check(world.state.time.year == 0.1, "Speed change preserves accumulated fraction")
|
||||
world.speed = 100
|
||||
check(world.speed == 5 and world.state.speed_in_year == 10000.0, "Upper speed limit unchanged")
|
||||
world.speed = -10
|
||||
check(world.speed == 1 and world.state.speed_in_year == 1.0, "Lower speed limit unchanged")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.entities.N.output_and = {"He": 1.0}
|
||||
world.entities.N.output_or = {}
|
||||
world.state.batches.N = [{"start_year": 0.1, "end_year": 0.1, "amount": 1.0}]
|
||||
world.evolute(0.1)
|
||||
check(world.state.amount.N == 0.0 and world.state.amount.He == 1.0, "Existing evolution executes at fractional boundary")
|
||||
dispose(world)
|
||||
check(Clock.processed_year(0.099) == 0.0, "Do not round a genuine incomplete tenth forward")
|
||||
check(Clock.processed_year(1e9 + 0.45) == 1e9 + 0.4, "Large ages preserve fractional precision")
|
||||
print("Time state: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bwon17tg8v8di
|
||||
@@ -0,0 +1,55 @@
|
||||
extends SceneTree
|
||||
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(value: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not value:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func run() -> void:
|
||||
var scene = load("res://world/world.tscn").instantiate()
|
||||
var stage = scene.get_node("Stage")
|
||||
stage.owniverse.free()
|
||||
stage.set_script(load("res://tests/fixtures/ui_stage.gd"))
|
||||
root.add_child(scene)
|
||||
await process_frame
|
||||
await process_frame
|
||||
stage.stage_page_scroller.scroll_horizontal = stage.PAGE_WIDTH
|
||||
await process_frame
|
||||
stage.owniverse.state.amount.H = 1000.0
|
||||
stage.owniverse.state.amount.RD = 2.0
|
||||
stage.owniverse.refresh_item_amounts()
|
||||
stage._update_ui(0.0)
|
||||
var hydrogen = stage.owniverse_items.H
|
||||
var dwarf = stage.owniverse_items.RD
|
||||
check(hydrogen.description.text == "1 000", "Visible page updates immediately on navigation")
|
||||
check(dwarf.description.text == "", "Offscreen counters are not rendered")
|
||||
stage.owniverse.state.amount.H = 2000.0
|
||||
stage.owniverse.refresh_item_amounts()
|
||||
check(stage.owniverse.item_amounts_dirty and stage.owniverse.items_to_update.H == 1000.0, "Simulation invalidates without recalculating display sums")
|
||||
stage._update_ui(0.05)
|
||||
check(hydrogen.description.text == "1 000", "No refresh before interval")
|
||||
stage._update_ui(0.051)
|
||||
check(hydrogen.description.text == "2 000" and not stage.owniverse.item_amounts_dirty, "Latest state appears at interval")
|
||||
stage.item_activated(hydrogen)
|
||||
check(hydrogen.description.text == root.get_node("Global").format_number(stage.owniverse.state.amount.H), "Player production updates counter immediately")
|
||||
check(stage.owniverse.state.amount.H > 2000.0, "Production action actually executed")
|
||||
dwarf.visible = true
|
||||
stage.stage_page_scroller.scroll_horizontal = 2 * stage.PAGE_WIDTH
|
||||
await process_frame
|
||||
stage._update_ui(0.0)
|
||||
check(dwarf.description.text == "2", "Newly visible page receives cached latest values")
|
||||
stage.owniverse.state.time.year = 1234.5
|
||||
stage._update_ui(0.1)
|
||||
check(stage.date_time.text == "1 234.5", "Time uses throttled display with tenths")
|
||||
for entity in stage.owniverse.entities.values(): entity.free()
|
||||
stage.owniverse.free()
|
||||
scene.free()
|
||||
print("UI updates: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8bt2hxqm4hsf
|
||||
+44
-10
@@ -23,24 +23,58 @@ var action_values: Dictionary = {
|
||||
}
|
||||
|
||||
|
||||
func set_actions(item_id: String, value: float) -> void:
|
||||
func set_actions(item_id: String, value: float, preview: Callable = Callable()) -> void:
|
||||
current_item_id = item_id
|
||||
_set_action_values(_get_distribution(value))
|
||||
var requests: Array = _get_distribution(value)
|
||||
var unique_requests: Array = []
|
||||
var displayed: Array = []
|
||||
var labels: Dictionary = {}
|
||||
# Prefer the largest request (including MAX) when previews or rounded
|
||||
# scientific labels coincide. Keep the request paired with its preview.
|
||||
for i in range(requests.size() - 1, -1, -1):
|
||||
if requests[i] <= 0.0: continue
|
||||
var amount: float = preview.call(requests[i]) if preview.is_valid() else requests[i]
|
||||
if not is_finite(amount) or amount < 1.0 or amount != floorf(amount): continue
|
||||
var text: String = _format_action_value(amount)
|
||||
if displayed.has(amount) or labels.has(text): continue
|
||||
labels[text] = true
|
||||
unique_requests.push_front(requests[i])
|
||||
displayed.push_front(amount)
|
||||
while unique_requests.size() < 5:
|
||||
unique_requests.append(0.0)
|
||||
displayed.append(0.0)
|
||||
_set_action_values(unique_requests, displayed)
|
||||
|
||||
func _get_distribution(value: float) -> Array:
|
||||
var distribution: Array = [value, 0, 0, 0, 0]
|
||||
|
||||
var exp_value = int(floor(log(abs(value)) / log(10.0)))
|
||||
var floor_value = max(exp_value - 4, -1)
|
||||
for ev in range(exp_value, floor_value, -1):
|
||||
distribution.push_front(pow(10.0, ev))
|
||||
if value < 1.0 or not is_finite(value):
|
||||
return [0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
value = floorf(value)
|
||||
var distribution: Array = []
|
||||
var power: float = 1.0
|
||||
while power < value:
|
||||
distribution.append(power)
|
||||
if distribution.size() > 4: distribution.pop_front()
|
||||
power *= 10.0
|
||||
distribution.append(value)
|
||||
while distribution.size() < 5: distribution.append(0.0)
|
||||
return distribution
|
||||
|
||||
func _set_action_values(values: Array) -> void:
|
||||
func _set_action_values(values: Array, displayed: Array) -> void:
|
||||
for ind in range(0, 5):
|
||||
action_values[ind] = values[ind]
|
||||
action_labels[ind].text = Global.format_number(values[ind]) if values[ind] > 0 else ""
|
||||
# Keep the original request for execution; show the exact planned output.
|
||||
var text: String = _format_action_value(displayed[ind])
|
||||
if action_labels[ind].text != text:
|
||||
action_labels[ind].text = text
|
||||
action_labels[ind].get_parent().get_node("TextureButton").disabled = values[ind] <= 0.0
|
||||
|
||||
|
||||
func _format_action_value(value: float) -> String:
|
||||
if value < 1.0 or not is_finite(value) or value != floorf(value):
|
||||
return ""
|
||||
return Global.format_number(value)
|
||||
|
||||
|
||||
func _on_action_button_pressed(button_id: int) -> void:
|
||||
if action_values.get(button_id, 0.0) > 0.0:
|
||||
action_pressed.emit(page_id, action_values[button_id])
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[ext_resource type="Script" uid="uid://c2ech05mh56y8" path="res://ui/actions/actions.gd" id="1_cjivn"]
|
||||
[ext_resource type="FontFile" uid="uid://d3k1ddvv0rmhs" path="res://fonts/Roboto_Condensed/RobotoCondensed-Light.ttf" id="1_lgdyb"]
|
||||
[ext_resource type="Texture2D" uid="uid://gsclv7fr1rbr" path="res://ui/actions/action_button.png" id="2_cjivn"]
|
||||
[ext_resource type="Texture2D" path="res://ui/actions/action_button.png" id="2_cjivn"]
|
||||
|
||||
[node name="Actions" type="HBoxContainer" unique_id=503388208]
|
||||
custom_minimum_size = Vector2(0, 216)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[gd_scene format=3 uid="uid://dg15dpxiepicb"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://chxxghyul1im7" path="res://ui/border/border-2px.png" id="1_6kaf8"]
|
||||
[ext_resource type="Texture2D" path="res://ui/border/border-2px.png" id="1_6kaf8"]
|
||||
|
||||
[node name="Border" type="TextureRect"]
|
||||
[node name="Border" type="TextureRect" unique_id=1148150816]
|
||||
texture = ExtResource("1_6kaf8")
|
||||
|
||||
@@ -20,7 +20,9 @@ var item_type = ""
|
||||
|
||||
|
||||
func set_label(value: float) -> void:
|
||||
description.text = Global.format_number(value)
|
||||
var text: String = Global.format_number(value)
|
||||
if description.text != text:
|
||||
description.text = text
|
||||
#description.text = str(value)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[gd_scene format=3 uid="uid://d3xjfxwe3w16v"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bxxllddlgurus" path="res://ui/owniverse_item/owniverse_item.gd" id="1_d5xam"]
|
||||
[ext_resource type="Texture2D" uid="uid://s16jmvdrq8cb" path="res://ui/owniverse_item/selected.png" id="2_pbv1j"]
|
||||
[ext_resource type="Texture2D" path="res://ui/owniverse_item/selected.png" id="2_pbv1j"]
|
||||
|
||||
[node name="Button" type="Control" unique_id=667620208]
|
||||
visible = false
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[ext_resource type="Script" uid="uid://bq7xkdvq4iejn" path="res://world/background/starry_sky_layer.gd" id="1_st4ay"]
|
||||
|
||||
[node name="StarrySkyLayer" type="Parallax2D"]
|
||||
[node name="StarrySkyLayer" type="Parallax2D" unique_id=1953063300]
|
||||
scroll_offset = Vector2(1080, 1920)
|
||||
repeat_size = Vector2(1080, 1920)
|
||||
repeat_times = 3
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
extends RefCounted
|
||||
|
||||
const State = preload("res://world/owniverse/civilization_state.gd")
|
||||
const ETHICS = State.ETHICS
|
||||
const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"}
|
||||
|
||||
static func total(pop: Dictionary) -> float:
|
||||
var result: float = 0.0
|
||||
for n in pop.values(): result += n
|
||||
return result
|
||||
|
||||
static func add(to: Dictionary, from: Dictionary) -> void:
|
||||
for key in from: to[key] = to.get(key, 0.0) + from[key]
|
||||
|
||||
static func take(world, pop: Dictionary, count: float) -> Dictionary:
|
||||
var taken: Dictionary = world._allocate_whole(pop, count)
|
||||
for key in taken: pop[key] -= taken[key]
|
||||
return taken
|
||||
|
||||
static func rate(target: Dictionary, key: String, count: float, probability: float) -> float:
|
||||
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
||||
var expected: float = count * maxf(0.0, probability) + target.civilization_remainders.get(key, 0.0)
|
||||
var actual: float = floorf(snappedf(expected, 1e-10))
|
||||
target.civilization_remainders[key] = maxf(0.0, expected - actual)
|
||||
return actual
|
||||
|
||||
# Fixed-size largest-remainder apportionment with saved signed rounding errors.
|
||||
# Runtime depends on the number of branches, never the number of civilizations.
|
||||
static func split(target: Dictionary, key: String, count: float, weights: Dictionary) -> Dictionary:
|
||||
var result: Dictionary = {}
|
||||
var sum_weights: float = total(weights)
|
||||
if count <= 0.0 or sum_weights <= 0.0: return result
|
||||
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
||||
var expected: Dictionary = {}
|
||||
var positive_sum: float = 0.0
|
||||
for id in weights:
|
||||
expected[id] = count * weights[id] / sum_weights + target.civilization_remainders.get(key + ":" + id, 0.0)
|
||||
positive_sum += maxf(0.0, expected[id])
|
||||
var order: Array = []
|
||||
var assigned: float = 0.0
|
||||
for id in weights:
|
||||
var normalized: float = maxf(0.0, expected[id]) * count / positive_sum if positive_sum > 0.0 else 0.0
|
||||
result[id] = floorf(normalized)
|
||||
assigned += result[id]
|
||||
order.append({"id": id, "rest": normalized - result[id]})
|
||||
order.sort_custom(func(a, b): return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
|
||||
for entry in order:
|
||||
if assigned >= count: break
|
||||
result[entry.id] += 1.0
|
||||
assigned += 1.0
|
||||
for id in weights:
|
||||
target.civilization_remainders[key + ":" + id] = clampf(expected[id] - result[id], -1.0, 1.0)
|
||||
return result
|
||||
|
||||
static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictionary,
|
||||
diplomacy: bool = false, colony: bool = false) -> Dictionary:
|
||||
var weights: Dictionary = {}
|
||||
for ethic in pop:
|
||||
var influence: float = float(rows[ethic].diplomacy_rate) if diplomacy else 1.0
|
||||
for other in ETHICS:
|
||||
var weight: float = 9.0 if colony else 1.0
|
||||
if other == ethic: weight = 960.0 if colony else 196.0
|
||||
elif other == OPPOSITE[ethic]: weight = 4.0 if colony else 0.0
|
||||
weights[other] = weights.get(other, 0.0) + pop[ethic] * influence * weight
|
||||
if total(weights) <= 0.0: return pop.duplicate()
|
||||
return split(target, key, total(pop), weights)
|
||||
|
||||
static func emit(plan: Dictionary, destination: String, pop: Dictionary) -> void:
|
||||
if not plan.outputs.has(destination): plan.outputs[destination] = {}
|
||||
add(plan.outputs[destination], pop)
|
||||
|
||||
static func fall(target: Dictionary, plan: Dictionary, rules: Dictionary, pop: Dictionary) -> void:
|
||||
for ethic in pop:
|
||||
var weights: Dictionary = {}
|
||||
for term in str(rules[plan.id].ethics[ethic].fall).split("^", false):
|
||||
var pair: PackedStringArray = term.split(":")
|
||||
weights[pair[0]] = float(pair[1])
|
||||
if weights.is_empty(): weights[rules[plan.id].previous] = 1.0
|
||||
var shares: Dictionary = split(target, "fall:" + plan.id + ":" + ethic, pop[ethic], weights)
|
||||
for stage in shares: emit(plan, stage, {ethic: shares[stage]})
|
||||
|
||||
static func progress(world, target: Dictionary, rules: Dictionary, budget: Dictionary,
|
||||
plan: Dictionary, pop: Dictionary) -> Dictionary:
|
||||
var successor: String = rules[plan.id].next
|
||||
var admitted: float = State.admit_transition(budget, rules, plan.id, successor, total(pop))
|
||||
var accepted: Dictionary = take(world, pop, admitted)
|
||||
emit(plan, successor, accepted)
|
||||
return pop
|
||||
|
||||
static func new_plan(id: String, year: float) -> Dictionary:
|
||||
return {"id": id, "from": year, "to": -INF, "amount": 0.0, "outputs": {}}
|
||||
|
||||
static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop: Dictionary, count: float, year: float) -> Dictionary:
|
||||
var old_total: float = total(pop)
|
||||
var removed: Dictionary = take(world, pop, count)
|
||||
var actual: float = total(removed)
|
||||
if actual > 0.0:
|
||||
world._scale_batches(target, plan.id, (old_total - actual) / old_total)
|
||||
plan.amount += actual
|
||||
plan.to = maxf(plan.to, year)
|
||||
return removed
|
||||
|
||||
static func prepare(world, target: Dictionary, expired: Dictionary, year: float, budget: Dictionary, rules: Dictionary) -> Dictionary:
|
||||
State.reconcile(world, target, rules)
|
||||
var remaining: Dictionary = target.ethics.duplicate(true)
|
||||
var plans: Dictionary = {}
|
||||
var colonies: Array = []
|
||||
var collateral: Dictionary = {}
|
||||
if not target.has("civilization_stats"): target.civilization_stats = {}
|
||||
var ids: Array = expired.keys()
|
||||
ids.sort()
|
||||
for id in ids:
|
||||
if not rules.has(id) or expired[id].is_empty(): continue
|
||||
var plan: Dictionary = new_plan(id, year)
|
||||
plans[id] = plan
|
||||
for part: Dictionary in expired[id]:
|
||||
plan.amount += part.amount
|
||||
plan.from = minf(plan.from, part.from)
|
||||
plan.to = maxf(plan.to, part.to)
|
||||
var triggered: Dictionary = take(world, remaining[id], plan.amount)
|
||||
var successes: Dictionary = {}
|
||||
var failures: Dictionary = {}
|
||||
var warriors: Dictionary = {}
|
||||
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 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))
|
||||
successes[ethic] = bounded.pro
|
||||
failures[ethic] = bounded.deg
|
||||
triggered[ethic] -= bounded.pro + bounded.deg
|
||||
add(triggered, progress(world, target, rules, budget, plan, successes))
|
||||
fall(target, plan, rules, failures)
|
||||
if rules[id].fights:
|
||||
for ethic in warriors: warriors[ethic] = minf(warriors[ethic], triggered[ethic])
|
||||
var wars: float = minf(total(warriors), floorf((total(triggered) + total(remaining[id])) / 2.0))
|
||||
warriors = world._allocate_whole(warriors, wars)
|
||||
for ethic in warriors: triggered[ethic] -= warriors[ethic]
|
||||
var outcomes: Dictionary = split(target, id + ":wars", wars, {"deg": 1.0, "pro25": 2.0, "pro50": 3.0, "pro75": 3.0})
|
||||
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))
|
||||
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 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
|
||||
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)
|
||||
if total(settlers) > 0.0: colonies.append({"stage": rules[id].colony, "source": id, "population": settlers})
|
||||
# Collateral is applied after all stages planned their own attempts.
|
||||
for id in collateral:
|
||||
if not rules.has(id): continue
|
||||
if not plans.has(id): plans[id] = new_plan(id, year)
|
||||
var plan: Dictionary = plans[id]
|
||||
var retry: Dictionary = plan.outputs.get(id, {})
|
||||
var amount: float = minf(collateral[id], total(retry) + total(remaining[id]))
|
||||
var sides: Dictionary = world._allocate_whole({"due": total(retry), "later": total(remaining[id])}, amount)
|
||||
var victims: Dictionary = take(world, retry, sides.due)
|
||||
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
|
||||
fall(target, plan, rules, victims)
|
||||
target.civilization_stats.collateral = target.civilization_stats.get("collateral", 0.0) + total(victims)
|
||||
var incoming: Dictionary = {}
|
||||
var planetary: Dictionary = {}
|
||||
for id in plans:
|
||||
var plan: Dictionary = plans[id]
|
||||
var products: Dictionary = {}
|
||||
for destination in plan.outputs:
|
||||
var pop: Dictionary = plan.outputs[destination]
|
||||
if rules.has(destination):
|
||||
if destination != "Ct":
|
||||
pop = drift(target, id + ":" + destination + ":drift", pop, rules[id].ethics, destination == id and rules[id].diplomacy)
|
||||
if not incoming.has(destination): incoming[destination] = {}
|
||||
add(incoming[destination], pop)
|
||||
products[destination] = total(pop)
|
||||
if State.is_space_civilization(id, rules) and world.entities[destination].group_as == "Planet":
|
||||
planetary[destination] = planetary.get(destination, 0.0) + total(pop)
|
||||
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": maxf(plan.from, plan.to), "products": products}]
|
||||
return {"remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies}
|
||||
|
||||
static func finish(world, target: Dictionary, transaction: Dictionary, year: float, rules: Dictionary) -> void:
|
||||
target.ethics = transaction.remaining
|
||||
for id in transaction.incoming:
|
||||
if not target.ethics.has(id): target.ethics[id] = {}
|
||||
add(target.ethics[id], transaction.incoming[id])
|
||||
# Space civilizations that fall back to planetary stages need a stellar host.
|
||||
var physical: Dictionary = world._physical_amounts(target)
|
||||
var hosts: Dictionary = {}
|
||||
for id in physical:
|
||||
if world.entities[id].event_group == "DwStar" and physical[id] > 0.0: hosts[id] = physical[id]
|
||||
for id in transaction.planetary:
|
||||
var shares: Dictionary = split(target, "return:" + id, minf(transaction.planetary[id], target.amount.get(id, 0.0)), hosts)
|
||||
for owner in shares:
|
||||
if not target.binds.has(owner): target.binds[owner] = {}
|
||||
target.binds[owner][id] = target.binds[owner].get(id, 0.0) + shares[owner]
|
||||
target.amount[id] -= shares[owner]
|
||||
colonize(world, target, transaction.colonies, year, rules)
|
||||
State.reconcile(world, target, rules)
|
||||
var worship: float = 0.0
|
||||
for id in rules:
|
||||
if not rules[id].worships: continue
|
||||
for ethic in target.ethics[id]: worship += target.ethics[id][ethic] * float(rules[id].ethics[ethic].worship)
|
||||
target.civilization_stats.worship = worship
|
||||
|
||||
static func colonize(world, target: Dictionary, colonies: Array, year: float, rules: Dictionary) -> void:
|
||||
var physical: Dictionary = world._physical_amounts(target)
|
||||
var stock: Dictionary = {}
|
||||
for id in ["GG", "AB", "RP", "HP"]: stock[id] = physical.get(id, 0.0)
|
||||
var requests: Dictionary = {}
|
||||
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
|
||||
var granted: Dictionary = world._allocate_whole(requests, total(stock))
|
||||
for i in colonies.size():
|
||||
var colony: Dictionary = colonies[i]
|
||||
var population: Dictionary = world._allocate_whole(colony.population, granted[str(i)])
|
||||
population = drift(target, colony.source + ":colonies", population, rules[colony.source].ethics, false, true)
|
||||
var count: float = total(population)
|
||||
var targets: Dictionary = world._allocate_whole(stock, count)
|
||||
for id in targets:
|
||||
var amount: float = targets[id]
|
||||
if amount <= 0.0: continue
|
||||
var slots: Dictionary = {"": target.amount.get(id, 0.0)}
|
||||
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0.0)
|
||||
var taken: Dictionary = world._allocate_whole(slots, amount)
|
||||
world._scale_batches(target, id, 1.0 - amount / stock[id])
|
||||
stock[id] -= amount
|
||||
for owner in taken:
|
||||
if taken[owner] <= 0.0: continue
|
||||
if owner == "": target.amount[id] -= taken[owner]
|
||||
else: target.binds[owner][id] -= taken[owner]
|
||||
var created: float = world._credit(target, colony.stage, taken[owner])
|
||||
if owner != "":
|
||||
target.amount[colony.stage] -= created
|
||||
target.binds[owner][colony.stage] = target.binds[owner].get(colony.stage, 0.0) + created
|
||||
world._append_batch(target, colony.stage, amount, year, year)
|
||||
if not target.ethics.has(colony.stage): target.ethics[colony.stage] = {}
|
||||
add(target.ethics[colony.stage], population)
|
||||
target.civilization_stats.colonies = target.civilization_stats.get("colonies", 0.0) + count
|
||||
@@ -0,0 +1 @@
|
||||
uid://cypckxhb2n2r8
|
||||
@@ -0,0 +1,235 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://bw88euxvptt51
|
||||
@@ -0,0 +1,87 @@
|
||||
extends RefCounted
|
||||
|
||||
const GREEN = "#40cc40"
|
||||
const RED = "#ff6060"
|
||||
const GREY = "#888888"
|
||||
|
||||
static func number(value: float, digits: int = 0) -> String:
|
||||
if not is_finite(value):
|
||||
return "—"
|
||||
if absf(value) >= 1e5:
|
||||
var exponent: int = int(floor(log(absf(value)) / log(10.0)))
|
||||
return "%.4fE%+d" % [value / pow(10.0, exponent), exponent]
|
||||
return "%.*f" % [digits, value]
|
||||
|
||||
static func colored(text: String, color: String) -> String:
|
||||
return "[color=" + color + "]" + text + "[/color]"
|
||||
|
||||
static func matches(world, id: String) -> Array:
|
||||
var result: Array = []
|
||||
for key in world.entities:
|
||||
var entity = world.entities[key]
|
||||
if key == id or entity.count_as == id or entity.group_as == id:
|
||||
if not world._is_group(key):
|
||||
result.append(key)
|
||||
return result
|
||||
|
||||
static func quantities(world, id: String) -> Dictionary:
|
||||
var free: float = 0.0
|
||||
var total: float = 0.0
|
||||
var physical: Dictionary = world._physical_amounts(world.state)
|
||||
for key in matches(world, id):
|
||||
free += world.state.amount.get(key, 0.0)
|
||||
total += physical.get(key, 0.0)
|
||||
if id == "SMBH":
|
||||
free = world.state.amount.get(id, 0.0)
|
||||
total = free
|
||||
return {"free": free, "total": total}
|
||||
|
||||
static func costs(world, global: Node, id: String) -> String:
|
||||
if not world.entities.has(id):
|
||||
return ""
|
||||
var lines: PackedStringArray = []
|
||||
for requirements in [world.entities[id].cost, world.entities[id].bind]:
|
||||
for resource in requirements:
|
||||
var required: float = requirements[resource]
|
||||
if required < 0.0:
|
||||
continue
|
||||
var available: float = floorf(world._available(world.state, resource))
|
||||
var name: String = str(global.get_template(resource, "Entity", "Name").get("text", resource))
|
||||
lines.append(colored(name + ": " + number(required) + " (" + number(available) + ")", GREEN if available >= required else RED))
|
||||
return "\n".join(lines)
|
||||
|
||||
static func variables(world, global: Node, id: String, rate: float = 0.0) -> Dictionary:
|
||||
var counts: Dictionary = quantities(world, id)
|
||||
var show_total: bool = false
|
||||
for key in matches(world, id):
|
||||
show_total = show_total or world.entities[key].group_as == "Planet"
|
||||
var amount: String = number(counts.total if show_total else counts.free) if world.entities.has(id) else "—"
|
||||
if not show_total and counts.total > counts.free:
|
||||
amount += " " + colored("(" + number(counts.total) + ")", GREY)
|
||||
if rate != 0.0:
|
||||
amount += " | " + colored(("−" if rate < 0.0 else "+") + number(absf(rate), 1), RED if rate < 0.0 else GREEN)
|
||||
var physical: Dictionary = world._physical_amounts(world.state)
|
||||
var voids: float = 0.0
|
||||
for key in physical:
|
||||
voids += physical[key] * world.entities[key].voids
|
||||
var he: float = world.state.amount.get("He", 0.0)
|
||||
var h: float = world.state.amount.get("H", 0.0)
|
||||
var mixture: String = "—"
|
||||
if he + h > 0.0:
|
||||
mixture = "He: " + number(100.0 * he / (he + h), 2) + "% / H: " + number(100.0 * h / (he + h), 2) + "%"
|
||||
var environment: Dictionary = world.stellar_environment(world.state)
|
||||
return {
|
||||
"amount": amount, "colored costs": costs(world, global, id),
|
||||
"lifespan": number(world.entities[id].lifespan) if world.entities.has(id) else "—",
|
||||
"voids": number(voids), "he / h": "\n" + mixture,
|
||||
"metallicity": number(environment.metallicity * 100.0, 2) + "%",
|
||||
"энергия": number(world.state.energy), "плотность": number(environment.density, 6),
|
||||
"лет в секунду": number(world.state.speed_in_year),
|
||||
"amount1": "—", "amount2": "—", "amount3": "—", "amount4": "—",
|
||||
}
|
||||
|
||||
static func render(world, global: Node, id: String, category: String = "Entity", rate: float = 0.0) -> String:
|
||||
var values: Dictionary = variables(world, global, id, rate)
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dumnryq20c2ij
|
||||
+1423
-168
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,13 @@ var cost: Dictionary = {}
|
||||
var bind: Dictionary = {}
|
||||
var mass: float
|
||||
var space: int
|
||||
var life_rule: Dictionary = {}
|
||||
var lifespan: float
|
||||
var lifespan_delta: float
|
||||
var auto: float
|
||||
var gamma: float
|
||||
var blast: int
|
||||
var blastability: bool = false
|
||||
var life_product: Dictionary = {}
|
||||
var product_per_year_type: String = ""
|
||||
var product_per_year_amount: float = 0
|
||||
@@ -38,13 +40,18 @@ func _init(values: Dictionary) -> void:
|
||||
unlock = _parse_and_string(values.unlock)
|
||||
cost = _parse_and_string(values.cost)
|
||||
bind = _parse_and_string(values.bind)
|
||||
#print(id, " | ", cost, " | ", bind)
|
||||
mass = float(values.mass)
|
||||
space = int(values.space)
|
||||
lifespan = float(values.lifespan)
|
||||
lifespan_delta = lifespan / Global.batch_count / 2 if lifespan > 10 else lifespan
|
||||
life_rule = Global.LifeRules.get(id, {}).duplicate(true)
|
||||
if Global.CivilizationRules.has(id):
|
||||
life_rule = {"ethical": true, "span": Global.CivilizationRules[id].span}
|
||||
lifespan = float(life_rule.span) if not life_rule.is_empty() else float(values.lifespan)
|
||||
lifespan_delta = lifespan / Global.batch_count if lifespan > 10 else lifespan
|
||||
auto = float(values.auto)
|
||||
gamma = float(values.gamma)
|
||||
blast = int(values.blast)
|
||||
blastability = str(values.get("blastability", "")) == "1"
|
||||
|
||||
life_product = _parse_and_string(values.life_product)
|
||||
for key in life_product.keys():
|
||||
@@ -92,12 +99,19 @@ func _parse_and_string(source_line: String) -> Dictionary:
|
||||
|
||||
func _parse_planet_string(source_line: String) -> Dictionary:
|
||||
var result = {}
|
||||
if source_line.contains("&"):
|
||||
var first_split = source_line.split("&")
|
||||
for elem in first_split:
|
||||
var second_split = elem.split(":")
|
||||
result[second_split[0]] = {}
|
||||
result[second_split[0]]["amount"] = int(second_split[1])
|
||||
result[second_split[0]]["chance"] = int(second_split[2])
|
||||
|
||||
for elem in source_line.split("&", false):
|
||||
var parts: PackedStringArray = elem.strip_edges().split(":")
|
||||
if parts.size() != 3 or parts[0].strip_edges().is_empty():
|
||||
push_warning("Invalid planet definition for %s: %s" % [id, elem])
|
||||
continue
|
||||
if not parts[1].is_valid_float() or not parts[2].is_valid_float():
|
||||
push_warning("Invalid planet numbers for %s: %s" % [id, elem])
|
||||
continue
|
||||
var count: float = float(parts[1])
|
||||
var chance: float = float(parts[2])
|
||||
if not is_finite(count) or not is_finite(chance) or count < 0.0 or count != floorf(count) or chance < 0.0 or chance > 1.0:
|
||||
push_warning("Planet values out of range for %s: %s" % [id, elem])
|
||||
continue
|
||||
# Planet chances are fractions (0.01 = 1%), unlike output_or percentages.
|
||||
result[parts[0].strip_edges()] = {"amount": count, "chance": chance}
|
||||
return result
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
extends Node
|
||||
|
||||
class_name OwniverseEvolution
|
||||
|
||||
const FLOAT_THRESHOLD: float = 1.0E14
|
||||
# TODO to save !!!
|
||||
var state: Dictionary = {
|
||||
'year': 0.0,
|
||||
'energy': 1E14,
|
||||
}
|
||||
|
||||
var entities: Dictionary[String, OwniverseEntity] = {}
|
||||
|
||||
var producing_entities: Array[OwniverseEntity] = []
|
||||
|
||||
# TODO to save !!!
|
||||
var auto_production_rest: Dictionary = {
|
||||
'H': 0.0,
|
||||
'S': 0.0,
|
||||
'auto': 0.0,
|
||||
}
|
||||
|
||||
var evolving_entities: Array[OwniverseEntity] = []
|
||||
|
||||
# TODO to save !!!
|
||||
var batches: Dictionary[String, Array] = {}
|
||||
# TODO to save !!!
|
||||
var amount: Dictionary[String, float] = {}
|
||||
|
||||
|
||||
var distribution: Dictionary[String, float] = {}
|
||||
|
||||
|
||||
func add(entity: OwniverseEntity, year: float, amount: float):
|
||||
var _batch_array: Array = batches[entity.id]
|
||||
var _from_year: float = year + entity.lifespan - entity.lifespan_delta
|
||||
var _to_year: float = year + entity.lifespan + entity.lifespan_delta
|
||||
if _batch_array.size() != 0 and _batch_array[-1].to_year > year + entity.lifespan:
|
||||
_batch_array[-1].amount += amount
|
||||
else:
|
||||
var _batch := {
|
||||
'from_year': _from_year,
|
||||
'to_year': _to_year,
|
||||
'amount': amount,
|
||||
}
|
||||
_batch_array.append(_batch)
|
||||
|
||||
print('---------')
|
||||
print(batches)
|
||||
|
||||
|
||||
func process(from_year: float, to_year: float) -> void:
|
||||
# производим
|
||||
# эволюционируем
|
||||
# записываем новые
|
||||
var _to_produce := {}
|
||||
var _to_evolute := {}
|
||||
for _entity_id in batches:
|
||||
_to_produce[_entity_id] = 0
|
||||
_to_evolute[_entity_id] = 0
|
||||
var _new_batch_array := []
|
||||
for _batch: Dictionary in batches[_entity_id]:
|
||||
# production
|
||||
_to_produce[_entity_id] += _batch.amount
|
||||
# evolution
|
||||
if _batch.to_year < to_year:
|
||||
_to_produce[_entity_id] += _batch.amount
|
||||
else:
|
||||
var _max_from_year: float = max(from_year, _batch.from_year)
|
||||
var _min_to_year: float = min(to_year, _batch.to_year)
|
||||
var _part_of_amount: float = _batch.amount \
|
||||
* (_min_to_year - _max_from_year) \
|
||||
/ (_batch.to_year - _batch.from_year)
|
||||
_to_evolute[_entity_id] += _part_of_amount
|
||||
var _rest_of_amount: float = _batch.amount - _part_of_amount
|
||||
|
||||
if _rest_of_amount > 0:
|
||||
_new_batch_array.append({
|
||||
'from_year': _batch._from_year,
|
||||
'to_year': _batch._to_year,
|
||||
'amount': _rest_of_amount,
|
||||
})
|
||||
|
||||
# entity level
|
||||
batches[_entity_id] = _new_batch_array
|
||||
|
||||
|
||||
func produce(from_year: float, to_year: float):
|
||||
var _time_delta = to_year - from_year
|
||||
# get entity array for production
|
||||
for _entity: OwniverseEntity in producing_entities:
|
||||
var _to_produce: float = 0.0
|
||||
for _batch: Dictionary in batches[_entity.id]:
|
||||
_to_produce += _batch.amount
|
||||
_to_produce *= _time_delta * _entity.product_per_year_amount
|
||||
if _entity.auto > 0:
|
||||
var _auto_production_part: float = _entity.auto * _to_produce
|
||||
_to_produce -= _auto_production_part
|
||||
auto_production_rest.auto += _auto_production_part
|
||||
|
||||
auto_production_rest[_entity.product_per_year_type] += _to_produce
|
||||
print("auto prod rest: ", auto_production_rest)
|
||||
auto_production_rest["H"] = produce_hs("H", auto_production_rest["H"])
|
||||
auto_production_rest["S"] = produce_hs("S", auto_production_rest["S"])
|
||||
|
||||
|
||||
func produce_hs(entity_id: String, value: float) -> float:
|
||||
var _to_produce: float = floorf(value)
|
||||
amount[entity_id] += _to_produce
|
||||
# TODO double double math
|
||||
if _to_produce > state["energy"]:
|
||||
_to_produce = state["energy"]
|
||||
state["energy"] -= _to_produce
|
||||
return value - _to_produce
|
||||
|
||||
func produce_auto() -> float:
|
||||
return 0.0
|
||||
|
||||
func init_entity(entity: OwniverseEntity) -> void:
|
||||
entities[entity.id] = entity
|
||||
amount[entity.id] = 0.0
|
||||
if entity.product_per_year_type != "":
|
||||
producing_entities.append(entity)
|
||||
|
||||
if entity.lifespan > 0:
|
||||
evolving_entities.append(entity)
|
||||
|
||||
batches[entity.id] = []
|
||||
|
||||
|
||||
func update_distribution(_distribute_by: Dictionary[String, float]) -> void:
|
||||
distribution = {}
|
||||
var _total: float = 0
|
||||
for id in _distribute_by:
|
||||
if _distribute_by[id] != 0:
|
||||
var _value: float = _distribute_by[id]
|
||||
_total += _value
|
||||
distribution[id] = _value
|
||||
|
||||
if _total == 0: return
|
||||
|
||||
for id in distribution:
|
||||
distribution[id] /= _total
|
||||
|
||||
#func _add_float(a: float, b: Array[float]) -> Array[float]:
|
||||
#if a < FLOAT_THRESHOLD:
|
||||
#return [a+b[0], 0]
|
||||
#else:
|
||||
#return [a+b[0], 0]
|
||||
@@ -1 +0,0 @@
|
||||
uid://mgnxt6kbf80n
|
||||
@@ -0,0 +1,13 @@
|
||||
extends RefCounted
|
||||
## datetime is accumulated time; year is the last processed tenth of a year.
|
||||
const STATE_VERSION: String = "0.1.1"
|
||||
|
||||
static func processed_year(datetime: float) -> float:
|
||||
# Remove only sub-nanotick accumulation noise at decimal boundaries.
|
||||
# A relative epsilon would incorrectly advance large ages by whole years.
|
||||
return floorf(snappedf(datetime * 10.0, 1e-9)) / 10.0
|
||||
|
||||
static func elapsed_years(from_year: float, to_year: float) -> float:
|
||||
# Subtract integer tenths before returning to float years. Otherwise 0.3-0.2
|
||||
# can fall below 0.1 and postpone a whole unit of resource production.
|
||||
return (roundf(to_year * 10.0) - roundf(from_year * 10.0)) / 10.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://c463vlrlhvpx8
|
||||
+128
-28
@@ -42,9 +42,25 @@
|
||||
[ext_resource type="Texture2D" uid="uid://bwqmmmg6w7dho" path="res://ui/indicators/trends/trend_icon_4.png" id="39_jms70"]
|
||||
[ext_resource type="Texture2D" uid="uid://4tpbmpe6tbk0" path="res://ui/indicators/trends/trend_bar5.png" id="40_50rdj"]
|
||||
[ext_resource type="Texture2D" uid="uid://cnf3ml5gj2cvl" path="res://ui/indicators/trends/trend_icon_5.png" id="41_mi6gc"]
|
||||
[ext_resource type="Texture2D" uid="uid://drcoll8tlms5s" path="res://main_menu/Owniverse.png" id="43_58wjt"]
|
||||
[ext_resource type="Texture2D" uid="uid://dcpavg1u7gne7" path="res://main_menu/en/CreateUniverse.png" id="43_k83x5"]
|
||||
[ext_resource type="Texture2D" uid="uid://c7osivfr1yusc" path="res://main_menu/en/Tutorial.png" id="44_b8tmp"]
|
||||
[ext_resource type="Texture2D" uid="uid://cq3ho84qxa2wj" path="res://main_menu/en/Achievements.png" id="45_vu62d"]
|
||||
[ext_resource type="Texture2D" uid="uid://clifqlkiqlp7s" path="res://main_menu/en/Russian.png" id="46_58wjt"]
|
||||
[ext_resource type="Texture2D" uid="uid://bp7fwgt6kwnwa" path="res://main_menu/ru/English.png" id="47_bfrib"]
|
||||
[ext_resource type="Texture2D" uid="uid://c15y3813cpwtd" path="res://main_menu/en/SoundNonActive.png" id="47_dp8ul"]
|
||||
[ext_resource type="Texture2D" uid="uid://cj0qbxoddsnjv" path="res://main_menu/en/EnglishActive.png" id="48_mip0j"]
|
||||
[ext_resource type="Texture2D" uid="uid://cbpds0flysd4i" path="res://main_menu/en/SoundActive.png" id="48_o7xkw"]
|
||||
[ext_resource type="Texture2D" uid="uid://dxh4l2ovvmrk1" path="res://main_menu/en/MusicNonActive.png" id="49_bfrib"]
|
||||
[ext_resource type="Texture2D" uid="uid://so06vth6le3b" path="res://main_menu/ru/RussianActive.png" id="50_l6135"]
|
||||
[ext_resource type="Texture2D" uid="uid://bamh6ko30ljb7" path="res://main_menu/en/MusicActive.png" id="50_mip0j"]
|
||||
[ext_resource type="Texture2D" uid="uid://bhfva6xtyij4k" path="res://main_menu/en/Credits.png" id="51_l6135"]
|
||||
[ext_resource type="Texture2D" uid="uid://dwqpcm1pryw44" path="res://main_menu/en/LeaveReality.png" id="52_uvuuk"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ifjjt"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_58wjt"]
|
||||
|
||||
[node name="World" type="Node2D" unique_id=1392592456]
|
||||
|
||||
[node name="Background" type="Node2D" parent="." unique_id=1988758765]
|
||||
@@ -227,11 +243,11 @@ theme_override_fonts/normal_font = ExtResource("14_o7xkw")
|
||||
theme_override_fonts/bold_font = ExtResource("7_gwdna")
|
||||
theme_override_fonts/bold_italics_font = ExtResource("15_bfrib")
|
||||
theme_override_fonts/italics_font = ExtResource("16_mip0j")
|
||||
theme_override_font_sizes/normal_font_size = 42
|
||||
theme_override_font_sizes/bold_font_size = 54
|
||||
theme_override_font_sizes/bold_italics_font_size = 42
|
||||
theme_override_font_sizes/italics_font_size = 42
|
||||
theme_override_font_sizes/mono_font_size = 42
|
||||
theme_override_font_sizes/normal_font_size = 34
|
||||
theme_override_font_sizes/bold_font_size = 34
|
||||
theme_override_font_sizes/bold_italics_font_size = 34
|
||||
theme_override_font_sizes/italics_font_size = 34
|
||||
theme_override_font_sizes/mono_font_size = 34
|
||||
bbcode_enabled = true
|
||||
horizontal_alignment = 1
|
||||
|
||||
@@ -370,11 +386,11 @@ theme_override_fonts/normal_font = ExtResource("14_o7xkw")
|
||||
theme_override_fonts/bold_font = ExtResource("7_gwdna")
|
||||
theme_override_fonts/bold_italics_font = ExtResource("15_bfrib")
|
||||
theme_override_fonts/italics_font = ExtResource("16_mip0j")
|
||||
theme_override_font_sizes/normal_font_size = 42
|
||||
theme_override_font_sizes/bold_font_size = 54
|
||||
theme_override_font_sizes/bold_italics_font_size = 42
|
||||
theme_override_font_sizes/italics_font_size = 42
|
||||
theme_override_font_sizes/mono_font_size = 42
|
||||
theme_override_font_sizes/normal_font_size = 34
|
||||
theme_override_font_sizes/bold_font_size = 34
|
||||
theme_override_font_sizes/bold_italics_font_size = 34
|
||||
theme_override_font_sizes/italics_font_size = 34
|
||||
theme_override_font_sizes/mono_font_size = 34
|
||||
bbcode_enabled = true
|
||||
horizontal_alignment = 1
|
||||
|
||||
@@ -498,11 +514,11 @@ theme_override_fonts/normal_font = ExtResource("14_o7xkw")
|
||||
theme_override_fonts/bold_font = ExtResource("7_gwdna")
|
||||
theme_override_fonts/bold_italics_font = ExtResource("15_bfrib")
|
||||
theme_override_fonts/italics_font = ExtResource("16_mip0j")
|
||||
theme_override_font_sizes/normal_font_size = 42
|
||||
theme_override_font_sizes/bold_font_size = 54
|
||||
theme_override_font_sizes/bold_italics_font_size = 42
|
||||
theme_override_font_sizes/italics_font_size = 42
|
||||
theme_override_font_sizes/mono_font_size = 42
|
||||
theme_override_font_sizes/normal_font_size = 34
|
||||
theme_override_font_sizes/bold_font_size = 34
|
||||
theme_override_font_sizes/bold_italics_font_size = 34
|
||||
theme_override_font_sizes/italics_font_size = 34
|
||||
theme_override_font_sizes/mono_font_size = 34
|
||||
bbcode_enabled = true
|
||||
text = "Stars"
|
||||
horizontal_alignment = 1
|
||||
@@ -639,11 +655,11 @@ theme_override_fonts/normal_font = ExtResource("14_o7xkw")
|
||||
theme_override_fonts/bold_font = ExtResource("7_gwdna")
|
||||
theme_override_fonts/bold_italics_font = ExtResource("15_bfrib")
|
||||
theme_override_fonts/italics_font = ExtResource("16_mip0j")
|
||||
theme_override_font_sizes/normal_font_size = 42
|
||||
theme_override_font_sizes/bold_font_size = 54
|
||||
theme_override_font_sizes/bold_italics_font_size = 42
|
||||
theme_override_font_sizes/italics_font_size = 42
|
||||
theme_override_font_sizes/mono_font_size = 42
|
||||
theme_override_font_sizes/normal_font_size = 34
|
||||
theme_override_font_sizes/bold_font_size = 34
|
||||
theme_override_font_sizes/bold_italics_font_size = 34
|
||||
theme_override_font_sizes/italics_font_size = 34
|
||||
theme_override_font_sizes/mono_font_size = 34
|
||||
bbcode_enabled = true
|
||||
text = "Structures"
|
||||
horizontal_alignment = 1
|
||||
@@ -787,11 +803,11 @@ theme_override_fonts/normal_font = ExtResource("14_o7xkw")
|
||||
theme_override_fonts/bold_font = ExtResource("7_gwdna")
|
||||
theme_override_fonts/bold_italics_font = ExtResource("15_bfrib")
|
||||
theme_override_fonts/italics_font = ExtResource("16_mip0j")
|
||||
theme_override_font_sizes/normal_font_size = 42
|
||||
theme_override_font_sizes/bold_font_size = 54
|
||||
theme_override_font_sizes/bold_italics_font_size = 42
|
||||
theme_override_font_sizes/italics_font_size = 42
|
||||
theme_override_font_sizes/mono_font_size = 42
|
||||
theme_override_font_sizes/normal_font_size = 34
|
||||
theme_override_font_sizes/bold_font_size = 34
|
||||
theme_override_font_sizes/bold_italics_font_size = 34
|
||||
theme_override_font_sizes/italics_font_size = 34
|
||||
theme_override_font_sizes/mono_font_size = 34
|
||||
bbcode_enabled = true
|
||||
text = "Planets"
|
||||
horizontal_alignment = 1
|
||||
@@ -868,12 +884,96 @@ theme_override_styles/panel = SubResource("StyleBoxEmpty_ifjjt")
|
||||
[node name="Border" parent="Stage/StageRows" unique_id=145801370 instance=ExtResource("12_58wjt")]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="MainMenu" type="VBoxContainer" parent="Stage" unique_id=731440017]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
theme_override_constants/separation = 32
|
||||
|
||||
[node name="TopMenu" type="HBoxContainer" parent="Stage/MainMenu" unique_id=252548655]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="BurgerButton" type="TextureButton" parent="Stage/MainMenu/TopMenu" unique_id=1685450387]
|
||||
custom_minimum_size = Vector2(128, 128)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 4
|
||||
texture_normal = ExtResource("4_b8tmp")
|
||||
texture_pressed = ExtResource("5_en1oy")
|
||||
|
||||
[node name="Logo" type="TextureRect" parent="Stage/MainMenu" unique_id=1181845167]
|
||||
custom_minimum_size = Vector2(1080, 256)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("43_58wjt")
|
||||
|
||||
[node name="CreateOwniverse" type="TextureButton" parent="Stage/MainMenu" unique_id=1568939047]
|
||||
custom_minimum_size = Vector2(1024, 128)
|
||||
layout_mode = 2
|
||||
texture_normal = ExtResource("43_k83x5")
|
||||
|
||||
[node name="Tutorial" type="TextureButton" parent="Stage/MainMenu" unique_id=663512289]
|
||||
custom_minimum_size = Vector2(1024, 128)
|
||||
layout_mode = 2
|
||||
texture_normal = ExtResource("44_b8tmp")
|
||||
|
||||
[node name="Achievements" type="TextureButton" parent="Stage/MainMenu" unique_id=141361251]
|
||||
custom_minimum_size = Vector2(1024, 128)
|
||||
layout_mode = 2
|
||||
texture_normal = ExtResource("45_vu62d")
|
||||
|
||||
[node name="Language" type="HBoxContainer" parent="Stage/MainMenu" unique_id=2100638475]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="English" type="TextureButton" parent="Stage/MainMenu/Language" unique_id=1495557975]
|
||||
custom_minimum_size = Vector2(512, 128)
|
||||
layout_mode = 2
|
||||
toggle_mode = true
|
||||
button_group = SubResource("ButtonGroup_58wjt")
|
||||
texture_normal = ExtResource("47_bfrib")
|
||||
texture_pressed = ExtResource("48_mip0j")
|
||||
|
||||
[node name="Russian" type="TextureButton" parent="Stage/MainMenu/Language" unique_id=2024992008]
|
||||
custom_minimum_size = Vector2(512, 128)
|
||||
layout_mode = 2
|
||||
toggle_mode = true
|
||||
button_group = SubResource("ButtonGroup_58wjt")
|
||||
texture_normal = ExtResource("46_58wjt")
|
||||
texture_pressed = ExtResource("50_l6135")
|
||||
|
||||
[node name="Media" type="HBoxContainer" parent="Stage/MainMenu" unique_id=583896501]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Sound" type="TextureButton" parent="Stage/MainMenu/Media" unique_id=116977198]
|
||||
custom_minimum_size = Vector2(512, 128)
|
||||
layout_mode = 2
|
||||
toggle_mode = true
|
||||
texture_normal = ExtResource("47_dp8ul")
|
||||
texture_pressed = ExtResource("48_o7xkw")
|
||||
|
||||
[node name="Music" type="TextureButton" parent="Stage/MainMenu/Media" unique_id=953224171]
|
||||
custom_minimum_size = Vector2(512, 128)
|
||||
layout_mode = 2
|
||||
toggle_mode = true
|
||||
texture_normal = ExtResource("49_bfrib")
|
||||
texture_pressed = ExtResource("50_mip0j")
|
||||
|
||||
[node name="Credits" type="TextureButton" parent="Stage/MainMenu" unique_id=44726060]
|
||||
custom_minimum_size = Vector2(1024, 128)
|
||||
layout_mode = 2
|
||||
texture_normal = ExtResource("51_l6135")
|
||||
|
||||
[node name="LeaveReality" type="TextureButton" parent="Stage/MainMenu" unique_id=2139602731]
|
||||
custom_minimum_size = Vector2(1024, 128)
|
||||
layout_mode = 2
|
||||
texture_normal = ExtResource("52_uvuuk")
|
||||
|
||||
[node name="ItemAmountUpdate" type="Timer" parent="Stage" unique_id=1953928024]
|
||||
wait_time = 0.2
|
||||
autostart = true
|
||||
|
||||
[node name="SaveData" type="Timer" parent="Stage" unique_id=1208861837]
|
||||
wait_time = 5.0
|
||||
[node name="Unlock&Save" type="Timer" parent="Stage" unique_id=1208861837]
|
||||
wait_time = 2.0
|
||||
autostart = true
|
||||
|
||||
[connection signal="timeout" from="Background/Timer" to="Background" method="randomize_parallax"]
|
||||
@@ -890,4 +990,4 @@ autostart = true
|
||||
[connection signal="pressed" from="Stage/StageRows/PageScroller/HBoxContainer/LifePage/LifeHeader/AggressionIndicator" to="Stage" method="_on_indicator_pressed" binds= ["Aggression", 4]]
|
||||
[connection signal="pressed" from="Stage/StageRows/PageScroller/HBoxContainer/LifePage/LifeHeader/TrendIndicator" to="Stage" method="_on_indicator_pressed" binds= ["Trends", 4]]
|
||||
[connection signal="timeout" from="Stage/ItemAmountUpdate" to="Stage" method="set_owniverse_item_value"]
|
||||
[connection signal="timeout" from="Stage/SaveData" to="Stage" method="_on_save_data"]
|
||||
[connection signal="timeout" from="Stage/Unlock&Save" to="Stage" method="_on_unlock_and_save_timeout"]
|
||||
|
||||
Reference in New Issue
Block a user