Compare commits
64 Commits
v0.6.14
...
f2111a376a
| Author | SHA1 | Date | |
|---|---|---|---|
| f2111a376a | |||
| da03ba11c0 | |||
| c2313e9163 | |||
| 2f0a3ee8d4 | |||
| 15ee7833ed | |||
| e79f3fa362 | |||
| e87d4083e9 | |||
| 5a18b41528 | |||
| d6fd40969f | |||
| a3d24114a1 | |||
| 8867da569a | |||
| 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 | |||
| 3a91528e95 | |||
| 3c37be2a8e | |||
| 544b5f4f56 | |||
| cc9de54d42 | |||
| e76414ce17 | |||
| 68c2b4f25d | |||
| 78946f1bb4 | |||
| 0d1671fe8f | |||
| ea35017aa0 | |||
| 0db24fec60 | |||
| b2a18c9c41 | |||
| 16ec80929c | |||
| 3536e25d52 | |||
| 935d249183 | |||
| 8cb01d77de | |||
| a446b7cb41 | |||
| 82681d27e1 | |||
| 777ec2907b | |||
| 8513bf0313 | |||
| a4019ed98c | |||
| 4731489015 | |||
| 711ace6678 | |||
| 89342dc614 |
@@ -1,13 +1,19 @@
|
|||||||
---
|
---
|
||||||
name: Godot 4.4 Core Rules
|
name: Godot 4.7 Core Rules
|
||||||
priority: high
|
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]` и т.д.).
|
- Пиши строго типизированный код (`var health: int = 100`, `: Array[Node]` и т.д.).
|
||||||
- Предпочитай Signals вместо проверки в _process.
|
- Предпочитай Signals вместо проверки в _process.
|
||||||
- Используй @export, @export_range, @export_enum, @onready.
|
- Используй @export, @export_range, @export_enum, @onready.
|
||||||
- Composition over deep inheritance.
|
- Composition over deep inheritance.
|
||||||
- Менеджеры и автолоады выноси в отдельные singleton'ы.
|
- Менеджеры и автолоады выноси в отдельные singleton'ы.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
- use spaces not tabs for indentation
|
||||||
@@ -12,20 +12,52 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
- name: Godot version
|
- name: Godot version
|
||||||
run: godot --version
|
run: godot --version
|
||||||
|
|
||||||
|
- name: Prepare Godot
|
||||||
|
run: |
|
||||||
|
rm -rf .godot
|
||||||
|
|
||||||
|
godot \
|
||||||
|
--headless \
|
||||||
|
--path . \
|
||||||
|
--editor \
|
||||||
|
--import \
|
||||||
|
--quit \
|
||||||
|
--verbose
|
||||||
|
|
||||||
|
|
||||||
- name: Build APK
|
- name: Build APK
|
||||||
run: |
|
run: |
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
|
|
||||||
|
godot \
|
||||||
|
--headless \
|
||||||
|
--path . \
|
||||||
|
--editor \
|
||||||
|
--import \
|
||||||
|
--quit
|
||||||
|
|
||||||
godot \
|
godot \
|
||||||
--headless \
|
--headless \
|
||||||
--path . \
|
--path . \
|
||||||
--export-release "Android" \
|
--export-release "Android" \
|
||||||
build/app.apk
|
build/app.apk
|
||||||
|
|
||||||
|
|
||||||
|
- name: Check imported resources
|
||||||
|
run: |
|
||||||
|
echo "=== PNG imports ==="
|
||||||
|
find .godot/imported -iname '*habitable*' -o -iname '*civ2*'
|
||||||
|
|
||||||
|
echo "=== PNG files ==="
|
||||||
|
ls -lh ui/4-life/habitable_planet.png
|
||||||
|
ls -lh ui/4-life/civ2.png
|
||||||
|
|
||||||
|
|
||||||
- name: Verify APK
|
- name: Verify APK
|
||||||
run: |
|
run: |
|
||||||
test -f build/app.apk
|
test -f build/app.apk
|
||||||
@@ -59,4 +91,4 @@ jobs:
|
|||||||
-X POST \
|
-X POST \
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
-F "attachment=@build/app.apk" \
|
-F "attachment=@build/app.apk" \
|
||||||
"https://gt.owad.ru/api/v1/repos/K/owniverse/releases/${RELEASE_ID}/assets?name=owniverse-${GITHUB_REF_NAME}.apk"
|
"https://gt.owad.ru/api/v1/repos/K/owniverse/releases/${RELEASE_ID}/assets?name=owniverse-${GITHUB_REF_NAME}.apk"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
/android/
|
/android/
|
||||||
*.import
|
*.import
|
||||||
|
!data/*.txt.import
|
||||||
|
|
||||||
# MacOS specific ignores
|
# MacOS specific ignores
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
extends Node
|
|
||||||
|
|
||||||
const SPEED_LIMIT: int = 4
|
|
||||||
|
|
||||||
var Settings: Dictionary = {
|
|
||||||
"filename": "save-0001",
|
|
||||||
"sound_volume": 1.0,
|
|
||||||
"language": "en",
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO getter setter for locale
|
|
||||||
|
|
||||||
var Locales: Dictionary = {}
|
|
||||||
var Locale = "en"
|
|
||||||
|
|
||||||
var OwniverseEntities: Dictionary = {}
|
|
||||||
var OwniverseItems: Dictionary = {}
|
|
||||||
var Templates: Dictionary = {}
|
|
||||||
|
|
||||||
var batch_count: float = 40.0 # must be even not odd
|
|
||||||
|
|
||||||
|
|
||||||
func get_localized_item_description(item_id: String, item: String) -> String:
|
|
||||||
if Locales[Locale].has(item_id):
|
|
||||||
return Locales[Locale][item_id][item]
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
func format_number(value: float, digits: int = 0) -> String:
|
|
||||||
if value == 0: return ""
|
|
||||||
if value < 10_000:
|
|
||||||
return "%.*f" % [digits, value]
|
|
||||||
|
|
||||||
var exp_value = int(floor(log(abs(value)) / log(10.0)))
|
|
||||||
var mantissa = value / pow(10.0, exp_value)
|
|
||||||
|
|
||||||
return "%.*f" % [digits, mantissa] + "E%+d" % exp_value
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
OwniverseEntities = _read_from_csv("res://data/owniverse_entitites.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")
|
|
||||||
|
|
||||||
|
|
||||||
func _read_from_csv(file_path: String) -> Dictionary:
|
|
||||||
if not FileAccess.file_exists(file_path):
|
|
||||||
printerr("File not found: " + file_path)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
var file = FileAccess.open(file_path, FileAccess.READ)
|
|
||||||
if file == null:
|
|
||||||
printerr("File can not be opened:" + file_path)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
var headers: PackedStringArray = []
|
|
||||||
var data: Array = []
|
|
||||||
|
|
||||||
if not file.eof_reached():
|
|
||||||
headers = PackedStringArray()
|
|
||||||
for val in file.get_csv_line("|"):
|
|
||||||
headers.append(val.strip_edges())
|
|
||||||
|
|
||||||
while not file.eof_reached():
|
|
||||||
var raw_row = file.get_csv_line("|")
|
|
||||||
if raw_row.is_empty(): # пропускаем пустые строки
|
|
||||||
continue
|
|
||||||
|
|
||||||
var trimmed_row: PackedStringArray = []
|
|
||||||
for val in raw_row:
|
|
||||||
trimmed_row.append(val.strip_edges())
|
|
||||||
|
|
||||||
data.append(trimmed_row)
|
|
||||||
|
|
||||||
file.close()
|
|
||||||
|
|
||||||
var dict_data: Dictionary = {}
|
|
||||||
for row in data:
|
|
||||||
if row.size() < 2: continue
|
|
||||||
|
|
||||||
if row.size() == headers.size():
|
|
||||||
var dict: Dictionary = {}
|
|
||||||
for i in headers.size():
|
|
||||||
dict[headers[i]] = row[i]
|
|
||||||
dict_data[row[0]] = dict
|
|
||||||
|
|
||||||
return dict_data
|
|
||||||
|
|
||||||
|
|
||||||
func _read_folder_list(folder_name: String) -> void:
|
|
||||||
var dir := DirAccess.open(folder_name)
|
|
||||||
if dir == null: printerr("Could not open folder: ", folder_name); return
|
|
||||||
dir.list_dir_begin()
|
|
||||||
for file: String in dir.get_files():
|
|
||||||
print(" → ", file)
|
|
||||||
|
|
||||||
|
|
||||||
func _print_dict(dict_to_print: Dictionary):
|
|
||||||
for d in dict_to_print:
|
|
||||||
print(d, " → ", dict_to_print[d])
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# SaveManager.gd (добавить в AutoLoad)
|
|
||||||
extends Node
|
|
||||||
|
|
||||||
# ~/Library/Application Support/Godot/app_userdata/Owniverse Adventure/savegame.cfg
|
|
||||||
const SETTING_SECTION := "Settings"
|
|
||||||
const SAVE_PATH = "user://settings.cfg"
|
|
||||||
|
|
||||||
#region Save Load routines
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
if file == null:
|
|
||||||
push_error("Can't write to the file: ", filename)
|
|
||||||
return
|
|
||||||
|
|
||||||
file.store_var(data) # бинарно + быстрее + сериализация
|
|
||||||
file.close()
|
|
||||||
|
|
||||||
|
|
||||||
func load_data(filename: String, password: String) -> Dictionary:
|
|
||||||
filename = "user://" + filename + ".adventure"
|
|
||||||
if not FileAccess.file_exists(filename):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
var file := FileAccess.open_encrypted_with_pass(filename, FileAccess.READ, password)
|
|
||||||
if file == null:
|
|
||||||
push_error(filename, " couldn't be decrypted.")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
var data: Variant = file.get_var()
|
|
||||||
file.close()
|
|
||||||
|
|
||||||
return data if data is Dictionary else {}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Settings
|
|
||||||
func save_settings():
|
|
||||||
var config = ConfigFile.new()
|
|
||||||
|
|
||||||
for item in Global.Settings:
|
|
||||||
config.set_value(SETTING_SECTION, item, Global.Settings[item])
|
|
||||||
config.save(SAVE_PATH)
|
|
||||||
|
|
||||||
func load_settings():
|
|
||||||
var config = ConfigFile.new()
|
|
||||||
var err = config.load(SAVE_PATH)
|
|
||||||
if err != OK:
|
|
||||||
return false
|
|
||||||
for item in Global.Settings:
|
|
||||||
Global.Settings[item] = config.get_value(SETTING_SECTION, item, Global.Settings[item])
|
|
||||||
return true
|
|
||||||
#endregion
|
|
||||||
-306
@@ -1,306 +0,0 @@
|
|||||||
extends Control
|
|
||||||
|
|
||||||
const OWNIVERSE_ITEM = preload("uid://d3xjfxwe3w16v")
|
|
||||||
|
|
||||||
const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png"
|
|
||||||
|
|
||||||
const PAGE_COUNT = 5
|
|
||||||
const PAGE_WIDTH = 1080
|
|
||||||
const DRAG_TOLERANCE = 360
|
|
||||||
const SWIPE_DURATION: float = 0.4
|
|
||||||
|
|
||||||
@onready var stage_page_scroller: ScrollContainer = $StageRows/PageScroller
|
|
||||||
|
|
||||||
@onready var item_pages: Array[Control] = [
|
|
||||||
$StageRows/PageScroller/HBoxContainer/ForcePage/Forces,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/ResourcePage/HBoxContainer/Resources,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/StarPage/Stars,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/StructurePage/Structures,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Life,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/LivingPlanet,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ0,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ1,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ2,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ3,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ4
|
|
||||||
]
|
|
||||||
|
|
||||||
@onready var page_actions: Dictionary[int, OwniverseAction] = {
|
|
||||||
2: $StageRows/PageScroller/HBoxContainer/StarPage/Actions,
|
|
||||||
3: $StageRows/PageScroller/HBoxContainer/StructurePage/Actions,
|
|
||||||
}
|
|
||||||
|
|
||||||
@onready var g_item_descriptions: Array = [
|
|
||||||
$StageRows/PageScroller/HBoxContainer/ForcePage/Description,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/ResourcePage/Description,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/StarPage/Description,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/StructurePage/Description,
|
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Description
|
|
||||||
]
|
|
||||||
|
|
||||||
@onready var date_time: Label = $StageRows/TopMenu/InfoPanel/DateTime
|
|
||||||
|
|
||||||
@onready var g_speed_icons: Array = [
|
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed1,
|
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed2,
|
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed3,
|
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed4,
|
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed5,
|
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed6,
|
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed7,
|
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed8
|
|
||||||
]
|
|
||||||
|
|
||||||
@onready var g_time_minus: TextureButton = $StageRows/TopMenu/TimeMinus
|
|
||||||
@onready var g_time_plus: TextureButton = $StageRows/TopMenu/TimePlus
|
|
||||||
|
|
||||||
|
|
||||||
var g_tween: Tween = null
|
|
||||||
var g_dragged_start: Vector2 = Vector2.ZERO
|
|
||||||
var g_scroll_start: int = 0
|
|
||||||
#var g_current_page: int = 0
|
|
||||||
|
|
||||||
var g_last_scroll_updated: int = 0
|
|
||||||
|
|
||||||
var f_check_swipe: bool = false
|
|
||||||
var g_swipe_time_delta: float = 0
|
|
||||||
|
|
||||||
var g_counters: Dictionary[String, Counter] = {
|
|
||||||
"swipe": Counter.new(),
|
|
||||||
}
|
|
||||||
|
|
||||||
var g_active_items: Dictionary = {}
|
|
||||||
|
|
||||||
var owniverse:Owniverse = Owniverse.new()
|
|
||||||
var owniverse_items: Dictionary[String, OwniverseItem] = {}
|
|
||||||
|
|
||||||
var item_list_to_update: Dictionary = {}
|
|
||||||
|
|
||||||
func set_datetime(value: float):
|
|
||||||
date_time.text = str(snapped(value, 0.1))
|
|
||||||
|
|
||||||
#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 = {}
|
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
func _control_page(item: OwniverseItem) -> void:
|
|
||||||
# TODO переписать это ↓
|
|
||||||
if !g_active_items.has(item.item_page):
|
|
||||||
g_active_items[item.item_page] = null
|
|
||||||
|
|
||||||
var current_item: OwniverseItem = g_active_items[item.item_page]
|
|
||||||
if current_item == item:
|
|
||||||
return
|
|
||||||
|
|
||||||
g_active_items[item.item_page] = item
|
|
||||||
|
|
||||||
if current_item != null and current_item.item_page == item.item_page:
|
|
||||||
current_item.set_selection(false)
|
|
||||||
|
|
||||||
item.set_selection(true)
|
|
||||||
|
|
||||||
# subpages
|
|
||||||
if item.item_sub_page != -1:
|
|
||||||
item_pages[item.item_sub_page].visible = true
|
|
||||||
|
|
||||||
if current_item == null \
|
|
||||||
or current_item.item_sub_page == -1 \
|
|
||||||
or current_item.item_sub_page == item.item_sub_page:
|
|
||||||
return
|
|
||||||
item_pages[current_item.item_sub_page].visible = false
|
|
||||||
|
|
||||||
|
|
||||||
func _init_owniverse_items() -> bool:
|
|
||||||
owniverse_items = {}
|
|
||||||
for item_id in Global.OwniverseItems:
|
|
||||||
var item_props: Dictionary = Global.OwniverseItems[item_id]
|
|
||||||
var item: OwniverseItem = OWNIVERSE_ITEM.instantiate()
|
|
||||||
if !item_props.has("page"):
|
|
||||||
printerr("Item `", item_id, "` has no `page` key.")
|
|
||||||
return false
|
|
||||||
var page: int = int(item_props.page)
|
|
||||||
item_pages[page].add_child(item)
|
|
||||||
item.init_button(item_props)
|
|
||||||
item.item_activated.connect(item_activated)
|
|
||||||
|
|
||||||
owniverse_items[item_id] = item
|
|
||||||
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
func item_activated(item: OwniverseItem) -> void:
|
|
||||||
_control_page(item)
|
|
||||||
_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)
|
|
||||||
|
|
||||||
|
|
||||||
func _check_counters(delta: float) -> Array:
|
|
||||||
var actions: Array = []
|
|
||||||
for k in g_counters:
|
|
||||||
var cnt: Counter = g_counters[k]
|
|
||||||
if not cnt.is_active:
|
|
||||||
continue
|
|
||||||
|
|
||||||
cnt.value += delta
|
|
||||||
if cnt.value >= cnt.sec:
|
|
||||||
actions.append(k)
|
|
||||||
cnt.value = 0
|
|
||||||
cnt.is_active = false
|
|
||||||
|
|
||||||
return actions
|
|
||||||
|
|
||||||
|
|
||||||
#region Swipe
|
|
||||||
|
|
||||||
func _do_actions(actions: Array) -> void:
|
|
||||||
for action: String in actions:
|
|
||||||
match action:
|
|
||||||
"swipe":
|
|
||||||
_swipe_to_nearest_page()
|
|
||||||
|
|
||||||
|
|
||||||
func _swipe_to_nearest_page() -> void:
|
|
||||||
if stage_page_scroller.scroll_horizontal % PAGE_WIDTH == 0: return
|
|
||||||
|
|
||||||
var new_page: int = round(1.0 * stage_page_scroller.scroll_horizontal / PAGE_WIDTH)
|
|
||||||
|
|
||||||
if g_tween != null:
|
|
||||||
if g_tween.is_running():
|
|
||||||
return
|
|
||||||
|
|
||||||
g_tween = stage_page_scroller.get_tree().create_tween()
|
|
||||||
g_tween.tween_property(stage_page_scroller, "scroll_horizontal",
|
|
||||||
new_page * PAGE_WIDTH, SWIPE_DURATION).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
|
||||||
if event is InputEventScreenDrag:
|
|
||||||
stage_page_scroller.scroll_horizontal = clamp(
|
|
||||||
stage_page_scroller.scroll_horizontal - PAGE_COUNT * event.screen_relative.x,
|
|
||||||
0, PAGE_WIDTH * (PAGE_COUNT - 1))
|
|
||||||
g_counters.swipe.is_active = true
|
|
||||||
g_counters.swipe.value = 0
|
|
||||||
elif event is InputEventPanGesture:
|
|
||||||
g_counters.swipe.is_active = true
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func _page_action_pressed(page_id: int, action_amount: float) -> void:
|
|
||||||
if !g_active_items.has(page_id):
|
|
||||||
return
|
|
||||||
|
|
||||||
var item_id: String = g_active_items[page_id].item_id
|
|
||||||
|
|
||||||
if action_amount > 0:
|
|
||||||
owniverse.produce(item_id, action_amount)
|
|
||||||
|
|
||||||
|
|
||||||
func _change_speed(delta: int) -> void:
|
|
||||||
var speed: int = owniverse.change_speed(delta)
|
|
||||||
var ind: int = 1
|
|
||||||
for icon in g_speed_icons:
|
|
||||||
if ind <= speed:
|
|
||||||
icon.visible = true
|
|
||||||
else:
|
|
||||||
icon.visible = false
|
|
||||||
ind += 1
|
|
||||||
if speed == 1:
|
|
||||||
g_time_minus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % "x")
|
|
||||||
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % "2")
|
|
||||||
elif speed == Global.SPEED_LIMIT:
|
|
||||||
g_time_minus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(Global.SPEED_LIMIT - 1))
|
|
||||||
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % "x")
|
|
||||||
else:
|
|
||||||
g_time_minus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed - 1))
|
|
||||||
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed + 1))
|
|
||||||
|
|
||||||
|
|
||||||
func _on_topmenu_button_pressed(ButtonName) -> void:
|
|
||||||
print(ButtonName)
|
|
||||||
#var s: float = 1E15
|
|
||||||
#var z = 1.0
|
|
||||||
#for i in range(30):
|
|
||||||
#print(i, " | ", z, " | ", " | ", s - z)
|
|
||||||
#z *= 10
|
|
||||||
|
|
||||||
|
|
||||||
func _on_save_data() -> void:
|
|
||||||
owniverse.save_data()
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[gd_scene format=3 uid="uid://dg15dpxiepicb"]
|
|
||||||
|
|
||||||
[ext_resource type="Texture2D" uid="uid://chxxghyul1im7" path="res://ui/border/border-2px.png" id="1_6kaf8"]
|
|
||||||
|
|
||||||
[node name="Border" type="TextureRect"]
|
|
||||||
texture = ExtResource("1_6kaf8")
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
extends HBoxContainer
|
|
||||||
class_name OwniverseAction
|
|
||||||
|
|
||||||
signal action_pressed(page_id, amount: float)
|
|
||||||
|
|
||||||
@onready var action_labels: Array = [
|
|
||||||
$ActionControl0/Label,
|
|
||||||
$ActionControl1/Label,
|
|
||||||
$ActionControl2/Label,
|
|
||||||
$ActionControl3/Label,
|
|
||||||
$ActionControl4/Label,
|
|
||||||
]
|
|
||||||
|
|
||||||
var page_id: int = -1
|
|
||||||
var current_item_id = ""
|
|
||||||
|
|
||||||
var action_values: Dictionary = {
|
|
||||||
0: 0,
|
|
||||||
1: 0,
|
|
||||||
2: 0,
|
|
||||||
3: 0,
|
|
||||||
4: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func set_actions(item_id: String, value: float) -> void:
|
|
||||||
current_item_id = item_id
|
|
||||||
_set_action_values(_get_distribution(value))
|
|
||||||
|
|
||||||
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))
|
|
||||||
return distribution
|
|
||||||
|
|
||||||
func _set_action_values(values: 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 ""
|
|
||||||
|
|
||||||
|
|
||||||
func _on_action_button_pressed(button_id: int) -> void:
|
|
||||||
action_pressed.emit(page_id, action_values[button_id])
|
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
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",
|
||||||
|
"sound_volume": 1.0,
|
||||||
|
"language": "en",
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO getter setter for locale
|
||||||
|
|
||||||
|
var Locales: Dictionary = {}
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
func get_localized_item_description(item_id: String, item: String) -> String:
|
||||||
|
if Locales[Locale].has(item_id):
|
||||||
|
return Locales[Locale][item_id][item]
|
||||||
|
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:
|
||||||
|
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 ""
|
||||||
|
# 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_prefix: String = ""
|
||||||
|
if integer.begins_with("-") or integer.begins_with("+"):
|
||||||
|
sign_prefix = 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_prefix + 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)
|
||||||
|
|
||||||
|
return "%.*f" % [digits, mantissa] + "E%+d" % exp_value
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
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:
|
||||||
|
if not FileAccess.file_exists(file_path):
|
||||||
|
printerr("File not found: " + file_path)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
var file = FileAccess.open(file_path, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
printerr("File can not be opened:" + file_path)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
var headers: PackedStringArray = []
|
||||||
|
var data: Array = []
|
||||||
|
|
||||||
|
if not file.eof_reached():
|
||||||
|
headers = PackedStringArray()
|
||||||
|
for val in file.get_csv_line("|"):
|
||||||
|
headers.append(val.strip_edges())
|
||||||
|
|
||||||
|
while not file.eof_reached():
|
||||||
|
var raw_row = file.get_csv_line("|")
|
||||||
|
if raw_row.is_empty(): # пропускаем пустые строки
|
||||||
|
continue
|
||||||
|
|
||||||
|
var trimmed_row: PackedStringArray = []
|
||||||
|
for val in raw_row:
|
||||||
|
trimmed_row.append(val.strip_edges())
|
||||||
|
|
||||||
|
data.append(trimmed_row)
|
||||||
|
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
var dict_data: Dictionary = {}
|
||||||
|
for row in data:
|
||||||
|
if row.size() < 2: continue
|
||||||
|
|
||||||
|
if row.size() == headers.size():
|
||||||
|
var dict: Dictionary = {}
|
||||||
|
for i in headers.size():
|
||||||
|
dict[headers[i]] = row[i]
|
||||||
|
dict_data[row[0]] = dict
|
||||||
|
|
||||||
|
return dict_data
|
||||||
|
|
||||||
|
|
||||||
|
func _read_folder_list(folder_name: String) -> void:
|
||||||
|
var dir := DirAccess.open(folder_name)
|
||||||
|
if dir == null: printerr("Could not open folder: ", folder_name); return
|
||||||
|
dir.list_dir_begin()
|
||||||
|
for file: String in dir.get_files():
|
||||||
|
print(" → ", file)
|
||||||
|
|
||||||
|
|
||||||
|
func _print_dict(dict_to_print: Dictionary):
|
||||||
|
for d in dict_to_print:
|
||||||
|
print(d, " → ", dict_to_print[d])
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
# SaveManager.gd (добавить в AutoLoad)
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# ~/Library/Application Support/Godot/app_userdata/Owniverse Adventure/savegame.cfg
|
||||||
|
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 WorldParameters = preload("res://world/owniverse/world_parameters.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) -> 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:
|
||||||
|
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:
|
||||||
|
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(path, FileAccess.READ, password)
|
||||||
|
if file == null:
|
||||||
|
blocked_saves[filename] = true
|
||||||
|
_fail("Cannot decrypt save: " + path)
|
||||||
|
return {}
|
||||||
|
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",
|
||||||
|
]
|
||||||
|
# Optional additions to the current format. Saves predating indicators keep
|
||||||
|
# their quantities and receive neutral defaults from Owniverse on load.
|
||||||
|
for field in saved:
|
||||||
|
if field not in required and field not in ["world_parameters", "trends"]:
|
||||||
|
return "unsupported state structure"
|
||||||
|
if saved.has("world_parameters"):
|
||||||
|
if not _number_map(saved.world_parameters) or saved.world_parameters.size() != WorldParameters.defaults().size():
|
||||||
|
return "invalid state.world_parameters"
|
||||||
|
for key in WorldParameters.defaults():
|
||||||
|
if not saved.world_parameters.has(key) or saved.world_parameters[key] < 1.0:
|
||||||
|
return "invalid world parameter " + key
|
||||||
|
if key in ["Bur", "Exp", "Agg"] and saved.world_parameters[key] > 19.0:
|
||||||
|
return "invalid world scale " + key
|
||||||
|
if saved.has("trends"):
|
||||||
|
if not _number_map(saved.trends) or saved.trends.size() != WorldParameters.TRENDS.size():
|
||||||
|
return "invalid state.trends"
|
||||||
|
for key in WorldParameters.TRENDS:
|
||||||
|
if not saved.trends.has(key) or saved.trends[key] >= 1.0:
|
||||||
|
return "invalid trend " + key
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Settings
|
||||||
|
func save_settings():
|
||||||
|
var config = ConfigFile.new()
|
||||||
|
|
||||||
|
for item in Global.Settings:
|
||||||
|
config.set_value(SETTING_SECTION, item, Global.Settings[item])
|
||||||
|
config.save(SAVE_PATH)
|
||||||
|
|
||||||
|
func load_settings():
|
||||||
|
var config = ConfigFile.new()
|
||||||
|
var err = config.load(SAVE_PATH)
|
||||||
|
if err != OK:
|
||||||
|
return false
|
||||||
|
for item in Global.Settings:
|
||||||
|
Global.Settings[item] = config.get_value(SETTING_SECTION, item, Global.Settings[item])
|
||||||
|
return true
|
||||||
|
#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
|
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||||
|
S|1|S:0|E:1||||||||||||||||S||||
|
||||||
H|1|H:0|H:0||||||||||||||||H||||
|
H|1|H:0|E:1||||||||||||||||H||||
|
||||||
He|1|He:1|||||||||||||||||||||
|
He|1|He:1|||||||||||||||||||||
|
||||||
Met|1|Met:1|||||||||||||||||||||
|
Met|1|Met:1|||||||||||||||||||||
|
||||||
BD|1|BD:1|||||||||||||||||||||
|
BD|1|BD:1|||||||||||||||||||||
|
||||||
@@ -21,14 +21,14 @@ LRG|0||||50||50000000||||S:300||DSN^SGRB%0.1|2|||RG|Star|||||
|
|||||||
HRG|0||||100||10000000||||S:400.||SN^GRB%1|2|||RG|Star|||||
|
HRG|0||||100||10000000||||S:400.||SN^GRB%1|2|||RG|Star|||||
|
||||||
RSG|0||||300||1000000||||S:500||BSN^LGRB%3|2|||RG|Star|||||
|
RSG|0||||300||1000000||||S:500||BSN^LGRB%3|2|||RG|Star|||||
|
||||||
RHG|0||||1000||100000||||S:600||HN^HGRB%10|2|||RG|Star|||||
|
RHG|0||||1000||100000||||S:600||HN^HGRB%10|2|||RG|Star|||||
|
||||||
DSN|0||||||1|||2|S:59.1||SNS:1&H:6.5&He:13.5&Met:11&blast:2|2|mass:15&H:6.5&HE:13.5&MET:11||||SNGRBB||||
|
DSN|0||||||1|||2|S:59.1||SNS:1&H:6.5&He:13.5&Met:11|2|mass:15&H:6.5&HE:13.5&MET:11||||SNGRBB||||
|
||||||
SN|0||||||1|||9|S:99.1||LNS:1&H:8&He:50&Met:11&blast:9|2|mass:18&H:8&HE:50&MET:11||||SNGRBB||||
|
SN|0||||||1|||9|S:99.1||LNS:1&H:8&He:50&Met:11|2|mass:18&H:8&HE:50&MET:11||||SNGRBB||||
|
||||||
BSN|0||||||1||.|305|S:309.1||MGT:1&H:15&He:186&Met:15&blast:305|2|mass:21&H:15&HE:186&MET:15||||SNGRBB||||
|
BSN|0||||||1||.|305|S:309.1||MGT:1&H:15&He:186&Met:15|2|mass:21&H:15&HE:186&MET:15||||SNGRBB||||
|
||||||
HN|0||||||1|||9990|S:939.1||BH40:1&H:30&He:560&Met:30&blast:9990|2|mass:40&H:30&HE:560&MET:30||||SNGRBB||||
|
HN|0||||||1|||9990|S:939.1||BH40:1&H:30&He:560&Met:30|2|mass:40&H:30&HE:560&MET:30||||SNGRBB||||
|
||||||
SGRB|0||||||1||0.001|31|S:149.1||BH25:1&H:0.5&He:10&Met:4&blast:31|2|mass:25&H:0.5&HE:10&MET:4||||SNGRBB||||
|
SGRB|0||||||1||0.001|31|S:149.1||BH25:1&H:0.5&He:10&Met:4|2|mass:25&H:0.5&HE:10&MET:4||||SNGRBB||||
|
||||||
GRB|0||||||1||0.01|136|S:239.1||BH30:1&H:19&He:12&Met:5&blast:136|2|mass:30&H:19&HE:12&MET:5||||SNGRBB||||
|
GRB|0||||||1||0.01|136|S:239.1||BH30:1&H:19&He:12&Met:5|2|mass:30&H:19&HE:12&MET:5||||SNGRBB||||
|
||||||
LGRB|0||||||1||0.03|3446|S:669.1||BH40:1&H:68&He:18&Met:9&blast:3446|2|mass:40&H:68&HE:18&MET:9||||SNGRBB||||
|
LGRB|0||||||1||0.03|3446|S:669.1||BH40:1&H:68&He:18&Met:9|2|mass:40&H:68&HE:18&MET:9||||SNGRBB||||
|
||||||
HGRB|0||||||1||0.1|97342|S:1939.1||BH60:1&H:0&He:40&Met:10&blast:97342|2|mass:60&H:0&HE:40&MET:10||||SNGRBB||||
|
HGRB|0||||||1||0.1|97342|S:1939.1||BH60:1&H:0&He:40&Met:10|2|mass:60&H:0&HE:40&MET:10||||SNGRBB||||
|
||||||
CBD|0||||0.1||||||||||||BD||||||
|
CBD|0||||0.1||||||||||||BD||||||
|
||||||
WD|1|WD:1|||||||||||||||||||||
|
WD|1|WD:1|||||||||||||||||||||
|
||||||
TWD|0||||0.8||||||||||||WD|Star|Degen||||
|
TWD|0||||0.8||||||||||||WD|Star|Degen||||
|
||||||
@@ -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||||
|
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||||
|
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
|
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
|
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|44289|C2.5
|
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|44230|C2.6
|
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
|
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
|
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|44409|C3.0
|
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|44378|C3.1
|
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|44348|C3.2
|
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|44317|C3.3
|
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|||44318|C2.9
|
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|||44257|C3.0
|
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|||44229|C3.1
|
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|||44198|C3.2
|
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
|
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
|
LGG|1|GAL:20||GAL:50|||10000000000|||||||||||LS|LS|||1.5|C3.4
|
||||||
LSC|1|LGG:1&GAL:100||LGG:200|||20000000000|||||||||||LS|LS|||44287|C3.6
|
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|||44256|C3.8
|
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|||44228|C4.1
|
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|||44197|C4.4
|
LUB|1|LGW:1&LGF:2000||LGW:1000|||100000000000|||||1000000000||||||LS|LS|||1.1|C4.4
|
||||||
GG|1|GG:1||||||||||||||||Planet|BPlanet||||
|
GG|1|GG:1||||||||||||||||Planet|BPlanet||||
|
||||||
AB|1|AB:1||||||||||||||||Planet|BPlanet||||
|
AB|1|AB:1||||||||||||||||Planet|BPlanet||||
|
||||||
RP|1|RP: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
|
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|||
|
S.Description|%value0%[p]Voids: %value1%[p]Needed for placing objects.|amount|voids|||
|
||||||
H.Description|%value0%[p]Used to create clouds and stars.|amount||||
|
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|||
|
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|
|
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|
|
G.Description|%value0%[p]Produces more hydrogen.[p]%value2%[p]Baseline lifespan: %value3%|amount||colored costs|lifespan|
|
||||||
@@ -1183,4 +1183,4 @@ Type1Civilizations.Path|GameHelp/en/Type1Civilizations|||||
|
|||||||
Type2Civilizations.Path|GameHelp/en/Type2Civilizations|||||
|
Type2Civilizations.Path|GameHelp/en/Type2Civilizations|||||
|
||||||
Type3Civilizations.Path|GameHelp/en/Type3Civilizations|||||
|
Type3Civilizations.Path|GameHelp/en/Type3Civilizations|||||
|
||||||
Type4Civilizations.Path|GameHelp/en/Type4Civilizations|||||
|
Type4Civilizations.Path|GameHelp/en/Type4Civilizations|||||
|
||||||
ButtonAbout.Image|Path/To/en/ButtonAbout.webp|||||
|
ButtonAbout.Image|Path/To/en/ButtonAbout.webp|||||
|
||||||
@@ -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]
|
[preset.0]
|
||||||
|
|
||||||
name="Windows Desktop"
|
name="Windows Desktop"
|
||||||
platform="Windows Desktop"
|
platform="Windows Desktop"
|
||||||
runnable=true
|
|
||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_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()
|
patches=PackedStringArray()
|
||||||
patch_delta_encoding=false
|
patch_delta_encoding=false
|
||||||
patch_delta_compression_level_zstd=19
|
patch_delta_compression_level_zstd=19
|
||||||
@@ -27,7 +30,7 @@ script_export_mode=2
|
|||||||
custom_template/debug=""
|
custom_template/debug=""
|
||||||
custom_template/release=""
|
custom_template/release=""
|
||||||
debug/export_console_wrapper=1
|
debug/export_console_wrapper=1
|
||||||
binary_format/embed_pck=false
|
binary_format/embed_pck=true
|
||||||
texture_format/s3tc_bptc=true
|
texture_format/s3tc_bptc=true
|
||||||
texture_format/etc2_astc=false
|
texture_format/etc2_astc=false
|
||||||
shader_baker/enabled=false
|
shader_baker/enabled=false
|
||||||
@@ -74,7 +77,6 @@ Remove-Item -Recurse -Force '{temp_dir}'"
|
|||||||
|
|
||||||
name="Android"
|
name="Android"
|
||||||
platform="Android"
|
platform="Android"
|
||||||
runnable=false
|
|
||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
@@ -110,7 +112,7 @@ architectures/armeabi-v7a=false
|
|||||||
architectures/arm64-v8a=true
|
architectures/arm64-v8a=true
|
||||||
architectures/x86=false
|
architectures/x86=false
|
||||||
architectures/x86_64=false
|
architectures/x86_64=false
|
||||||
version/code=130
|
version/code=146
|
||||||
version/name=""
|
version/name=""
|
||||||
package/unique_name="com.KIDGameProduction.Owniverse"
|
package/unique_name="com.KIDGameProduction.Owniverse"
|
||||||
package/name="Owniverse"
|
package/name="Owniverse"
|
||||||
@@ -136,11 +138,12 @@ screen/support_normal=true
|
|||||||
screen/support_large=true
|
screen/support_large=true
|
||||||
screen/support_xlarge=true
|
screen/support_xlarge=true
|
||||||
screen/background_color=Color(0, 0, 0, 1)
|
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
|
user_data_backup/allow=false
|
||||||
command_line/extra_args=""
|
command_line/extra_args=""
|
||||||
apk_expansion/enable=false
|
|
||||||
apk_expansion/SALT=""
|
|
||||||
apk_expansion/public_key=""
|
|
||||||
permissions/custom_permissions=PackedStringArray()
|
permissions/custom_permissions=PackedStringArray()
|
||||||
permissions/access_checkin_properties=false
|
permissions/access_checkin_properties=false
|
||||||
permissions/access_coarse_location=false
|
permissions/access_coarse_location=false
|
||||||
@@ -295,3 +298,6 @@ permissions/write_sms=false
|
|||||||
permissions/write_social_stream=false
|
permissions/write_social_stream=false
|
||||||
permissions/write_sync_settings=false
|
permissions/write_sync_settings=false
|
||||||
permissions/write_user_dictionary=false
|
permissions/write_user_dictionary=false
|
||||||
|
apk_expansion/enable=false
|
||||||
|
apk_expansion/SALT=""
|
||||||
|
apk_expansion/public_key=""
|
||||||
|
|||||||
Binary file not shown.
+5
-5
@@ -8,24 +8,24 @@
|
|||||||
|
|
||||||
config_version=5
|
config_version=5
|
||||||
|
|
||||||
[animation]
|
[accessibility]
|
||||||
|
|
||||||
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
general/updates_per_second=10
|
||||||
|
|
||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Owniverse"
|
config/name="Owniverse"
|
||||||
config/version="0.6.14"
|
config/version="0.7.0"
|
||||||
run/main_scene="uid://bv384dpmvjv8o"
|
run/main_scene="uid://bv384dpmvjv8o"
|
||||||
config/features=PackedStringArray("4.7", "Mobile")
|
config/features=PackedStringArray("4.7", "Mobile")
|
||||||
boot_splash/show_image=false
|
boot_splash/show_image=false
|
||||||
config/icon="uid://do5teb1i7uiuw"
|
config/icon="uid://djiwkyk470qb2"
|
||||||
config/size/borderless=true
|
config/size/borderless=true
|
||||||
config/stretch_mode=0
|
config/stretch_mode=0
|
||||||
|
|
||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
Global="*uid://dojk16crca3cx"
|
Global="*uid://dy0hrqie4ritn"
|
||||||
SaveManager="*uid://ul0srjc4gcgg"
|
SaveManager="*uid://ul0srjc4gcgg"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
# Эволюция звёзд 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 нет. Миграция сохранений не требуется: квота использует существующий словарь, отсутствующее значение равно нулю.
|
||||||
|
|
||||||
|
## Постепенные партии
|
||||||
|
|
||||||
|
### Короткоживущие звёздные события внутри длинного шага
|
||||||
|
|
||||||
|
Для SN, DSN, BSN, HN и GRB-событий полностью завершившиеся окна рождения не создают отдельные словари партий. Их целые количества суммируются в очередь завершений текущего расчёта; обычные партии сохраняются только для окон, срок которых ещё не закончился. Состояние этой очереди не записывается в сохранение: она полностью обрабатывается до публикации нового состояния.
|
||||||
|
|
||||||
|
Окно завершено, если `window_end + lifespan ≤ to_year`. Его доля количества рассчитывается прежним методом разности накопленных целых значений. Для производства пространства интеграл исчезновения вычитается по окнам в прежнем порядке, чтобы сохранить округление. Здесь остаётся лёгкий арифметический проход по окнам, но отсутствуют создание, копирование, сортировка и последующая обработка тысяч временных словарей.
|
||||||
|
|
||||||
|
Ресурсы, остатки звёзд и бласты от завершившейся части применяются на следующем поколении расчёта, как раньше. Поэтому сохраняются порядок передачи привязок, среда бластов и ограничения цивилизаций. Ускорение допускается только для событий группы `SNGRBB` с линейным выходом и без преемников с ненулевым сроком жизни. При изменении таблиц остальные случаи автоматически используют обычный путь. Сроки жизни и ширина окон не увеличиваются, формат сохранения не меняется.
|
||||||
|
|
||||||
|
На сохранении возраста 140,55 млн лет, при шестой скорости и шаге 1/60 секунды: максимальное временное число партий SN/GRB уменьшилось с 1668 до 2; средний расчёт кадра без отрисовки — примерно с 15 до 10 мс. За 120 шагов результаты старого и нового путей совпали по всем полям состояния. Эти значения характеризуют конкретное сохранение, а не гарантированную частоту кадров на всех устройствах.
|
||||||
|
|
||||||
|
### Общие окна рождения и эволюции
|
||||||
|
|
||||||
|
Окно рождения шириной `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`.
|
||||||
|
|
||||||
|
Планетные бонусы сил пока не перенесены. Параметры мира Bur/Exp/Agg имеют собственные сохраняемые шкалы и эффективные множители (см. раздел индикаторов ниже). Металличность для производства фиксируется до расходования ресурсов на звёзды. Среда бласта фиксируется для поколения до добавления его продуктов.
|
||||||
|
|
||||||
|
## Разрушение и бласты
|
||||||
|
|
||||||
|
Локальное воздействие: `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` сущности: `power = expired_count × max(0, entity.blast)`. При `blast = 0` внешнего воздействия нет. Старые вхождения `blast:N` в `output` игнорируются, включая альтернативные выходы; двойного суммирования нет. Внутренний выход `blast` остаётся событием, а не предметом. `destroy_level` независимо задаёт уровень локального разрушения у умирающего объекта; внешние бласты всегда действуют на уровне 1.
|
||||||
|
|
||||||
|
## Границы этапа
|
||||||
|
|
||||||
|
Рассчитываются агрегаты по типам, а не отдельные звёздные системы; конкретная принадлежность каждой планеты отдельной звезде не моделируется. Распределения целых объектов детерминированы. Порядок внутри длинного шага — по поколениям, поэтому нелинейное воздействие бластов и среды не обязано совпадать с серией более коротких шагов. Эволюция 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. Уже назначенные сроки партий не переписываются при изменении бонуса.
|
||||||
|
|
||||||
|
Ct не получает часы. Инициализация отсутствующих партий жизни выполняется при загрузке и перед продвижением симуляции. Параметры мира и тенденции описаны ниже; интерфейс применения сил в этот перенос не входит.
|
||||||
|
|
||||||
|
## Параметры мира, тенденции и индикаторы
|
||||||
|
|
||||||
|
Источник формул — Unity `OwniverseInstance`, `Life`, `LifeActions`, `Trends` и классы индикаторов. Расчёты находятся в `world_parameters.gd`; показ — в `ui/indicators/world_indicators.gd`. Шкалы обновляются каждые 0,1 реальной секунды и сразу при создании/загрузке вселенной. Подписи используют текущую локаль и шаблоны.
|
||||||
|
|
||||||
|
### Энергия и плотность
|
||||||
|
|
||||||
|
Энергия сохраняет экономику GI: запас расходуется на H/S. Заполнение индикатора — `clamp(energy / 1e14, 0, 1)`. Это не коэффициент производства Unity; дополнительное замедление производства при уменьшении запаса не вводится. В описании выводятся остаток и процент.
|
||||||
|
|
||||||
|
Плотность берётся из `stellar_environment`. Положение указателя ограничено диапазоном 0…1:
|
||||||
|
|
||||||
|
- `density ≤ 1e-5`: шкала разрыва, `(log10(max(density, 1e-7)) + 7) / 2`;
|
||||||
|
- `1e-5 < density < 0.9`: обычная шкала, `ln(density × 1e5) / ln(9e4)`;
|
||||||
|
- `density ≥ 0.9`: шкала сжатия, `(density − 0.9) × 10`.
|
||||||
|
|
||||||
|
В Unity в первой формуле было `−7`; это ошибка, не позволяющая указателю двигаться. Здесь исправлено на `+7`. Переключение шкалы меняет и заголовок/описание индикатора. Само наступление апокалипсиса индикатор не запускает.
|
||||||
|
|
||||||
|
Общая рамка `Background/WarningBorder` получает цвета Unity `WarningUI` в формате RGBA: при плотности ≤1e-5 — `C040407F`, ≤1e-6 — `DF2020BF`, ≤1e-7 — `FF0000FF`; при плотности ≥0,9 — `40A0C07F`, ≥0,97 — `20B0DFBF`, ≥1 — `00C0FFFF`. В нормальном диапазоне рамка прозрачна. Она обновляется вместе с индикаторами, не перехватывает нажатия и снимается при восстановлении плотности. Необратимая последовательность апокалипсиса Unity сюда не входит.
|
||||||
|
|
||||||
|
### Горючесть, взрывчатость и агрессия
|
||||||
|
|
||||||
|
`world_parameters` хранит Bur, Exp и Agg в диапазоне 1…19, начальное значение — 10. Эффективный множитель `M(x) = 10^((x−10)/9)`: крайние значения 0,1 и 10, нейтральное — 1.
|
||||||
|
|
||||||
|
- Bur делит срок жизни новых звёзд на `M(Bur)`. `modify_world_parameter("Bur", delta)` также пересчитывает оставшееся время существующих звёзд с коэффициентом `старый M / новый M`, сохраняя количество и остатки округления. Производство звёзд умножается на `M`, если `M ≤ 1`, иначе на `sqrt(M)`.
|
||||||
|
- Exp умножает вероятность альтернативного исхода (с ограничением до 1), мощность `entity.blast` и производство событий группы SNGRBB. Локальный `destroy_level` от него не меняется.
|
||||||
|
- Для Agg обозначим `a = M(Agg)`. Вероятность прогресса умножается на `a`, деградации — на `(1 − (1−a)/20) / Order`, войны — на `1 − (1−a)/5`, колонизации — на `1 − (1−a)/2`. Если сумма прогресса и деградации превышает 1, они пропорционально нормируются. Вероятности войны/колонизации ограничены единицей.
|
||||||
|
- Для первичной жизни дополнительный множитель прогресса — Enlightenment. Order дополнительно уменьшает потери в войнах и побочный ущерб. Начальные Enlightenment и Order равны 1.
|
||||||
|
|
||||||
|
Изменять шкалы следует через `owniverse.modify_world_parameter(key, delta)`, чтобы горючесть корректно обновляла партии. Готовых кнопок применения сил этот метод не добавляет.
|
||||||
|
|
||||||
|
### Тенденции
|
||||||
|
|
||||||
|
Пять направлений: Antitheism, Integration, Xenocide, Reconstruction, Spirituality. Участвуют космические стадии с непустым `home`; вес первой — 1, каждой следующей — вдвое больше. Для каждой тенденции:
|
||||||
|
|
||||||
|
`rate = Σ_стадий (вес стадии × Σ_этик(доля этики × вес этики))`
|
||||||
|
|
||||||
|
`прирост = rate × прошедшие игровые годы × 1e-9`.
|
||||||
|
|
||||||
|
Доли берутся на начало интервала, после проверки вместимости. Умножение на время исправляет ошибку Unity, где аргумент period не использовался. Накопление само по себе не зависит от FPS; изменение состава цивилизаций внутри крупных шагов остаётся дискретным, как остальная симуляция.
|
||||||
|
|
||||||
|
Веса этик в порядке пяти направлений:
|
||||||
|
|
||||||
|
| Этика | Антитеизм | Интеграция | Ксеноцид | Реконструкция | Духовность |
|
||||||
|
|---|---:|---:|---:|---:|---:|
|
||||||
|
| m | 0 | 2 | 2 | 0 | 6 |
|
||||||
|
| l | 6 | 1 | 1 | 2 | 0 |
|
||||||
|
| w | 2 | 0 | 6 | 0 | 2 |
|
||||||
|
| d | 0 | 6 | 0 | 1 | 3 |
|
||||||
|
| c | 1 | 3 | 3 | 3 | 0 |
|
||||||
|
| b | 3 | 0 | 0 | 6 | 1 |
|
||||||
|
|
||||||
|
При достижении 1 учитываются все целые срабатывания, дробный остаток сохраняется. Только Integration имеет последствия в исходной Unity: равновероятно выбирается Federation (+0,1 и Agg −1), Enlightenment (+0,2 и Agg −1) или Order (+0,1). Генератор сохраняет своё состояние. До 256 событий за шаг выбор выполняется по одному; для больших количеств используется нормальная аппроксимация мультиномиального распределения с сохранением точного суммарного числа событий, чтобы высокие скорости не блокировали кадр. Она может менять последовательность исходов при другом разбиении больших интервалов.
|
||||||
|
|
||||||
|
Federation сохраняется и увеличивается как в Unity, но в исходном коде не применяется к дипломатии. Дополнительное правило для него не придумано. Остальные четыре тенденции имеют счётчики срабатываний и сброс, но не последствия — так же, как в исходной реализации. Параметры и остатки тенденций сохраняются; у существующего сохранения текущего формата без этих полей используются нейтральные значения и нулевые тенденции, без изменения ресурсов или возраста.
|
||||||
+520
@@ -0,0 +1,520 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
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")
|
||||||
|
const WorldIndicators = preload("res://ui/indicators/world_indicators.gd")
|
||||||
|
var world_indicators: RefCounted
|
||||||
|
var indicator_elapsed: float = 0.0
|
||||||
|
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
|
||||||
|
|
||||||
|
@onready var stage_page_scroller: ScrollContainer = $World/PageScroller
|
||||||
|
|
||||||
|
@onready var item_pages: Array[Control] = [
|
||||||
|
$World/PageScroller/HBoxContainer/ForcePage/Forces,
|
||||||
|
$World/PageScroller/HBoxContainer/ResourcePage/HBoxContainer/Resources,
|
||||||
|
$World/PageScroller/HBoxContainer/StarPage/Stars,
|
||||||
|
$World/PageScroller/HBoxContainer/StructurePage/Structures,
|
||||||
|
$World/PageScroller/HBoxContainer/LifePage/Planets/Life,
|
||||||
|
$World/PageScroller/HBoxContainer/LifePage/Planets/LivingPlanet,
|
||||||
|
$World/PageScroller/HBoxContainer/LifePage/Planets/Civ0,
|
||||||
|
$World/PageScroller/HBoxContainer/LifePage/Planets/Civ1,
|
||||||
|
$World/PageScroller/HBoxContainer/LifePage/Planets/Civ2,
|
||||||
|
$World/PageScroller/HBoxContainer/LifePage/Planets/Civ3,
|
||||||
|
$World/PageScroller/HBoxContainer/LifePage/Planets/Civ4
|
||||||
|
]
|
||||||
|
|
||||||
|
@onready var page_actions: Dictionary[int, OwniverseAction] = {
|
||||||
|
2: $World/PageScroller/HBoxContainer/StarPage/Actions,
|
||||||
|
3: $World/PageScroller/HBoxContainer/StructurePage/Actions,
|
||||||
|
}
|
||||||
|
|
||||||
|
@onready var g_item_descriptions: Array = [
|
||||||
|
$World/PageScroller/HBoxContainer/ForcePage/Description,
|
||||||
|
$World/PageScroller/HBoxContainer/ResourcePage/Description,
|
||||||
|
$World/PageScroller/HBoxContainer/StarPage/Description,
|
||||||
|
$World/PageScroller/HBoxContainer/StructurePage/Description,
|
||||||
|
$World/PageScroller/HBoxContainer/LifePage/Description
|
||||||
|
]
|
||||||
|
|
||||||
|
@onready var date_time: Label = $World/TopMenu/InfoPanel/DateTime
|
||||||
|
|
||||||
|
@onready var g_speed_icons: Array = [
|
||||||
|
$World/TopMenu/InfoPanel/Speed/Speed1,
|
||||||
|
$World/TopMenu/InfoPanel/Speed/Speed2,
|
||||||
|
$World/TopMenu/InfoPanel/Speed/Speed3,
|
||||||
|
$World/TopMenu/InfoPanel/Speed/Speed4,
|
||||||
|
$World/TopMenu/InfoPanel/Speed/Speed5,
|
||||||
|
$World/TopMenu/InfoPanel/Speed/Speed6,
|
||||||
|
$World/TopMenu/InfoPanel/Speed/Speed7,
|
||||||
|
$World/TopMenu/InfoPanel/Speed/Speed8
|
||||||
|
]
|
||||||
|
|
||||||
|
@onready var g_time_minus: TextureButton = $World/TopMenu/TimeMinus
|
||||||
|
@onready var g_time_plus: TextureButton = $World/TopMenu/TimePlus
|
||||||
|
|
||||||
|
@onready var screens: Dictionary = {
|
||||||
|
"World": $World,
|
||||||
|
"MainMenu": $MainMenu,
|
||||||
|
"Report": $Report,
|
||||||
|
"Achievements": $Achievements,
|
||||||
|
"Credits": $Credits,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var g_tween: Tween = null
|
||||||
|
var g_dragged_start: Vector2 = Vector2.ZERO
|
||||||
|
var g_scroll_start: int = 0
|
||||||
|
#var g_current_page: int = 0
|
||||||
|
|
||||||
|
var g_last_scroll_updated: int = 0
|
||||||
|
|
||||||
|
var f_check_swipe: bool = false
|
||||||
|
var g_swipe_time_delta: float = 0
|
||||||
|
|
||||||
|
var g_counters: Dictionary[String, Counter] = {
|
||||||
|
"swipe": Counter.new(),
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = []
|
||||||
|
|
||||||
|
|
||||||
|
func set_datetime(value: float):
|
||||||
|
# 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 set_owniverse_item_value() -> void:
|
||||||
|
for item_id in owniverse.presentation.pending_amounts:
|
||||||
|
if owniverse_items.has(item_id):
|
||||||
|
owniverse_items[item_id].set_label(owniverse.presentation.pending_amounts[item_id])
|
||||||
|
owniverse.presentation.pending_amounts.clear()
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
func _unlock_items():
|
||||||
|
var result: Dictionary = owniverse.presentation.unlocks(owniverse.state, owniverse.catalog, locked_items)
|
||||||
|
for item_id in result.visible:
|
||||||
|
if owniverse_items.has(item_id):
|
||||||
|
owniverse_items[item_id].set_visible(true)
|
||||||
|
locked_items = result.remaining
|
||||||
|
|
||||||
|
func _on_indicator_pressed(indicator_name: String, page: int = 0):
|
||||||
|
_set_description(indicator_name, page)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_item_description(item: OwniverseItem) -> void:
|
||||||
|
var category: String = "Entity" if owniverse.catalog.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:
|
||||||
|
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:
|
||||||
|
# TODO переписать это ↓
|
||||||
|
if !g_active_items.has(item.item_page):
|
||||||
|
g_active_items[item.item_page] = null
|
||||||
|
|
||||||
|
var current_item: OwniverseItem = g_active_items[item.item_page]
|
||||||
|
if current_item == item:
|
||||||
|
return
|
||||||
|
|
||||||
|
g_active_items[item.item_page] = item
|
||||||
|
|
||||||
|
if current_item != null and current_item.item_page == item.item_page:
|
||||||
|
current_item.set_selection(false)
|
||||||
|
|
||||||
|
item.set_selection(true)
|
||||||
|
|
||||||
|
# subpages
|
||||||
|
if item.item_sub_page != -1:
|
||||||
|
item_pages[item.item_sub_page].visible = true
|
||||||
|
|
||||||
|
if current_item == null \
|
||||||
|
or current_item.item_sub_page == -1 \
|
||||||
|
or current_item.item_sub_page == item.item_sub_page:
|
||||||
|
return
|
||||||
|
item_pages[current_item.item_sub_page].visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func _init_owniverse_items() -> bool:
|
||||||
|
owniverse_items = {}
|
||||||
|
for item_id in Global.OwniverseItems:
|
||||||
|
var item_props: Dictionary = Global.OwniverseItems[item_id]
|
||||||
|
var item: OwniverseItem = OWNIVERSE_ITEM.instantiate()
|
||||||
|
if !item_props.has("page"):
|
||||||
|
printerr("Item `", item_id, "` has no `page` key.")
|
||||||
|
return false
|
||||||
|
var page: int = int(item_props.page)
|
||||||
|
item_pages[page].add_child(item)
|
||||||
|
item.init_button(item_props)
|
||||||
|
item.item_activated.connect(item_activated)
|
||||||
|
|
||||||
|
owniverse_items[item_id] = item
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func item_activated(item: OwniverseItem) -> void:
|
||||||
|
_control_page(item)
|
||||||
|
_set_item_description(item)
|
||||||
|
|
||||||
|
if item.item_id == "S" or item.item_id == "H":
|
||||||
|
var _value = owniverse.state.amount[item.item_id]
|
||||||
|
_value = 9 * _value if _value > 0 else 1
|
||||||
|
owniverse.production.produce_hs(item.item_id, _value)
|
||||||
|
|
||||||
|
|
||||||
|
func _check_counters(delta: float) -> Array:
|
||||||
|
var actions: Array = []
|
||||||
|
for k in g_counters:
|
||||||
|
var cnt: Counter = g_counters[k]
|
||||||
|
if not cnt.is_active:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cnt.value += delta
|
||||||
|
if cnt.value >= cnt.sec:
|
||||||
|
actions.append(k)
|
||||||
|
cnt.value = 0
|
||||||
|
cnt.is_active = false
|
||||||
|
|
||||||
|
return actions
|
||||||
|
|
||||||
|
|
||||||
|
#region Swipe and page
|
||||||
|
|
||||||
|
func _do_actions(actions: Array) -> void:
|
||||||
|
for action: String in actions:
|
||||||
|
match action:
|
||||||
|
"swipe":
|
||||||
|
_swipe_to_nearest_page()
|
||||||
|
|
||||||
|
|
||||||
|
func _swipe_to_nearest_page() -> void:
|
||||||
|
if stage_page_scroller.scroll_horizontal % PAGE_WIDTH == 0: return
|
||||||
|
|
||||||
|
var new_page: int = round(1.0 * stage_page_scroller.scroll_horizontal / PAGE_WIDTH)
|
||||||
|
|
||||||
|
if g_tween != null:
|
||||||
|
if g_tween.is_running():
|
||||||
|
return
|
||||||
|
|
||||||
|
g_tween = stage_page_scroller.get_tree().create_tween()
|
||||||
|
g_tween.tween_property(stage_page_scroller, "scroll_horizontal",
|
||||||
|
new_page * PAGE_WIDTH, SWIPE_DURATION).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventScreenDrag:
|
||||||
|
stage_page_scroller.scroll_horizontal = clamp(
|
||||||
|
stage_page_scroller.scroll_horizontal - PAGE_COUNT * event.screen_relative.x,
|
||||||
|
0, PAGE_WIDTH * (PAGE_COUNT - 1))
|
||||||
|
g_counters.swipe.is_active = true
|
||||||
|
g_counters.swipe.value = 0
|
||||||
|
elif event is InputEventPanGesture:
|
||||||
|
g_counters.swipe.is_active = true
|
||||||
|
g_counters.swipe.value = 0
|
||||||
|
elif event is InputEventScreenTouch:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
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 entity: OwniverseEntity = owniverse.catalog.entities[item_id]
|
||||||
|
|
||||||
|
#var amount = owniverse.get_available_to_produce(item_id)
|
||||||
|
|
||||||
|
#var amount = owniverse.get_available_to_produce(entity)
|
||||||
|
var amount = owniverse.production.calculate_max_producible(entity)
|
||||||
|
#print(item_id, " : ", amount)
|
||||||
|
if item_id == "SMBH":
|
||||||
|
var preview: Callable = func(requested: float) -> float:
|
||||||
|
return owniverse.production.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:
|
||||||
|
if !g_active_items.has(page_id):
|
||||||
|
return
|
||||||
|
|
||||||
|
var item_id: String = g_active_items[page_id].item_id
|
||||||
|
|
||||||
|
if action_amount > 0:
|
||||||
|
owniverse.production.produce(item_id, action_amount)
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
get_tree().auto_accept_quit = false
|
||||||
|
world_indicators = WorldIndicators.new(self)
|
||||||
|
create_new_universe()
|
||||||
|
load_data()
|
||||||
|
_refresh_universe_ui()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
## Starts a fresh universe. Can be connected directly to a button's pressed signal.
|
||||||
|
## Saving continues through the normal autosave/close path.
|
||||||
|
func create_new_universe() -> void:
|
||||||
|
var previous: Owniverse = owniverse
|
||||||
|
owniverse = Owniverse.new()
|
||||||
|
var inited_result: Dictionary = init_enitities()
|
||||||
|
locked_items = inited_result.locked_items
|
||||||
|
|
||||||
|
for entity in previous.catalog.entities.values():
|
||||||
|
entity.free()
|
||||||
|
previous.free()
|
||||||
|
|
||||||
|
if is_instance_valid(g_tween):
|
||||||
|
g_tween.kill()
|
||||||
|
g_active_items.clear()
|
||||||
|
description_targets.clear()
|
||||||
|
description_samples.clear()
|
||||||
|
description_rates.clear()
|
||||||
|
description_elapsed = 0.0
|
||||||
|
for label in g_item_descriptions:
|
||||||
|
label.text = ""
|
||||||
|
for item in owniverse_items.values():
|
||||||
|
item.get_parent().remove_child(item)
|
||||||
|
item.queue_free()
|
||||||
|
_init_owniverse_items()
|
||||||
|
for page in range(5, item_pages.size()):
|
||||||
|
item_pages[page].visible = false
|
||||||
|
for actions in page_actions.values():
|
||||||
|
actions.set_actions("", 0.0)
|
||||||
|
pending_page = RESOURCE_PAGE
|
||||||
|
_restore_page.call_deferred()
|
||||||
|
_refresh_universe_ui()
|
||||||
|
_on_topmenu_button_pressed("World")
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_universe_ui() -> void:
|
||||||
|
owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
|
||||||
|
_change_speed(0)
|
||||||
|
set_datetime(owniverse.state.time.year)
|
||||||
|
set_owniverse_item_value()
|
||||||
|
_unlock_items()
|
||||||
|
if world_indicators != null: world_indicators.update(owniverse)
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
|
||||||
|
owniverse.evolute(delta)
|
||||||
|
indicator_elapsed += delta
|
||||||
|
if indicator_elapsed >= 0.1:
|
||||||
|
indicator_elapsed = 0.0
|
||||||
|
world_indicators.update(owniverse)
|
||||||
|
_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:
|
||||||
|
|
||||||
|
owniverse.speed = owniverse.speed + delta
|
||||||
|
|
||||||
|
var speed: int = owniverse.speed
|
||||||
|
var ind: int = 1
|
||||||
|
for icon in g_speed_icons:
|
||||||
|
if ind <= speed:
|
||||||
|
icon.visible = true
|
||||||
|
else:
|
||||||
|
icon.visible = false
|
||||||
|
ind += 1
|
||||||
|
if speed == 1:
|
||||||
|
g_time_minus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % "x")
|
||||||
|
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % "2")
|
||||||
|
elif speed == Global.SPEED_LIMIT:
|
||||||
|
g_time_minus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(Global.SPEED_LIMIT - 1))
|
||||||
|
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % "x")
|
||||||
|
else:
|
||||||
|
g_time_minus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed - 1))
|
||||||
|
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed + 1))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_topmenu_button_pressed(screen_name: String) -> void:
|
||||||
|
|
||||||
|
for screen_id in screens:
|
||||||
|
#print(screen_name, " ←→ ", screen_id)
|
||||||
|
screens[screen_id].visible = screen_id == screen_name
|
||||||
|
|
||||||
|
|
||||||
|
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.evolution.initialize_life_batches()
|
||||||
|
owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
|
||||||
|
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.request_save("owniverse", "qwerty", {
|
||||||
|
"state": owniverse.state,
|
||||||
|
"ui": {"current_page": page},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
var closing: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
if not is_node_ready() or closing:
|
||||||
|
return
|
||||||
|
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
||||||
|
_close_after_save()
|
||||||
|
elif what == NOTIFICATION_APPLICATION_PAUSED:
|
||||||
|
save_data()
|
||||||
|
# The OS may suspend frames immediately; finish the queued write now.
|
||||||
|
SaveManager.flush_saves()
|
||||||
|
|
||||||
|
|
||||||
|
func _close_after_save() -> void:
|
||||||
|
closing = true
|
||||||
|
process_mode = Node.PROCESS_MODE_DISABLED
|
||||||
|
save_data()
|
||||||
|
if SaveManager.is_saving():
|
||||||
|
await SaveManager.saves_drained
|
||||||
|
get_tree().quit()
|
||||||
|
|
||||||
|
|
||||||
|
func init_enitities() -> Dictionary:
|
||||||
|
var _result := {
|
||||||
|
'locked_items': [],
|
||||||
|
}
|
||||||
|
owniverse.catalog.load_definitions(Global.OwniverseEntities)
|
||||||
|
for entity_id in owniverse.catalog.entities:
|
||||||
|
var entity: OwniverseEntity = owniverse.catalog.entities[entity_id]
|
||||||
|
|
||||||
|
owniverse.state.stats.total_amount[entity_id] = 0
|
||||||
|
owniverse.state.amount[entity_id] = 0
|
||||||
|
if entity.lifespan > 0:
|
||||||
|
owniverse.state.batches[entity.id] = []
|
||||||
|
owniverse.batches.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 != {}:
|
||||||
|
_result.locked_items.append(entity.id)
|
||||||
|
|
||||||
|
return _result
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
func _on_achievements_pressed(extra_arg_0: String) -> void:
|
||||||
|
pass # Replace with function body.
|
||||||
|
|
||||||
|
|
||||||
|
func _on_credits_pressed(extra_arg_0: String) -> void:
|
||||||
|
pass # Replace with function body.
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
# Проверки поэтапного переноса из Owniverse.U
|
||||||
|
|
||||||
|
## Модули симуляции
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_module_lifecycle.gd`
|
||||||
|
|
||||||
|
Проверяются отдельные экземпляры модулей и состояния для каждой вселенной, слабая обратная ссылка, освобождение модулей вместе с миром и защита от некорректного delta. Остальные тесты используют прямые API batches, ledger, production, automatic, stellar, life, environment и evolution; временные методы-переадресации Owniverse удалены. Описание границ модулей и порядка фаз: `../world/owniverse/README.md`.
|
||||||
|
|
||||||
|
## Математика распределений и каталог
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_quantity_catalog.gd`
|
||||||
|
|
||||||
|
Проверяются целочисленность и границы распределения, сохранение суммы, разрешение равных остатков, накопление дробей и долгов округления, нормализация весов, размер производственной партии, загрузка каталога и его индексы. Архитектура описана в `../world/owniverse/README.md`. Сохранение игрока не используется.
|
||||||
|
|
||||||
|
## Иконки стадий жизни
|
||||||
|
|
||||||
|
Тест также проверяет модуль `item_presentation.gd`: отсутствие изменений входного состояния и списка закрытых иконок, решения по разблокировке, отложенную и принудительную публикацию количеств.
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_life_icons.gd`
|
||||||
|
|
||||||
|
Проверяются одновременная разблокировка подробных и общих иконок EON/LP и C0.0/C0, количества свободных и связанных планет без двойного учёта, переключение подпанелей и очистка счётчика при исчезновении стадии. Сохранение игрока не используется.
|
||||||
|
|
||||||
|
## Параметры мира и индикаторы
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/test_world_indicators.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверяются сроки жизни существующих и новых звёзд при смене горючести, вероятности и мощность взрывов, влияние агрессии/порядка/просвещения на жизнь, накопление тенденций по времени и этикам, несколько событий за шаг, продолжение после сериализации, валидация сохранения, границы шкал и описания на двух языках. Сцена использует фикстуру без доступа к сохранению игрока.
|
||||||
|
|
||||||
|
## Прямой расчёт завершившихся сверхновых
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/test_short_lived_events.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
Сравниваются ускоренный путь и прежний путь с созданием всех партий, отключаемый только в тестовой фикстуре. Проверяются LRG, HRG, RSG и RHG на границах коротких и длинных интервалов, существующие SN, нехватка энергии, ресурсы, бласты, привязки, остатки округления и живые партии. Дополнительно проверяется, что длинный интервал создаёт не более двух живых окон и одну запись завершений, сохраняя общее количество. Сохранение игрока этот тест не использует.
|
||||||
|
|
||||||
|
`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` в этом тесте ожидаемы.
|
||||||
|
|
||||||
|
Тест интерфейса проверяет передачу количеств из presentation в контролы, очистку очереди после публикации, отложенное и принудительное обновление, производство по нажатию и формат времени. Stage в текущей реализации применяет очередь счётчиков и время каждый кадр; интервал 0,1 секунды используется для индикаторов. Рефакторинг этот порядок не меняет. Тест использует игровую сцену с отключёнными загрузкой, сохранением и автоматической симуляцией, не обращаясь к сохранению игрока.
|
||||||
|
|
||||||
|
## Время: шаг 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. Проверки контролируют сохранение населения, целочисленность этик, валидацию сохранения и детерминированное продолжение после сериализации.
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
extends "res://world/owniverse/owniverse.gd"
|
||||||
|
|
||||||
|
# Compare with the materialized-batch path, without overriding the coordinator.
|
||||||
|
func _init() -> void:
|
||||||
|
stellar.direct_completions = false
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
uid://cn6hhpjw1sjpb
|
||||||
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,117 @@
|
|||||||
|
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 initial_energy: float = world.state.energy
|
||||||
|
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 == initial_energy 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 == initial_energy, "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()
|
||||||
|
# Exercise the stage entry point too: manager tests alone do not catch a
|
||||||
|
# regression back to the synchronous API in stage.gd.
|
||||||
|
var shared_manager: Node = root.get_node("SaveManager")
|
||||||
|
var original_directory: String = shared_manager.save_directory
|
||||||
|
shared_manager.save_directory = directory
|
||||||
|
var stage = load("res://stage/stage.gd").new()
|
||||||
|
stage.pending_page = 2
|
||||||
|
stage.owniverse.state.energy = 42.0
|
||||||
|
stage.save_data()
|
||||||
|
check(shared_manager.is_saving(), "Stage queues a background save instead of waiting for disk")
|
||||||
|
stage.owniverse.state.energy = 43.0
|
||||||
|
shared_manager.flush_saves()
|
||||||
|
check(shared_manager.load_data("owniverse", "qwerty").state.energy == 42.0, "Stage save writes an independent snapshot")
|
||||||
|
stage.owniverse.free()
|
||||||
|
stage.free()
|
||||||
|
shared_manager.save_directory = original_directory
|
||||||
|
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.catalog.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.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
|
world.catalog.entities["N"].auto = 0.0
|
||||||
|
world.state.amount.N = 1.0
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.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.batches.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.catalog.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.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
|
world.catalog.entities["N"].auto = 0.0
|
||||||
|
world.state.amount.N = 1.0
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.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.evolution.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.ledger.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.evolution.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.evolution.evolve_batches(world.state, 2100.0)
|
||||||
|
restored.evolution.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.evolution.initialize_life_batches()
|
||||||
|
world.evolution.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.evolution.evolve_batches(world.state, 1.0)
|
||||||
|
var after: Dictionary = world.ledger.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.catalog.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.catalog.entities:
|
||||||
|
if world.catalog.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.catalog.entities[id].lifespan = 1.0
|
||||||
|
world.catalog.entities[id].lifespan_delta = 1.0
|
||||||
|
global.CivilizationRules[id].span = 1.0
|
||||||
|
seed_population(world, "C0.2", 6.0)
|
||||||
|
world.evolution.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.evolution.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.evolution.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.catalog.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.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
|
world.catalog.entities["N"].auto = 0.0
|
||||||
|
world.state.amount.N = 1.0
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.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.evolution.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.evolution.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.evolution.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.evolution.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.evolution.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.evolution.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.evolution.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.evolution.initialize_life_batches()
|
||||||
|
check(world.state == snapshot, "Capacity enforcement is idempotent")
|
||||||
|
world.state.amount.RD = 0.0
|
||||||
|
world.evolution.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.ledger.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.evolution.initialize_life_batches()
|
||||||
|
check(world.state.amount["C4.2"] == 2.0, "SMBH capacity uses stellar mass equivalent")
|
||||||
|
check(world.ledger.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.evolution.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.catalog.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.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
|
world.catalog.entities["N"].auto = 0.0
|
||||||
|
world.state.amount.N = 1.0
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.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.presentation.amounts(world.state, world.catalog)
|
||||||
|
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.presentation.amounts(world.state, world.catalog, 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.catalog.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.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
|
world.catalog.entities["N"].auto = 0.0
|
||||||
|
world.state.amount.N = 1.0
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.entities.values():
|
||||||
|
entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var world = fixture()
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
var expected: Array = []
|
||||||
|
for key in world.catalog.entities:
|
||||||
|
if world.catalog.entities[key].group_as == id:
|
||||||
|
expected.append(key)
|
||||||
|
check(world.catalog.is_group(id) == (not expected.is_empty()), "Group classification: " + id)
|
||||||
|
check(world.catalog.members(id) == (expected if not expected.is_empty() else [id]), "Group members: " + id)
|
||||||
|
var members: Array = world.catalog.members("Star")
|
||||||
|
members.clear()
|
||||||
|
check(not world.catalog.members("Star").is_empty(), "Returned members are independent")
|
||||||
|
world.catalog.entities.N.group_as = "TestGroup"
|
||||||
|
world.catalog.rebuild_group_index()
|
||||||
|
check(world.catalog.members("TestGroup") == ["N"], "Explicit definition reload")
|
||||||
|
world.catalog.entities.N.free()
|
||||||
|
world.catalog.entities.erase("N")
|
||||||
|
check(not world.catalog.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.catalog.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.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
|
world.catalog.entities["N"].auto = 0.0
|
||||||
|
world.state.amount.N = 1.0
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.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.evolution.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.evolution.initialize_life_batches()
|
||||||
|
check(world.state == before, "Initialization is idempotent")
|
||||||
|
world.evolution.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.ledger.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.evolution.evolve_batches(world.state, 11e6)
|
||||||
|
restored.evolution.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.evolution.initialize_life_batches()
|
||||||
|
world.evolution.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.life_products(world.state, world.catalog.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.evolution.initialize_life_batches()
|
||||||
|
check(world.state.batches.HP[0].created_from == 123.4, "Old bound worlds begin life at load time")
|
||||||
|
world.stellar.destroy_planets(world.state, "RD", 1.0, 1)
|
||||||
|
world.evolution.evolve_batches(world.state, 1e7)
|
||||||
|
check(world.ledger.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.evolution.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.evolution.initialize_life_batches()
|
||||||
|
check(world.state == before, "Life batch initialization is idempotent")
|
||||||
|
world.evolution.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.evolution.initialize_life_batches()
|
||||||
|
world.evolution.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,68 @@
|
|||||||
|
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
|
||||||
|
var world = stage.owniverse
|
||||||
|
world.state.binds.RD = {"EON1": 10.0, "EON2": 4.0, "C0.0": 3.0}
|
||||||
|
world.state.amount.EON1 = 2.0
|
||||||
|
world.state.stats.total_amount.EON1 = 12.0
|
||||||
|
world.state.stats.total_amount.EON2 = 4.0
|
||||||
|
world.state.stats.total_amount["C0.0"] = 3.0
|
||||||
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
|
stage._refresh_universe_ui()
|
||||||
|
var counts: Dictionary = world.presentation.amounts(world.state, world.catalog)
|
||||||
|
check(counts.EON1 == 12.0 and counts.EON2 == 4.0 and counts.LP == 16.0, "Individual eons and their total include bound planets")
|
||||||
|
check(counts["C0.0"] == 3.0 and counts.C0 == 3.0 and counts.Planet == 19.0, "Civilization detail does not double the planetary total")
|
||||||
|
check(world.presentation.amounts(world.state, world.catalog, true).EON1 == 12.0 and world.presentation.amounts(world.state, world.catalog, true).LP == 16.0, "Historical counts retain individual and aggregate values")
|
||||||
|
check(world.state == before, "Presentation does not modify simulation state")
|
||||||
|
var pending: Array = ["EON1", "EON2", "EON3", "C0.0"]
|
||||||
|
var pending_before: Array = pending.duplicate()
|
||||||
|
var decision: Dictionary = world.presentation.unlocks(world.state, world.catalog, pending)
|
||||||
|
check(decision.remaining == ["EON3"], "Only unmet requirements remain pending")
|
||||||
|
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
|
||||||
|
check(decision.visible.has(id), "Unlock projection includes detail and alias: " + id)
|
||||||
|
check(pending == pending_before and world.state == before, "Unlock evaluation leaves inputs untouched")
|
||||||
|
var snapshot: Dictionary = world.presentation.pending_amounts.duplicate()
|
||||||
|
world.presentation.deferred_updates = true
|
||||||
|
world.state.amount.EON1 += 1.0
|
||||||
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
|
check(world.presentation.dirty and world.presentation.pending_amounts == snapshot, "Deferred refresh retains pending snapshot")
|
||||||
|
world.presentation.refresh(world.state, world.catalog, true)
|
||||||
|
check(not world.presentation.dirty and world.presentation.pending_amounts.EON1 == 13.0, "Forced refresh publishes latest amounts")
|
||||||
|
world.state.amount.EON1 -= 1.0
|
||||||
|
world.presentation.deferred_updates = false
|
||||||
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
|
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
|
||||||
|
check(stage.owniverse_items[id].visible, "Unlock both levels: " + id)
|
||||||
|
check(stage.owniverse_items[id].description.text != "", "Populate the icon counter: " + id)
|
||||||
|
check(not stage.owniverse_items.EON3.visible, "Unreached eon remains locked")
|
||||||
|
stage.item_activated(stage.owniverse_items.LP)
|
||||||
|
check(stage.owniverse_items.EON1.is_visible_in_tree(), "Selecting LP shows its unlocked eons")
|
||||||
|
stage.item_activated(stage.owniverse_items.C0)
|
||||||
|
check(stage.owniverse_items["C0.0"].is_visible_in_tree() and not stage.owniverse_items.EON1.is_visible_in_tree(), "Selecting C0 switches the detail panel")
|
||||||
|
world.state.binds.RD.EON2 = 0.0
|
||||||
|
stage._refresh_universe_ui()
|
||||||
|
check(stage.owniverse_items.EON2.visible and stage.owniverse_items.EON2.description.text == "", "Previously reached stage stays unlocked after extinction with no stale count")
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
scene.free()
|
||||||
|
print("Life icons: %d checks, %d failures" % [checks, failures])
|
||||||
|
quit(0 if failures == 0 else 1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c4p1ad0ij32xo
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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 run() -> void:
|
||||||
|
var World = load("res://world/owniverse/owniverse.gd")
|
||||||
|
var first = World.new()
|
||||||
|
var second = World.new()
|
||||||
|
var names: Array = ["batches", "ledger", "production", "automatic", "stellar", "life", "environment", "evolution"]
|
||||||
|
var references: Array = []
|
||||||
|
for name in names:
|
||||||
|
check(first.get(name) != second.get(name), "World has its own " + name)
|
||||||
|
check(first.get(name).world == first, "Module refers to its owner: " + name)
|
||||||
|
references.append(weakref(first.get(name)))
|
||||||
|
first.state.amount.H = 123.0
|
||||||
|
first.presentation.pending_amounts.H = 123.0
|
||||||
|
check(not second.state.amount.has("H") and second.presentation.pending_amounts.is_empty(), "State and pending presentation are isolated")
|
||||||
|
first.free()
|
||||||
|
for reference in references:
|
||||||
|
check(reference.get_ref() == null, "Module is released with its world")
|
||||||
|
check(second.evolution.world == second, "Other world remains usable")
|
||||||
|
var snapshot: Dictionary = second.state.duplicate(true)
|
||||||
|
second.evolute(NAN)
|
||||||
|
second.evolute(-1.0)
|
||||||
|
check(second.state == snapshot, "Entry point rejects invalid time deltas")
|
||||||
|
second.free()
|
||||||
|
print("Module lifecycle: %d checks, %d failures" % [checks, failures])
|
||||||
|
quit(1 if failures else 0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d2xk4ldldsgo3
|
||||||
@@ -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.catalog.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.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
|
world.catalog.entities["N"].auto = 0.0
|
||||||
|
world.state.amount.N = 1.0
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.entities.values():
|
||||||
|
entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var world = fixture()
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
world.catalog.entities.N.cost = {"H": 2.0}
|
||||||
|
world.catalog.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.production.try_production(world.state, world.catalog.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.production.try_production(world.state, world.catalog.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,63 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const Math = preload("res://world/owniverse/quantity_math.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 run() -> void:
|
||||||
|
var Catalog = load("res://world/owniverse/entity_catalog.gd")
|
||||||
|
check(Math.allocate_whole({}, 10.0) == {}, "Empty allocation")
|
||||||
|
check(Math.allocate_whole({"b": 1.0, "a": 1.0}, 1.0) == {"a": 1.0, "b": 0.0}, "Stable lexical tie")
|
||||||
|
for limit in range(30):
|
||||||
|
var requests: Dictionary = {"a": 3.9, "b": 9.1, "c": -2.0, "d": 7.0}
|
||||||
|
var result: Dictionary = Math.allocate_whole(requests, limit)
|
||||||
|
check(Math.sum(result) == minf(limit, 19.0), "Conserve bounded allocation")
|
||||||
|
for id in result:
|
||||||
|
check(result[id] == floorf(result[id]) and result[id] >= 0.0 and result[id] <= floorf(maxf(0.0, requests[id])), "Whole bounded branch")
|
||||||
|
check(Math.normalize_distribution({"a": -1.0, "b": 0.0}) == {}, "Nonpositive weights")
|
||||||
|
check(Math.normalize_distribution({"a": 2.0, "b": 6.0}) == {"a": 0.25, "b": 0.75}, "Normalize weights")
|
||||||
|
check(Math.production_step([0.25, 0.1, 2.0]) == 20.0, "Common whole production step")
|
||||||
|
check(Math.production_step([]) == 1.0, "Free production step")
|
||||||
|
var target: Dictionary = {}
|
||||||
|
var produced: float = 0.0
|
||||||
|
for i in 10:
|
||||||
|
produced += Math.accumulate_whole(target, "remainders", "a", 0.1)
|
||||||
|
check(produced == 1.0, "Fractional production carries")
|
||||||
|
var remainders: Dictionary = {}
|
||||||
|
var totals: Dictionary = {"a": 0.0, "b": 0.0, "c": 0.0}
|
||||||
|
for i in 30:
|
||||||
|
var result: Dictionary = Math.split(remainders, "split", 1.0, {"a": 1.0, "b": 1.0, "c": 1.0})
|
||||||
|
check(Math.sum(result) == 1.0, "Split conserves count")
|
||||||
|
for id in result: totals[id] += result[id]
|
||||||
|
check(totals == {"a": 10.0, "b": 10.0, "c": 10.0}, "Rounding debt distributes over time")
|
||||||
|
produced = 0.0
|
||||||
|
for i in 10: produced += Math.accumulate_rate(remainders, "rate", 1.0, 0.1)
|
||||||
|
check(produced == 1.0, "Rate carries")
|
||||||
|
var catalog = Catalog.new()
|
||||||
|
var rows: Dictionary = root.get_node("Global").OwniverseEntities
|
||||||
|
catalog.load_definitions(rows)
|
||||||
|
check(catalog.entities.size() == rows.size(), "Catalog loads all definitions")
|
||||||
|
for id in catalog.entities:
|
||||||
|
var entity = catalog.entities[id]
|
||||||
|
check(catalog.automatic_producers.has(entity) == (entity.product_per_year_type != ""), "Producer index")
|
||||||
|
check(catalog.unlockable_entities.has(entity) == (not entity.unlock.is_empty()), "Unlock index")
|
||||||
|
if entity.group_as != "":
|
||||||
|
check(catalog.groups[entity.group_as].has(id) and catalog.members(entity.group_as).has(id), "Group index")
|
||||||
|
if entity.event_group != "":
|
||||||
|
check(catalog.event_groups[entity.event_group].has(id), "Event index")
|
||||||
|
var members: Array = catalog.members("Star")
|
||||||
|
members.clear()
|
||||||
|
check(not catalog.members("Star").is_empty(), "Group results are independent")
|
||||||
|
check(Catalog.new().entities.is_empty(), "Catalog instances are independent")
|
||||||
|
for entity in catalog.entities.values(): entity.free()
|
||||||
|
print("Quantity and catalog: %d checks, %d failures" % [checks, failures])
|
||||||
|
quit(1 if failures else 0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bvri1enpsjisk
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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(legacy: bool):
|
||||||
|
var world = load("res://tests/fixtures/legacy_stellar_events.gd" if legacy else "res://world/owniverse/owniverse.gd").new()
|
||||||
|
for id in root.get_node("Global").OwniverseEntities:
|
||||||
|
world.catalog.entities[id] = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
|
||||||
|
world.state.amount[id] = 0.0
|
||||||
|
world.state.stats.total_amount[id] = 0.0
|
||||||
|
if world.catalog.entities[id].group_as != "":
|
||||||
|
world.state.amount[world.catalog.entities[id].group_as] = 0.0
|
||||||
|
world.state.stats.total_amount[world.catalog.entities[id].group_as] = 0.0
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func equivalent(a: Variant, b: Variant) -> bool:
|
||||||
|
if a is Dictionary and b is Dictionary:
|
||||||
|
if a.size() != b.size(): return false
|
||||||
|
for key in a:
|
||||||
|
if not b.has(key) or not equivalent(a[key], b[key]): return false
|
||||||
|
return true
|
||||||
|
if a is Array and b is Array:
|
||||||
|
if a.size() != b.size(): return false
|
||||||
|
for i in a.size():
|
||||||
|
if not equivalent(a[i], b[i]): return false
|
||||||
|
return true
|
||||||
|
if (a is float or a is int) and (b is float or b is int):
|
||||||
|
return absf(float(a) - float(b)) < 1e-6
|
||||||
|
return a == b
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
for parent in ["LRG", "HRG", "RSG", "RHG"]:
|
||||||
|
for year in [101.0, 101.5, 104.75, 1767.3, 3005.0]:
|
||||||
|
var fast = fixture(false)
|
||||||
|
var legacy = fixture(true)
|
||||||
|
fast.state.amount[parent] = 20000.0
|
||||||
|
fast.state.amount.RD = 100000.0
|
||||||
|
fast.state.amount.HP = 50.0
|
||||||
|
fast.state.amount.Met = 100.0
|
||||||
|
fast.state.binds.RD = {"HP": 2000.0, "C0.1": 100.0}
|
||||||
|
fast.state.binds[parent] = {"HP": 10.0}
|
||||||
|
fast.state.batches[parent] = [{"amount": 20000.0, "start_year": 100.3, "end_year": 3000.7}]
|
||||||
|
# Existing short-lived populations test integration with new arrivals.
|
||||||
|
fast.state.amount.SN = 3.0
|
||||||
|
fast.batches.append_batch(fast.state, "SN", 3.0, 100.5, 100.5)
|
||||||
|
if year == 104.75: fast.state.energy = 7.0
|
||||||
|
legacy.state = fast.state.duplicate(true)
|
||||||
|
fast.evolution.evolve_batches(fast.state, year)
|
||||||
|
legacy.evolution.evolve_batches(legacy.state, year)
|
||||||
|
for field in fast.state:
|
||||||
|
check(equivalent(fast.state[field], legacy.state[field]), "%s at %s: %s matches materialized events" % [parent, year, field])
|
||||||
|
for id in ["SN", "DSN", "BSN", "HN", "GRB", "SGRB", "LGRB", "HGRB"]:
|
||||||
|
check(fast.state.batches.get(id, []).size() <= 3, "Only surviving event windows remain")
|
||||||
|
dispose(fast)
|
||||||
|
dispose(legacy)
|
||||||
|
var fast = fixture(false)
|
||||||
|
var pending: Dictionary = {}
|
||||||
|
fast.batches.append_batch(fast.state, "SN", 10000.0, 0.25, 1667.25, pending, 1667.25)
|
||||||
|
check(fast.state.batches.SN.size() <= 2 and pending.SN.size() == 1, "Completed windows aggregate without transient batch allocation")
|
||||||
|
var total: float = pending.SN[0].amount
|
||||||
|
for batch in fast.state.batches.SN: total += batch.amount
|
||||||
|
check(total == 10000.0, "Direct completions plus survivors conserve event count")
|
||||||
|
dispose(fast)
|
||||||
|
print("Short-lived events: %d checks, %d failures" % [checks, failures])
|
||||||
|
quit(0 if failures == 0 else 1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dp7a88n0mmyb5
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
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.catalog.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.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
|
world.catalog.entities["N"].auto = 0.0
|
||||||
|
world.state.amount.N = 1.0
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.entities.values():
|
||||||
|
entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var world = fixture()
|
||||||
|
for id in ["DSN", "SN", "BSN", "HN", "SGRB", "GRB", "LGRB", "HGRB"]:
|
||||||
|
check(world.stellar.evolution_products(world.state, world.catalog.entities[id], 3.0).blast == 3.0 * world.catalog.entities[id].blast, "Entity blast determines power: " + id)
|
||||||
|
world.catalog.entities.SN.output_and.blast = 999.0
|
||||||
|
world.catalog.entities.SN.blast = 4
|
||||||
|
check(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).blast == 8.0, "Imported output cannot override or double blast power")
|
||||||
|
world.catalog.entities.SN.blast = 0
|
||||||
|
check(not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).has("blast"), "Zero entity blast disables external event")
|
||||||
|
world.catalog.entities.SN.output_or = {"positive": "blast:999", "negative": "blast:999", "chance": 50.0}
|
||||||
|
check(not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).has("blast"), "Alternative output cannot restore disabled blast")
|
||||||
|
dispose(world)
|
||||||
|
world = fixture()
|
||||||
|
var rd = world.catalog.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.catalog.entities[id].blastability, "Blast vulnerable: " + id)
|
||||||
|
for id in ["RP", "GG", "AB", "C1.3", "C1.4", "C1.5"]:
|
||||||
|
check(not world.catalog.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.stellar.evolution_products(world.state, world.catalog.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.stellar.evolution_products(world.state, world.catalog.entities.HRG, 37.0) == restored.stellar.evolution_products(restored.state, restored.catalog.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.batches.append_batch(world.state, "RD", 4.0, 0.0, 0.0)
|
||||||
|
var lifespan: float = rd.lifespan
|
||||||
|
var width: float = rd.lifespan_delta
|
||||||
|
world.evolution.evolve_batches(world.state, lifespan)
|
||||||
|
check(world.state.amount.RD == 4.0, "Window start does not kill the whole batch")
|
||||||
|
world.evolution.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.evolution.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.evolution.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.stellar.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.stellar.create_planets(world.state, "RD", 37.0, 0.0, 0.0, 0.7)
|
||||||
|
restored.stellar.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.batches.append_batch(world.state, "RD", 4.0, 0.0, 0.0)
|
||||||
|
world.evolution.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.evolution.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.batches.append_batch(world.state, "WS", 2.0, 0.0, 0.0)
|
||||||
|
world.evolution.evolve_batches(world.state, world.catalog.entities.WS.lifespan + world.catalog.entities.WS.lifespan_delta)
|
||||||
|
var physical: Dictionary = world.ledger.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.stellar.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.stellar.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.stellar.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.batches.append_batch(world.state, "SN", 1.0, 0.0, 0.0)
|
||||||
|
world.evolution.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.production.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.catalog.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.presentation.amounts(world.state, world.catalog).BD == 10.0 and world.state.batches.HBD.size() > 0, "Brown dwarfs appear in counter and evolution batches")
|
||||||
|
world.catalog.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.catalog.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.catalog.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.evolution.evolve_batches(world.state, 101.0)
|
||||||
|
check(world.state.amount.HBD == 0.0 and world.state.amount.CBD == 10.0 and world.presentation.amounts(world.state, world.catalog).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.automatic.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.automatic.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.automatic.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.catalog.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.evolution.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,92 @@
|
|||||||
|
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.catalog.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.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
|
world.catalog.entities["N"].auto = 0.0
|
||||||
|
world.state.amount.N = 1.0
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.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
|
||||||
|
var speed_limit: int = root.get_node("Global").SPEED_LIMIT
|
||||||
|
check(world.speed == speed_limit and world.state.speed_in_year == pow(10.0, speed_limit - 1), "Configured upper speed limit")
|
||||||
|
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.catalog.entities.N.output_and = {"He": 1.0}
|
||||||
|
world.catalog.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,60 @@
|
|||||||
|
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.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog)
|
||||||
|
stage.set_owniverse_item_value()
|
||||||
|
var hydrogen = stage.owniverse_items.H
|
||||||
|
var dwarf = stage.owniverse_items.RD
|
||||||
|
check(hydrogen.description.text == "1 000", "Pending quantities reach controls")
|
||||||
|
check(dwarf.description.text == "2", "Other page receives its current quantity")
|
||||||
|
check(stage.owniverse.presentation.pending_amounts.is_empty(), "Published queue is consumed by Stage")
|
||||||
|
stage.owniverse.presentation.deferred_updates = true
|
||||||
|
stage.owniverse.state.amount.H = 2000.0
|
||||||
|
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog)
|
||||||
|
check(stage.owniverse.presentation.dirty and stage.owniverse.presentation.pending_amounts.is_empty(), "Deferred refresh marks dirty without republishing")
|
||||||
|
stage.set_owniverse_item_value()
|
||||||
|
check(hydrogen.description.text == "1 000", "Labels retain last published quantity")
|
||||||
|
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog, true)
|
||||||
|
stage.set_owniverse_item_value()
|
||||||
|
check(hydrogen.description.text == "2 000" and not stage.owniverse.presentation.dirty, "Forced refresh publishes latest state")
|
||||||
|
stage.owniverse.presentation.deferred_updates = false
|
||||||
|
stage.item_activated(hydrogen)
|
||||||
|
stage.set_owniverse_item_value()
|
||||||
|
check(hydrogen.description.text == root.get_node("Global").format_number(stage.owniverse.state.amount.H), "Player production publishes the new quantity")
|
||||||
|
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._refresh_universe_ui()
|
||||||
|
check(dwarf.description.text == "2", "Newly visible page receives cached latest values")
|
||||||
|
stage.owniverse.state.time.year = 1234.5
|
||||||
|
stage._refresh_universe_ui()
|
||||||
|
check(stage.date_time.text == "1 234.5", "Time displays tenths with thousands separator")
|
||||||
|
for entity in stage.owniverse.catalog.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
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||||
|
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
|
||||||
|
|
||||||
|
const Parameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
|
const Descriptions = preload("res://world/owniverse/item_description.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()
|
||||||
|
for id in root.get_node("Global").OwniverseEntities:
|
||||||
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
|
||||||
|
world.catalog.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
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var global: Node = root.get_node("Global")
|
||||||
|
var world = fixture()
|
||||||
|
world.state.amount.RD = 400.0
|
||||||
|
world.batches.append_batch(world.state, "RD", 400.0, 0.0, 0.0)
|
||||||
|
var end: float = world.state.batches.RD[0].end_year
|
||||||
|
world.environment.modify_world_parameter("Bur", 9.0)
|
||||||
|
check(is_equal_approx(world.state.batches.RD[0].end_year, end / 10.0), "Burnability accelerates existing cohorts")
|
||||||
|
var count: float = world.state.batches.RD[0].amount
|
||||||
|
world.environment.modify_world_parameter("Bur", -9.0)
|
||||||
|
check(is_equal_approx(world.state.batches.RD[0].end_year, end) and world.state.batches.RD[0].amount == count, "Changing burnability back preserves population and lifetime")
|
||||||
|
world.environment.modify_world_parameter("Bur", 9.0)
|
||||||
|
world.batches.append_batch(world.state, "YD", 10.0, 0.0, 0.0)
|
||||||
|
check(is_equal_approx(world.state.batches.YD[0].start_year, world.catalog.entities.YD.lifespan / 10.0), "New stars use the same burnability")
|
||||||
|
world.state.time = {"datetime": end / 10.0 - 0.1, "year": end / 10.0 - 0.1}
|
||||||
|
world.batches.withdraw_batches(world.state, "RD", world.state.time.year)
|
||||||
|
world.environment.modify_world_parameter("Bur", -18.0)
|
||||||
|
check(world.state.batches.RD[0].end_year >= world.state.time.year, "Slowing a partially evolved cohort keeps its remaining window in the future")
|
||||||
|
world.environment.modify_world_parameter("Exp", 9.0)
|
||||||
|
check(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).blast == 180.0, "Explosion power includes world explosiveness")
|
||||||
|
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1000.0)
|
||||||
|
check(products.get("GRB", 0.0) == 100.0 and products.get("SN", 0.0) == 900.0, "Explosiveness raises rare-event probability without duplicating stars")
|
||||||
|
world.catalog.entities.HRG.output_or.chance = 50.0
|
||||||
|
products = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 100.0)
|
||||||
|
check(products.get("GRB", 0.0) == 100.0 and products.get("SN", 0.0) == 0.0, "Rare-event probability is bounded at one")
|
||||||
|
dispose(world)
|
||||||
|
|
||||||
|
world = fixture()
|
||||||
|
var ordinary: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
|
||||||
|
world.state.life_remainders.clear()
|
||||||
|
world.environment.modify_world_parameter("Agg", 9.0)
|
||||||
|
var aggressive: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
|
||||||
|
check(aggressive.EON1 > ordinary.EON1, "Aggression accelerates primitive evolution")
|
||||||
|
world.state.life_remainders.clear()
|
||||||
|
world.state.world_parameters.Order = 10.0
|
||||||
|
var orderly: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
|
||||||
|
check(orderly.RP < aggressive.RP, "Order reduces degradation")
|
||||||
|
world.state.life_remainders.clear()
|
||||||
|
world.state.world_parameters.Enlightenment = 10.0
|
||||||
|
var enlightened: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
|
||||||
|
check(enlightened.EON1 > orderly.EON1, "Enlightenment increases primitive progress")
|
||||||
|
dispose(world)
|
||||||
|
|
||||||
|
world = fixture()
|
||||||
|
var hotter = fixture()
|
||||||
|
for simulation in [world, hotter]:
|
||||||
|
simulation.state.amount.RD = 1000000.0
|
||||||
|
simulation.batches.append_batch(simulation.state, "RD", 1000000.0, 0.0, 0.0)
|
||||||
|
hotter.environment.modify_world_parameter("Bur", 9.0)
|
||||||
|
world.evolute(1000.0)
|
||||||
|
hotter.evolute(1000.0)
|
||||||
|
check(hotter.state.amount.S > world.state.amount.S, "Burnability changes actual space production")
|
||||||
|
check(hotter.state.energy + hotter.state.amount.S == Parameters.INITIAL_ENERGY, "Enhanced production still spends the GI energy budget")
|
||||||
|
dispose(world)
|
||||||
|
dispose(hotter)
|
||||||
|
|
||||||
|
world = fixture()
|
||||||
|
world.state.amount["C0.2"] = 1000000.0
|
||||||
|
world.state.ethics["C0.2"] = {"m": 1000000.0}
|
||||||
|
var baseline: Dictionary = world.state.duplicate(true)
|
||||||
|
var advanced: Dictionary = baseline.duplicate(true)
|
||||||
|
advanced.world_parameters.Agg = 19.0
|
||||||
|
var next_stage: String = global.CivilizationRules["C0.2"].next
|
||||||
|
var outcomes: Array = []
|
||||||
|
for target in [baseline, advanced]:
|
||||||
|
var expired: Dictionary = {"C0.2": [{"amount": 1000000.0, "from": 0.0, "to": 1.0}]}
|
||||||
|
var budget: Dictionary = CivilizationState.stellar_budget(world, target, global.CivilizationRules)
|
||||||
|
CivilizationEvolution.prepare(world, target, expired, 1.0, budget, global.CivilizationRules)
|
||||||
|
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0.0))
|
||||||
|
check(outcomes[1] > outcomes[0], "Aggression changes actual ethical civilization transitions")
|
||||||
|
dispose(world)
|
||||||
|
|
||||||
|
# Trends use ethical proportions and stage weights, not population magnitude.
|
||||||
|
world = fixture()
|
||||||
|
var rules: Dictionary = {"C2.0": {"home": "DwStar"}, "C2.1": {"home": "DwStar"}}
|
||||||
|
world.state.ethics = {"C2.0": {"d": 100.0}, "C2.1": {"d": 100.0}}
|
||||||
|
var source: Dictionary = world.state.duplicate(true)
|
||||||
|
var single: Dictionary = source.duplicate(true)
|
||||||
|
var split: Dictionary = source.duplicate(true)
|
||||||
|
Parameters.advance(source, single, rules, 1.0e6)
|
||||||
|
for i in 10: Parameters.advance(source, split, rules, 1.0e5)
|
||||||
|
check(is_equal_approx(single.trends.Integration, 0.018), "Trend integrates elapsed years and doubled stage weights")
|
||||||
|
for key in Parameters.TRENDS:
|
||||||
|
check(is_equal_approx(single.trends[key], split.trends[key]), "Trend is independent of frame partition: " + key)
|
||||||
|
source.ethics["C2.0"].d = 1000000.0
|
||||||
|
var larger: Dictionary = world.state.duplicate(true)
|
||||||
|
Parameters.advance(source, larger, rules, 1.0e6)
|
||||||
|
check(is_equal_approx(larger.trends.Integration, single.trends.Integration), "Population proportions determine trend influence")
|
||||||
|
world.state.trends.Integration = 0.99
|
||||||
|
Parameters.advance(source, world.state, rules, 1.0e8)
|
||||||
|
check(world.state.civilization_stats["trend:Integration"] == 2.0, "Long steps process every crossed threshold")
|
||||||
|
check(is_equal_approx(world.state.trends.Integration, 0.79), "Threshold crossing preserves the fractional remainder")
|
||||||
|
check(world.state.world_parameters.Federation > 1.0 or world.state.world_parameters.Enlightenment > 1.0 or world.state.world_parameters.Order > 1.0, "Integration events change the world")
|
||||||
|
var restored: Dictionary = bytes_to_var(var_to_bytes(world.state))
|
||||||
|
Parameters.advance(source, world.state, rules, 1.0e8)
|
||||||
|
Parameters.advance(source, restored, rules, 1.0e8)
|
||||||
|
check(world.state == restored, "Trend events resume identically after serialization")
|
||||||
|
Parameters.advance(source, world.state, rules, 1.0e12)
|
||||||
|
check(world.state.trends.Integration >= 0.0 and world.state.trends.Integration < 1.0, "Very large intervals finish with a valid trend fraction")
|
||||||
|
check(world.state.world_parameters.Agg >= 1.0, "Repeated integration events cannot reduce aggression below the scale")
|
||||||
|
var payload: Dictionary = {"format_version": 2, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
|
||||||
|
var manager: Node = root.get_node("SaveManager")
|
||||||
|
check(manager.validate_data(payload) == "", "New parameters and trends pass save validation")
|
||||||
|
var old: Dictionary = payload.duplicate(true)
|
||||||
|
old.state.erase("world_parameters")
|
||||||
|
old.state.erase("trends")
|
||||||
|
check(manager.validate_data(old) == "", "Existing current-format saves remain readable")
|
||||||
|
payload.state.world_parameters.Agg = 20.0
|
||||||
|
check(manager.validate_data(payload) != "", "Out-of-range scales are rejected")
|
||||||
|
dispose(world)
|
||||||
|
|
||||||
|
for sample in [[1e-7, 0.0], [1e-6, 0.5], [1e-5, 1.0], [0.9, 0.0], [0.95, 0.5], [1.0, 1.0]]:
|
||||||
|
check(is_equal_approx(Parameters.density_display(sample[0]).position, sample[1]), "Density boundary " + str(sample[0]))
|
||||||
|
# The fixture overrides saving/loading; the player's save is never accessed.
|
||||||
|
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.owniverse.state.energy = Parameters.INITIAL_ENERGY / 4.0
|
||||||
|
stage.owniverse.state.world_parameters.Bur = 19.0
|
||||||
|
stage.owniverse.state.trends.Integration = 0.75
|
||||||
|
stage.world_indicators.update(stage.owniverse)
|
||||||
|
var warning: TextureRect = scene.get_node("Background/WarningBorder")
|
||||||
|
check(warning.mouse_filter == Control.MOUSE_FILTER_IGNORE, "Warning overlay does not block input")
|
||||||
|
var environments: Array = [
|
||||||
|
[1.0e-7, "ff0000ff"], [1.0e-6, "df2020bf"], [1.0e-5, "c040407f"],
|
||||||
|
[0.1, "00000000"], [0.9, "40a0c07f"], [0.97, "20b0dfbf"], [1.0, "00c0ffff"],
|
||||||
|
[0.1, "00000000"],
|
||||||
|
]
|
||||||
|
for sample in environments:
|
||||||
|
check(Parameters.density_warning_color(sample[0]).is_equal_approx(Color.html(sample[1])), "Unity warning threshold: " + str(sample[0]))
|
||||||
|
stage.owniverse.state.amount.H = 1e14
|
||||||
|
stage.world_indicators.update(stage.owniverse)
|
||||||
|
check(warning.self_modulate.is_equal_approx(Color.html("00c0ffff")), "Compression warning uses current universe density")
|
||||||
|
stage.owniverse.state.amount.H = 0.0
|
||||||
|
stage.owniverse.state.amount.S = 1e24
|
||||||
|
stage.world_indicators.update(stage.owniverse)
|
||||||
|
check(warning.self_modulate.is_equal_approx(Color.html("ff0000ff")), "Rip warning replaces compression warning")
|
||||||
|
stage.owniverse.state.amount.S = 0.0
|
||||||
|
stage.world_indicators.update(stage.owniverse)
|
||||||
|
check(warning.self_modulate.a == 0.0, "Warning clears when density returns to normal")
|
||||||
|
for bar in stage.world_indicators.energy_bars:
|
||||||
|
check(bar.scale.x == 0.25, "Energy fill is synchronized on both pages")
|
||||||
|
var pointer: Control = stage.world_indicators.pointers.Bur
|
||||||
|
check(is_equal_approx(pointer.position.x + pointer.size.x / 2.0, 304.0), "Burnability pointer reaches the correct scale endpoint")
|
||||||
|
check(stage.world_indicators.trend_bars[1].scale.y == 0.75, "Trend bar displays its stored level")
|
||||||
|
for locale in ["en", "ru"]:
|
||||||
|
global.Locale = locale
|
||||||
|
for id in ["Energy", "Density", "Burnability", "Explosiveness", "Aggression", "Trends"]:
|
||||||
|
var description: String = Descriptions.render(stage.owniverse, global, id, "Indicator")
|
||||||
|
check(not description.contains("—") and not description.contains("%value"), "Resolved indicator description: " + locale + "/" + id)
|
||||||
|
dispose(stage.owniverse)
|
||||||
|
scene.free()
|
||||||
|
print("World indicators: %d checks, %d failures" % [checks, failures])
|
||||||
|
quit(0 if failures == 0 else 1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dmldg06dx3utl
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user