62 Commits

Author SHA1 Message Date
Kirill c2313e9163 Enhance item visibility logic and update initial energy parameter; add tests for life icon functionality 2026-09-28 22:56:40 +03:00
Kirill 2f0a3ee8d4 Warming border added 2026-09-28 22:32:52 +03:00
Kirill 15ee7833ed Add world indicators and parameters to enhance universe simulation
- new universe creation function.
2026-09-28 22:19:13 +03:00
Kirill e79f3fa362 Fix highlight visibility toggle and enhance color modulation logic 2026-09-22 23:54:13 +03:00
Kirill e87d4083e9 Refactor code structure for improved readability and maintainability 2026-09-22 23:47:59 +03:00
Kirill 5a18b41528 Refactor code structure for improved readability and maintainability 2026-09-22 23:10:09 +03:00
Kirill d6fd40969f Implement short-lived stellar event handling and optimize batch processing; add tests for new functionality 2026-09-22 22:50:54 +03:00
Kirill a3d24114a1 Add screen management to top menu and update button functionality; replace close button with back button 2026-09-21 22:32:56 +03:00
Kirill 8867da569a Refactor code structure and remove redundant sections for improved readability and maintainability 2026-09-21 21:46:43 +03:00
Kirill 1796829338 Update speed limit, enable embedded PCK format, and enhance brown dwarf production logic 2026-09-21 20:42:40 +03:00
Kirill f246efd6ad Add unit tests for action value formatting and distribution
- Implemented a new test script `test_action_values.gd` to verify the precision of fractional action amounts and the distinctiveness of adjacent values.
- Added checks to ensure that action values are correctly formatted and that the distribution of actions preserves distinct requests.
- Included validation for rendered button labels to confirm they are distinguishable.
2026-09-20 23:55:38 +03:00
Kirill f25b77b605 Implement asynchronous save manager with UI update optimizations and add tests for save functionality 2026-09-20 22:14:47 +03:00
Kirill a09e3df220 Refactor save manager validation and update state structure
- Updated the save manager to enforce a new format version (2) and improved validation checks for the save data structure.
- Enhanced the `validate_data` function to ensure all required fields are present and valid.
- Adjusted the handling of legacy saves and introduced new checks for various state fields.
- Improved number formatting in the global script for better readability of large numbers.
- Updated tests to reflect changes in the save manager and validation logic, ensuring compatibility with the new state structure.
- Added new tests for group indexing and production transactions to ensure correctness of entity management and resource production.
2026-09-20 20:56:15 +03:00
Kirill 26347fca30 Add comprehensive civilization evolution tests and enhance civilization state management
- Introduced a new test script for civilization evolution (`test_civilization_evolution.gd`) to validate the evolution mechanics, including population dynamics, ethical transitions, and war impacts.
- Updated `README.md` to include instructions for running the new civilization evolution tests.
- Modified `test_civilization_state.gd` to accommodate changes in civilization rules and ensure proper validation of ethical stages.
- Enhanced `test_descriptions.gd` to check for accurate population counters and state validation.
- Adjusted `test_life.gd` to reflect updates in life version management.
- Improved `civilization_state.gd` to include habitat budget calculations and transition management.
- Added `civilization_evolution.gd` to handle the logic for civilization evolution, including population drift, warfare, and colonization mechanics.
- Updated `owniverse.gd` and `owniverse_entity.gd` to integrate new evolution logic and ensure proper initialization of entities.
2026-09-16 19:54:47 +03:00
Kirill 5ea65a7e9b Update export presets, project version, and enhance civilization simulation rules
- Updated export path and version code in export_presets.cfg
- Changed project version in project.godot to 0.7.0
- Expanded simulation rules in simulation_rules.md to clarify resource management
- Improved testing framework in tests/README.md to include new civilization budget checks
- Added new tests for batch boundaries and civilization state transitions
- Enhanced civilization_state.gd with shared budget and transition logic
- Refactored owniverse.gd to enforce capacity during life batch initialization
2026-09-16 07:58:19 +03:00
Kirill 5f3875bc14 Add life rules and implement tests for civilization and life states
- Introduced a new file `life_rules.txt` to define life progression and degradation rules.
- Created `test_civilization_state.gd` to validate the functionality of civilization rules and ethics.
- Added `test_life.gd` to test life evolution, including initialization, batch processing, and state validation.
- Implemented `civilization_state.gd` to manage civilization ethics and space civilization detachment logic.
- Ensured all tests check for expected outcomes and validate state integrity after operations.
2026-09-15 23:05:05 +03:00
Kirill cc2700d507 Add item description and testing framework for Owniverse
- Introduced `item_description.gd` to handle item descriptions with localization support.
- Implemented a testing script `test_descriptions.gd` to validate item description rendering and localization.
- Added a new template file for Russian localization.
- Included a large package file `owniverse.0.6.29-145.pck` for game assets.
- Ensured proper memory management in tests with fixture and dispose functions.
2026-09-15 22:01:45 +03:00
Kirill 5f5cc6f894 Implement gradual evolution mechanics and simulation time handling in Owniverse.GI
- Added `blastability` property to entities for vulnerability tracking.
- Enhanced planet string parsing with validation and error handling.
- Introduced `simulation_rules.md` to document evolution mechanics and rules.
- Created tests for time progression and stellar evolution rules, ensuring correct handling of fractional years and resource production.
- Implemented `simulation_time.gd` for managing accumulated time and year processing.
- Updated tests to validate legacy save compatibility and migration to new time schema.
2026-09-15 08:27:04 +03:00
Kirill 7de65f6145 Refactor workflow and configuration files; enhance Android build process with import options, update .gitignore for entity imports, correct entity file names, and add missing import files for localization and entities. 2026-09-15 07:30:01 +03:00
Kirill 85a13eb241 Refactor save management; update validation logic in save_manager.gd and enhance action handling in actions.gd for improved data integrity and user experience 2026-09-14 07:47:21 +03:00
Kirill dea744bc30 Refactor save management; enhance save_data function with verification and plaintext copy options, and update load_data logic for improved error handling 2026-09-13 22:57:25 +03:00
Kirill c22b21a537 Refactor global.gd to update SPEED_LIMIT constant from 4 to 5
Enhance stage.gd debug output in _on_topmenu_button_pressed function for better state tracking; comment out unnecessary print statements.

Update owniverse.gd to introduce new state properties: batch_origins, distribution, and auto_resources; refactor evolute function for improved state management and batch processing.

Revise state.gd to clean up debug print statements in add function for better readability and maintainability.
2026-09-13 21:09:42 +03:00
Kirill b44f92aacf Refactor state structure; encapsulate datetime and year within a time dictionary and update related logic 2026-09-11 09:31:37 +03:00
Kirill 0162084e53 Refactor state management; update unlock logic and enhance signal bindings in scene 2026-09-01 23:53:05 +03:00
Kirill 2f4b6a98d8 Refactor production logic to use state amounts; update calculate_max_producible function to accept default desired amount 2026-09-01 21:29:07 +03:00
Kirill d90f8693c9 Update Godot core rules to version 4.7; refactor production logic and add entity batch processing 2026-08-31 23:51:07 +03:00
Kirill e870c96879 Add 'binds' key to state dictionary for enhanced state management 2026-08-30 22:31:24 +03:00
Kirill e531e7d9b6 Refactor item management and unlock logic; remove unused item update list and signal 2026-08-28 00:07:18 +03:00
Kirill 57cc023558 Implement item unlocking mechanism and refactor save functionality; update scene timer node 2026-08-27 23:04:56 +03:00
Kirill 23929e3651 Refactor owniverse state management; replace evolution class with state handling and update cycle logic 2026-08-25 00:26:31 +03:00
Kirill 829aadca2d Update version to 0.6.27 and increment version code to 143; refactor resource paths in scene files
Android / android (push) Successful in 30s
2026-08-20 22:58:24 +03:00
Kirill 30486616f8 Update version to 0.6.26 and increment version code to 142
Android / android (push) Successful in 30s
2026-08-20 22:55:01 +03:00
Kirill d9a2679af4 Replace pressed burger button image and update resource path in world.tscn 2026-08-20 22:54:42 +03:00
Kirill 4f57c37572 Update version to 0.6.25 and increment version code to 141; replace burger button image
Android / android (push) Successful in 30s
2026-08-20 22:45:22 +03:00
Kirill e4a0dd2d13 Add new spirituality trend indicator image
Android / android (push) Successful in 30s
2026-08-20 22:29:11 +03:00
Kirill 0a10cff0fa Refactor code structure for improved readability and maintainability 2026-08-20 22:28:03 +03:00
Kirill e4cf5bdad0 Add new button images for time adjustment in the top menu 2026-08-20 22:27:36 +03:00
Kirill 645751cd23 Add new button images for time adjustment in the top menu 2026-08-20 22:26:57 +03:00
Kirill b92a49821d Update version to 0.6.23 and increment version code to 139; add unique IDs to UI nodes
Android / android (push) Successful in 31s
2026-08-20 22:19:19 +03:00
Kirill 3a91528e95 Bump version to 0.6.22 and increment version code to 138
Android / android (push) Successful in 30s
2026-08-20 22:11:09 +03:00
Kirill 3c37be2a8e Add new PNG assets for time and speed UI components 2026-08-20 22:10:40 +03:00
Kirill 544b5f4f56 Add new time speed icons for UI 2026-08-20 22:10:22 +03:00
Kirill cc9de54d42 Bump version to 0.6.21 and increment version code to 137
Android / android (push) Successful in 31s
2026-08-20 21:55:23 +03:00
Kirill e76414ce17 Refactor code structure for improved readability and maintainability 2026-08-20 21:54:45 +03:00
Kirill 68c2b4f25d Refactor code structure for improved readability and maintainability 2026-08-20 21:54:32 +03:00
Kirill 78946f1bb4 Bump version to 0.6.20 and increment version code to 136
Android / android (push) Successful in 32s
2026-08-20 21:41:23 +03:00
Kirill 0d1671fe8f Update project icon reference in configuration file 2026-08-20 21:40:48 +03:00
Kirill ea35017aa0 Bump version to 0.6.19 and increment version code to 135
Android / android (push) Successful in 29s
2026-08-20 21:20:08 +03:00
Kirill 0db24fec60 Refactor background and starry sky layer scripts; move to new directory structure 2026-08-20 21:19:47 +03:00
Kirill b2a18c9c41 Refactor code structure for improved readability and maintainability 2026-08-20 21:19:30 +03:00
Kirill 16ec80929c Bump version to 0.6.18 and increment version code to 134; update Android workflow configuration
Android / android (push) Successful in 31s
2026-08-20 21:11:46 +03:00
Kirill 3536e25d52 Update version to 0.6.17 and increment version code to 133; enhance Android build workflow
Android / android (push) Successful in 27s
2026-08-20 21:05:22 +03:00
Kirill 935d249183 Bump version to 0.6.16 and update version code to 132
Android / android (push) Successful in 22s
2026-08-20 20:25:12 +03:00
Kirill 8cb01d77de Refactor Android build workflow to separate preparation and build steps 2026-08-20 20:24:30 +03:00
Kirill a446b7cb41 Update Global autoload reference in project configuration 2026-08-20 20:19:01 +03:00
Kirill 82681d27e1 Bump version to 0.6.15 and update version code to 131
Android / android (push) Failing after 17s
2026-08-20 20:11:39 +03:00
Kirill 777ec2907b Add new PNG assets for time and speed UI components 2026-08-20 20:09:14 +03:00
Kirill 8513bf0313 Add new PNG images for time and speed menu 2026-08-20 20:03:18 +03:00
Kirill a4019ed98c Add global.gd script with localization and CSV reading functionality 2026-08-20 19:55:20 +03:00
Kirill 4731489015 Refactor global scripts: remove Global.gd, add globals.gd with enhanced functionality 2026-08-20 19:54:50 +03:00
Kirill 711ace6678 Refactor autoload structure by moving Global and SaveManager scripts to the autoload directory 2026-08-20 19:53:49 +03:00
Kirill 89342dc614 Add Global and SaveManager scripts with localization and save/load functionality 2026-08-20 19:53:32 +03:00
371 changed files with 9165 additions and 1087 deletions
+9 -3
View File
@@ -1,13 +1,19 @@
---
name: Godot 4.4 Core Rules
name: Godot 4.7 Core Rules
priority: high
---
Ты — senior Godot 4.4 GDScript разработчик.
Ты — senior Godot 4.7 GDScript разработчик.
- Use Godot 4.7+ API.
- Do not use Godot 3.x syntax or APIs.
- Follow the existing project architecture and coding style.
- Do not introduce unnecessary abstractions.
- Do not modify unrelated files.
- Всегда используй Godot 4.4+ API.
- Пиши строго типизированный код (`var health: int = 100`, `: Array[Node]` и т.д.).
- Предпочитай Signals вместо проверки в _process.
- Используй @export, @export_range, @export_enum, @onready.
- Composition over deep inheritance.
- Менеджеры и автолоады выноси в отдельные singleton'ы.
+1
View File
@@ -0,0 +1 @@
- use spaces not tabs for indentation
+34 -2
View File
@@ -12,20 +12,52 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: true
- name: Godot version
run: godot --version
- name: Prepare Godot
run: |
rm -rf .godot
godot \
--headless \
--path . \
--editor \
--import \
--quit \
--verbose
- name: Build APK
run: |
mkdir -p build
godot \
--headless \
--path . \
--editor \
--import \
--quit
godot \
--headless \
--path . \
--export-release "Android" \
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
run: |
test -f build/app.apk
@@ -59,4 +91,4 @@ jobs:
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-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"
+1
View File
@@ -3,6 +3,7 @@
.vscode/
/android/
*.import
!data/*.txt.import
# MacOS specific ignores
.DS_Store
-114
View File
@@ -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])
-56
View File
@@ -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
View File
@@ -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()
-6
View File
@@ -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")
-46
View File
@@ -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])
+165
View File
@@ -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: String = ""
if integer.begins_with("-") or integer.begins_with("+"):
sign = integer.left(1)
integer = integer.substr(1)
var grouped: String = ""
while integer.length() > 3:
grouped = " " + integer.right(3) + grouped
integer = integer.left(integer.length() - 3)
grouped = sign + integer + grouped
return grouped + ("." + parts[1] if parts.size() > 1 else "")
var exp_value = int(floor(log(abs(value)) / log(10.0)))
var mantissa = value / pow(10.0, exp_value)
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])
+338
View File
@@ -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
+259
View File
@@ -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
+9
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
[remap]
importer="keep"
+3
View File
@@ -0,0 +1,3 @@
[remap]
importer="keep"
@@ -1,6 +1,6 @@
id|is_visible|unlock|cost|bind|mass|space|lifespan|auto|gamma|blast|life_product|voids|output|destroy_level|degradation|planets|count_as|group_as|event_group|blastability|space_bonus|civ_bonus|claim
S|1|S:0|S:0||||||||||||||||S||||
H|1|H:0|H:0||||||||||||||||H||||
S|1|S:0|E:1||||||||||||||||S||||
H|1|H:0|E:1||||||||||||||||H||||
He|1|He:1|||||||||||||||||||||
Met|1|Met:1|||||||||||||||||||||
BD|1|BD:1|||||||||||||||||||||
@@ -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|||||
RSG|0||||300||1000000||||S:500||BSN^LGRB%3|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||||
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||||
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||||
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||||
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||||
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||||
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||||
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||||
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|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|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|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|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|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|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|2|mass:60&H:0&HE:40&MET:10||||SNGRBB||||
CBD|0||||0.1||||||||||||BD||||||
WD|1|WD:1|||||||||||||||||||||
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||||
ASTN|1|MC:1&S:60000|H:200000&MC:1||202011|200000|100000000|0.65|||H:2000000||H:202011&S:204020|||||CL|CL||||
SCD|1|Star:300||Star:1000|||10000000|||||||||||SC|SC||0.1|4|C2.3
SCS|1|SCD:1&Star:2000||Star:10000&BH:10|||1000000000|||||||||||SC|SC||0.2|44350|C2.4
SCM|1|SCS:1&Star:20000||Star:100000&BH:100|||3000000000|||||||||||SC|SC||0.3|44289|C2.5
SCL|1|SCM:1&Star:200000||Star:1000000&BH:1000|||10000000000|||||||||||SC|SC||0.5|44230|C2.6
SCS|1|SCD:1&Star:2000||Star:10000&BH:10|||1000000000|||||||||||SC|SC||0.2|3.6|C2.4
SCM|1|SCS:1&Star:20000||Star:100000&BH:100|||3000000000|||||||||||SC|SC||0.3|3.4|C2.5
SCL|1|SCM:1&Star:200000||Star:1000000&BH:1000|||10000000000|||||||||||SC|SC||0.5|3.2|C2.6
SCG|1|SCL:1&Star:2000000||Star:10000000&BH:10000|||30000000000|||||||||||SC|SC||0.8|3|C2.7
GED|1|SMBH:1&SC:1|SMBH:10000|Star:1000000000&BH:1000000&SC:100|||12000000000|||||||||||GAL|GALE||1|44440|C2.9
GES|1|GED:1&Star:2000000000|SMBH:100000|Star:10000000000&BH:10000000&SC:1000|||13000000000|||||||||||GAL|GALE||1.5|44409|C3.0
GEM|1|GES:1&Star:20000000000|SMBH:1000000|Star:100000000000&BH:100000000&SC:10000|||15000000000|||||||||||GAL|GALE||2|44378|C3.1
GEL|1|GEM:1&Star:200000000000|SMBH:10000000|Star:1000000000000&BH:1000000000&SC:100000|||17000000000|||||||||||GAL|GALE||3|44348|C3.2
GEG|1|GEL:1&Star:2000000000000|SMBH:100000000|Star:10000000000000&BH:10000000000&SC:1000000|||20000000000|||||||||||GAL|GALE||4.5|44317|C3.3
GSD|1|GED:1|STN:10000&ASTN:10|GED:1||-5240200|2000000000|0.8|||H:2400000000||H:4112110|||||GAL|GALS|||44318|C2.9
GSS|1|GES:1|STN:100000&ASTN:100|GES:1||-52402000|3000000000|0.8|||H:36000000000||H:41121100|||||GAL|GALS|||44257|C3.0
GSM|1|GEM:1|STN:1000000&ASTN:1000|GEM:1||-524020000|4000000000|0.8|||H:480000000000||H:411211000|||||GAL|GALS|||44229|C3.1
GSL|1|GEL:1|STN:10000000&ASTN:10000|GEL:1||-5240200000|6000000000|0.8|||H:7200000000000||H:4112110000|||||GAL|GALS|||44198|C3.2
GED|1|SMBH:1&SC:1|SMBH:10000|Star:1000000000&BH:1000000&SC:100|||12000000000|||||||||||GAL|GALE||1|1.9|C2.9
GES|1|GED:1&Star:2000000000|SMBH:100000|Star:10000000000&BH:10000000&SC:1000|||13000000000|||||||||||GAL|GALE||1.5|1.8|C3.0
GEM|1|GES:1&Star:20000000000|SMBH:1000000|Star:100000000000&BH:100000000&SC:10000|||15000000000|||||||||||GAL|GALE||2|1.7|C3.1
GEL|1|GEM:1&Star:200000000000|SMBH:10000000|Star:1000000000000&BH:1000000000&SC:100000|||17000000000|||||||||||GAL|GALE||3|1.6|C3.2
GEG|1|GEL:1&Star:2000000000000|SMBH:100000000|Star:10000000000000&BH:10000000000&SC:1000000|||20000000000|||||||||||GAL|GALE||4.5|1.5|C3.3
GSD|1|GED:1|STN:10000&ASTN:10|GED:1||-5240200|2000000000|0.8|||H:2400000000||H:4112110|||||GAL|GALS|||2.5|C2.9
GSS|1|GES:1|STN:100000&ASTN:100|GES:1||-52402000|3000000000|0.8|||H:36000000000||H:41121100|||||GAL|GALS|||2.3|C3.0
GSM|1|GEM:1|STN:1000000&ASTN:1000|GEM:1||-524020000|4000000000|0.8|||H:480000000000||H:411211000|||||GAL|GALS|||2.2|C3.1
GSL|1|GEL:1|STN:10000000&ASTN:10000|GEL:1||-5240200000|6000000000|0.8|||H:7200000000000||H:4112110000|||||GAL|GALS|||2.1|C3.2
GSG|1|GEG:1|STN:100000000&ASTN:100000|GEG:1||-52402000000|9000000000|0.8|||H:108000000000000||H:41121100000|||||GAL|GALS|||2|C3.3
LGG|1|GAL:20||GAL:50|||10000000000|||||||||||LS|LS|||44317|C3.4
LSC|1|LGG:1&GAL:100||LGG:200|||20000000000|||||||||||LS|LS|||44287|C3.6
LGF|1|LSC:1&LGG:400||LSC:100|||30000000000|||||5||||||LS|LS|||44256|C3.8
LGW|1|LGF:1&LSC:200||LGF:1000|||60000000000|||||10000||||||LS|LS|||44228|C4.1
LUB|1|LGW:1&LGF:2000||LGW:1000|||100000000000|||||1000000000||||||LS|LS|||44197|C4.4
LGG|1|GAL:20||GAL:50|||10000000000|||||||||||LS|LS|||1.5|C3.4
LSC|1|LGG:1&GAL:100||LGG:200|||20000000000|||||||||||LS|LS|||1.4|C3.6
LGF|1|LSC:1&LGG:400||LSC:100|||30000000000|||||5||||||LS|LS|||1.3|C3.8
LGW|1|LGF:1&LSC:200||LGF:1000|||60000000000|||||10000||||||LS|LS|||1.2|C4.1
LUB|1|LGW:1&LGF:2000||LGW:1000|||100000000000|||||1000000000||||||LS|LS|||1.1|C4.4
GG|1|GG:1||||||||||||||||Planet|BPlanet||||
AB|1|AB:1||||||||||||||||Planet|BPlanet||||
RP|1|RP:1||||||||||||||||Planet|BPlanet||||
+3
View File
@@ -0,0 +1,3 @@
[remap]
importer="keep"
+3
View File
@@ -0,0 +1,3 @@
[remap]
importer="keep"
+2 -2
View File
@@ -725,7 +725,7 @@ Aggression.Description|Evolution and competition.[p]%value0%: %value1%|scale|amo
Trends.Description|Actions of advanced civilizations.[p]Antitheism: %value0%[p]Integration: %value1%[p]Xenocide: %value2%[p]Reconstruction: %value3%[p]Spirituality: %value4%[p]|antitheism|integration|xenocide|reconstruction|spirituality
S.Description|%value0%[p]Voids: %value1%[p]Needed for placing objects.|amount|voids|||
H.Description|%value0%[p]Used to create clouds and stars.|amount||||
He.Description|%value0%[p]Saves hydrogen.%value1%|amount he / h||||
He.Description|%value0%[p]Saves hydrogen.%value1%|amount|he / h|||
Met.Description|%value0%[p]Needed to create planets.[p]Metallicity: %value1%|amount|metallicity|||
N.Description|%value0%[p]Produces hydrogen.[p]%value2%[p]Baseline lifespan: %value3%|amount||colored costs|lifespan|
G.Description|%value0%[p]Produces more hydrogen.[p]%value2%[p]Baseline lifespan: %value3%|amount||colored costs|lifespan|
@@ -1183,4 +1183,4 @@ Type1Civilizations.Path|GameHelp/en/Type1Civilizations|||||
Type2Civilizations.Path|GameHelp/en/Type2Civilizations|||||
Type3Civilizations.Path|GameHelp/en/Type3Civilizations|||||
Type4Civilizations.Path|GameHelp/en/Type4Civilizations|||||
ButtonAbout.Image|Path/To/en/ButtonAbout.webp|||||
ButtonAbout.Image|Path/To/en/ButtonAbout.webp|||||
+3
View File
@@ -0,0 +1,3 @@
[remap]
importer="keep"
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
[remap]
importer="skip"
+14 -8
View File
@@ -1,14 +1,17 @@
[runnable_presets]
"Windows Desktop"="Windows Desktop"
[preset.0]
name="Windows Desktop"
platform="Windows Desktop"
runnable=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../../Documents/Owad/Owniverse.I/Builds/owniverse.0.6.13-129.exe"
export_path="../../../Documents/Owad/Owniverse.I/Builds/owniverse.0.7.0-146.console.exe"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
@@ -27,7 +30,7 @@ script_export_mode=2
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=false
binary_format/embed_pck=true
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
shader_baker/enabled=false
@@ -74,7 +77,6 @@ Remove-Item -Recurse -Force '{temp_dir}'"
name="Android"
platform="Android"
runnable=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
@@ -110,7 +112,7 @@ architectures/armeabi-v7a=false
architectures/arm64-v8a=true
architectures/x86=false
architectures/x86_64=false
version/code=130
version/code=146
version/name=""
package/unique_name="com.KIDGameProduction.Owniverse"
package/name="Owniverse"
@@ -136,11 +138,12 @@ screen/support_normal=true
screen/support_large=true
screen/support_xlarge=true
screen/background_color=Color(0, 0, 0, 1)
splash_screen/disable_godot_boot_splash=false
splash_screen/icon=""
splash_screen/branding_image=""
splash_screen/background_color=Color(0, 0, 0, 1)
user_data_backup/allow=false
command_line/extra_args=""
apk_expansion/enable=false
apk_expansion/SALT=""
apk_expansion/public_key=""
permissions/custom_permissions=PackedStringArray()
permissions/access_checkin_properties=false
permissions/access_coarse_location=false
@@ -295,3 +298,6 @@ permissions/write_sms=false
permissions/write_social_stream=false
permissions/write_sync_settings=false
permissions/write_user_dictionary=false
apk_expansion/enable=false
apk_expansion/SALT=""
apk_expansion/public_key=""
Binary file not shown.
+5 -5
View File
@@ -8,24 +8,24 @@
config_version=5
[animation]
[accessibility]
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
general/updates_per_second=10
[application]
config/name="Owniverse"
config/version="0.6.14"
config/version="0.7.0"
run/main_scene="uid://bv384dpmvjv8o"
config/features=PackedStringArray("4.7", "Mobile")
boot_splash/show_image=false
config/icon="uid://do5teb1i7uiuw"
config/icon="uid://djiwkyk470qb2"
config/size/borderless=true
config/stretch_mode=0
[autoload]
Global="*uid://dojk16crca3cx"
Global="*uid://dy0hrqie4ritn"
SaveManager="*uid://ul0srjc4gcgg"
[display]
+201
View File
@@ -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, но в исходном коде не применяется к дипломатии. Дополнительное правило для него не придумано. Остальные четыре тенденции имеют счётчики срабатываний и сброс, но не последствия — так же, как в исходной реализации. Параметры и остатки тенденций сохраняются; у существующего сохранения текущего формата без этих полей используются нейтральные значения и нулевые тенденции, без изменения ресурсов или возраста.
+549
View File
@@ -0,0 +1,549 @@
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.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.clear()
#endregion
func _unlock_items():
if locked_items.size() == 0: return
var _new_locked_items = []
var display_totals: Dictionary = owniverse.get_item_amounts(true)
for _item_id in locked_items:
#print(_item_id ' | ', _entity.unlock)
var _flag_to_unlock = true
for _elem in owniverse.entities[_item_id].unlock:
if maxf(owniverse.state.stats.total_amount.get(_elem, 0.0), display_totals.get(_elem, 0.0)) < owniverse.entities[_item_id].unlock[_elem]:
_flag_to_unlock = false
break
if _flag_to_unlock:
for item_id in [_item_id, owniverse.entities[_item_id].count_as]:
if owniverse_items.has(item_id):
owniverse_items[item_id].set_visible(true)
else:
_new_locked_items.append(_item_id)
locked_items = _new_locked_items
func _on_indicator_pressed(indicator_name: String, page: int = 0):
_set_description(indicator_name, page)
func _set_item_description(item: OwniverseItem) -> void:
var category: String = "Entity" if owniverse.entities.has(item.item_id) else "Force"
var id: String = item.item_id if category == "Entity" else item.item_name.capitalize()
var page: int = clampi(item.item_page, 0, 4)
if description_targets.get(page, {}).get("id", "") != id:
description_samples.erase(id)
description_rates.erase(id)
description_targets[page] = {"id": id, "category": category}
_refresh_descriptions()
func _set_description(item_name: String, page: int) -> void:
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 = 10 * _value if _value > 0 else 1
owniverse.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.entities[item_id]
#var amount = owniverse.get_available_to_produce(item_id)
#var amount = owniverse.get_available_to_produce(entity)
var amount = owniverse.calculate_max_producible(entity)
#print(item_id, " : ", amount)
if item_id == "SMBH":
var preview: Callable = func(requested: float) -> float:
return owniverse.calculate_max_producible(entity, requested)
page_actions[pa].set_actions(item_id, amount, preview)
else:
page_actions[pa].set_actions(item_id, amount)
func _page_action_pressed(page_id: int, action_amount: float) -> void:
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)
#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
owniverse.new_state = owniverse.state
for entity in previous.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.refresh_item_amounts()
_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.new_state = complete
owniverse.initialize_life_batches()
owniverse.refresh_item_amounts()
pending_page = clampi(int(game_data.get("ui", {}).get("current_page", 0)), 0, PAGE_COUNT - 1)
_restore_page.call_deferred()
return true
func _restore_page() -> void:
# Wait until containers have calculated the scrollable width.
await get_tree().process_frame
if pending_page >= 0:
stage_page_scroller.scroll_horizontal = pending_page * PAGE_WIDTH
pending_page = -1
func save_data() -> void:
var page: int = pending_page
if page < 0:
page = clampi(roundi(float(stage_page_scroller.scroll_horizontal) / PAGE_WIDTH), 0, PAGE_COUNT - 1)
SaveManager.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': [],
}
for entity_id in Global.OwniverseEntities:
var entity = OwniverseEntity.new(Global.OwniverseEntities[entity_id])
owniverse.entities[entity_id] = entity
owniverse.state.stats.total_amount[entity_id] = 0
owniverse.state.amount[entity_id] = 0
if entity.lifespan > 0:
owniverse.state.batches[entity.id] = []
owniverse.add_entity_to_batch(entity_id, 0, 0)
if entity.group_as != "":
owniverse.state.amount[entity.group_as] = 0
owniverse.state.stats.total_amount[entity.group_as] = 0
if entity.unlock != {}:
owniverse.locked_entities.append(entity)
_result.locked_items.append(entity.id)
if entity.product_per_year_type != "":
owniverse.auto_production.append(entity)
# groups
if entity.group_as != "":
if !owniverse.entity_groups.has(entity.group_as):
owniverse.entity_groups[entity.group_as] = []
owniverse.entity_groups[entity.group_as].append(entity_id)
if entity.event_group != "":
if !owniverse.entity_event_groups.has(entity.event_group):
owniverse.entity_event_groups[entity.event_group] = []
owniverse.entity_event_groups[entity.event_group].append(entity_id)
return _result
#endregion
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.
+112
View File
@@ -0,0 +1,112 @@
# Проверки поэтапного переноса из Owniverse.U
## Иконки стадий жизни
`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` в этом тесте ожидаемы.
Счётчики, время и кнопки производства обновляются раз в 0,1 секунды. Изменения симуляции помечают суммы для пересчёта; сами суммы вычисляются при обновлении интерфейса. Текст счётчиков применяется только к видимым элементам и только при изменении строки. Переход между страницами, выбор элемента и производство вызывают немедленное обновление. Описания сохраняют прежний интервал 0,25 секунды. Тест интерфейса использует игровую сцену с отключёнными загрузкой, сохранением и автоматической симуляцией, не обращаясь к сохранению игрока.
## Время: шаг 0,1 игрового года
```sh
godot --headless --path Owniverse.GI --script res://tests/test_time.gd
```
Команда выполняется из общей папки проектов. Тесты используют искусственные состояния; сохранение игрока не читается и не перезаписывается.
Проверяются границы десятых долей года, производство ресурсов на этих границах, накопление дробного остатка, скорости 1–10 000 лет/сек, продолжение после загрузки и недопустимые интервалы.
Схема состояния `0.1.1` хранит:
- `time.datetime` — накопленный возраст, включая необработанную дробную часть;
- `time.year` — последний рассчитанный возраст с точностью до десятой доли года.
Сохранения используют формат 2 и актуальную структуру состояния. Проверяются целостность данных, фоновая запись и повторная загрузка; поддержка и миграция прежних форматов отсутствуют.
Файл с несовместимым номером формата при загрузке переносится в `*.adventure.incompatible.bak` (при совпадении имени добавляется числовой суффикс). Игра начинает новую сессию и может сохранять её в формате 2. Прогресс несовместимого файла не переносится, исходные байты сохраняются в архиве. Ошибка архивирования, расшифровки или повреждение данных текущего формата по-прежнему блокируют перезапись.
## Согласованные правила эволюции
Сохраняются постепенные интервальные окна GI: начало преобразования — через `lifespan`, длительность окна — `lifespan / 40` при `lifespan > 10`, иначе `lifespan`.
Вероятностные исходы используют накопление дробного остатка GI, без случайного округления. Этот же подход выбран для будущего создания планет. Шансы планет записаны долями (`0.01` = 1%), а альтернативные исходы звёзд — процентами (`%0.1` = 0.1%). Парсер планет сохраняет дроби и поддерживает один элемент без `&`.
Уязвимость к бластам читается из `blastability` таблицы GI: HP, EON и цивилизации до C1.2 включительно; C1.3 и выше неуязвимы. Это ограничение относится к бластам уровня 1; локальное уничтожение планет уровня 2 в Unity охватывает все планеты.
```sh
godot --headless --path Owniverse.GI --script res://tests/test_stellar_rules.gd
```
Проверяются парсер планет, граница уязвимости, накопление редких исходов, сериализация остатков и постепенное преобразование реальной партии RD в TWD.
Создание планет подключено к ручному и автоматическому производству звёзд. Бласты обрабатываются как события, а не ресурс. Выжившие планеты переходят преемнику звезды пропорционально числу реально полученных преемников; при отсутствии преемника освобождаются. Локальное разрушение выполняется перед передачей.
Дополнительно проверяются частичная передача, альтернативные преемники, уровни разрушения 1/2, накопление слабых бластов, сохранения, ручное/автоматическое создание планет и несколько поколений за длинный шаг. Правила и формулы описаны в `../simulation_rules.md`.
## Описания выбранных объектов
```sh
godot --headless --path Owniverse.GI --script res://tests/test_descriptions.gd
```
Источник текста — `Global.Templates[Global.Locale]`. Заголовок берётся из `Name`, основной текст — из `Description`. Поддерживаются ключи с классом (`RD.Entity.Description`) и старые ключи без класса (`RD.Description`).
`%valueN%` заменяется значением переменной, имя которой записано в поле `valueN` этой строки. Поддерживаются `amount`, `colored costs`, `lifespan`, `voids`, `he / h`, `metallicity`, показатели энергии, плотности и скорости времени. `amount1`–`amount4` сил пока отображаются как `—`; неизвестные непустые обозначения тоже дают `—`, пустые поля — пустую строку. `[p]` превращается в разделение абзацев, цветовые BBCode-теги сохраняются.
Доступные требования зелёные, недоступные красные. Для объектов со связанными экземплярами выводится доступное количество и серым общее; для планет — общее количество. Скорость изменения вычисляется по реальному времени и обновляется раз в 0,25 секунды. Доли He/H вычисляются из текущих свободных запасов; при пустой смеси показывается `—`. Металличность и плотность используют расчёт среды GI. Счётчики исходной материи и этики Unity в текущей GI отсутствуют и здесь не добавляются.
## Первичная жизнь
```sh
godot --headless --path Owniverse.GI --script res://tests/test_life.gd
```
Проверяются абиогенез, деградация, повторные попытки без создания дополнительных планет, появление C0.0 на прежней звезде, остатки вероятностей, сохранение/продолжение, однократная инициализация старых планет и отсутствие воскрешения после бластов.
Проверка загрузки этических правил, распределений и отделения космических цивилизаций:
```sh
godot --headless --path Owniverse.GI --script res://tests/test_civilization_state.gd
```
Тест цивилизаций дополнительно проверяет общий звёздный бюджет, повторное использование резерва при переходе, звёзды в структурах, массовый эквивалент SMBH, блокировку неподдержанного прогресса, каскад деградации и повторное применение ограничений без потерь.
## Полная эволюция цивилизаций
```sh
godot --headless --path Owniverse.GI --script res://tests/test_civilization_evolution.gd
```
Проверяются реальные коэффициенты C0.2, войны и побочный ущерб C2.0, замена планет колониями с сохранением хозяина, дипломатическое влияние, бонус периода от структур и деградация при их отсутствии. Ускоренная модель проходит все стадии C0.2 → C4.4 → Ct. Проверки контролируют сохранение населения, целочисленность этик, валидацию сохранения и детерминированное продолжение после сериализации.
+5
View File
@@ -0,0 +1,5 @@
extends "res://world/owniverse/owniverse.gd"
# Keep the old materialized-batch path as an independent comparison mode.
func _uses_short_lived_fast_path(_id: String) -> bool:
return false
+1
View File
@@ -0,0 +1 @@
uid://cn6hhpjw1sjpb
+14
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://c8ewikdhlvnex
+47
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://dilbcphuku6k1
+116
View File
@@ -0,0 +1,116 @@
extends SceneTree
var failures: int = 0
var checks: int = 0
var completions: Array = []
func _initialize() -> void:
call_deferred("run")
func check(value: bool, message: String) -> void:
checks += 1
if not value:
failures += 1
push_error(message)
func run() -> void:
var manager = load("res://autoload/save_manager.gd").new()
var directory: String = "/tmp/owniverse-async-%s-%s" % [OS.get_process_id(), Time.get_ticks_usec()]
DirAccess.make_dir_recursive_absolute(directory)
manager.save_directory = directory
root.add_child(manager)
manager.save_completed.connect(func(filename, success, error): completions.append([filename, success, error]))
var world = load("res://world/owniverse/owniverse.gd").new()
var data: Dictionary = {"state": world.state, "ui": {"current_page": 1}}
var global: Node = root.get_node("Global")
var old_plaintext: bool = global.save_plaintext_copy
global.save_plaintext_copy = true
check(manager.request_save("snapshot", "test-password", data), "Snapshot queued")
data.state.energy = 99.0
data.ui.current_page = 2
var frames: int = 0
var deadline: int = Time.get_ticks_msec() + 10000
while manager.is_saving() and Time.get_ticks_msec() < deadline:
frames += 1
await process_frame
check(not manager.is_saving() and frames > 0, "Frames continue while save is pending")
var saved: Dictionary = manager.load_data("snapshot", "test-password")
check(saved.state.energy == 1e14 and saved.ui.current_page == 1, "Worker owns a deep snapshot")
var plain: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("snapshot.adventure.json")))
check(plain.state.energy == 1e14, "Diagnostic JSON uses same snapshot")
check(completions.size() == 1 and completions[0][1], "Completion reports successful write")
completions.clear()
data.state.energy = 1.0
manager.request_save("latest", "test-password", data)
data.state.energy = 2.0
manager.request_save("latest", "test-password", data)
data.state.energy = 3.0
manager.request_save("latest", "test-password", data)
data.state.energy = 4.0
manager.flush_saves()
check(completions.size() == 2, "Repeated requests coalesce behind active write")
check(manager.load_data("latest", "test-password").state.energy == 3.0, "Last queued snapshot wins")
var incompatible_path: String = directory.path_join("incompatible.adventure")
var incompatible = FileAccess.open_encrypted_with_pass(incompatible_path, FileAccess.WRITE, "test-password")
incompatible.store_var({"format_version": -1, "state": {"unknown": true}}, false)
incompatible.close()
var original: PackedByteArray = FileAccess.get_file_as_bytes(incompatible_path)
var previous_archive = FileAccess.open(incompatible_path + ".incompatible.bak", FileAccess.WRITE)
previous_archive.store_string("keep previous backup")
previous_archive.close()
manager.blocked_saves.incompatible = true
check(manager.load_data("incompatible", "test-password").is_empty() and manager.last_error == "", "Unsupported format permits a fresh session")
check(not manager.blocked_saves.has("incompatible") and not FileAccess.file_exists(incompatible_path), "Incompatible file no longer blocks saving")
check(FileAccess.get_file_as_bytes(incompatible_path + ".incompatible.bak.1") == original, "Unsupported save archived without losing bytes")
check(FileAccess.get_file_as_string(incompatible_path + ".incompatible.bak") == "keep previous backup", "Earlier archive preserved")
check(manager.request_save("incompatible", "test-password", data), "New session can queue a save")
manager.flush_saves()
check(manager.load_data("incompatible", "test-password").state == data.state, "New format saves and reloads after incompatible file")
# Expected failures below must leave the last good encrypted file intact.
data.state.energy = -1.0
manager.request_save("latest", "test-password", data)
manager.flush_saves()
check(not completions.back()[1] and manager.last_error.contains("Invalid save"), "Worker validation error reaches main thread")
check(manager.load_data("latest", "test-password").state.energy == 3.0, "Invalid snapshot preserves previous save")
manager.blocked_saves.latest = true
check(not manager.request_save("latest", "test-password", data), "Unreadable save protection also applies to queue")
# A valid snapshot exercises an actual I/O failure, not validation.
data.state.energy = 5.0
manager.request_save("missing/failure", "test-password", data)
manager.flush_saves()
check(manager.last_error.contains("Cannot write"), "I/O error reported without hanging queue")
manager.request_save("shutdown", "test-password", data)
data.state.energy = 6.0
manager.request_save("shutdown", "test-password", data)
manager.free()
var reader = load("res://autoload/save_manager.gd").new()
reader.save_directory = directory
check(reader.load_data("shutdown", "test-password").state.energy == 6.0, "Teardown drains final pending snapshot")
reader.free()
world.free()
# 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)
+1
View File
@@ -0,0 +1 @@
uid://dsn0ih3yfrry2
+57
View File
@@ -0,0 +1,57 @@
extends SceneTree
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks += 1
if not condition:
failures += 1
push_error(message)
func fixture():
var world = load("res://world/owniverse/owniverse.gd").new()
var global: Node = root.get_node("Global")
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
world.entities[id] = entity
world.state.amount[id] = 0.0
world.state.stats.total_amount[id] = 0.0
if entity.group_as != "":
world.state.amount[entity.group_as] = 0.0
world.state.stats.total_amount[entity.group_as] = 0.0
world.entities["N"].life_product = {}
world.entities["N"].product_per_year_type = "H"
world.entities["N"].product_per_year_amount = 10.0
world.entities["N"].auto = 0.0
world.state.amount.N = 1.0
return world
func dispose(world) -> void:
for entity in world.entities.values():
entity.free()
world.free()
func run() -> void:
var world = fixture()
world.state.batch_origins.LGRB = 10357034.80000000074505806
var boundary: float = 41215289.79999999701976776
for interval in [[boundary - 2.5, boundary + 2.25], [boundary, boundary + 2.0]]:
world.state.batches.LGRB = []
world._append_batch(world.state, "LGRB", 100.0, interval[0], interval[1])
var total: float = 0.0
var last: float = -INF
for batch: Dictionary in world.state.batches.LGRB:
total += batch.amount
check(batch.created_from > last, "Batch windows advance monotonically")
check(batch.created_to > batch.created_from, "Positive window duration")
check(batch.amount >= 0.0, "No negative batch amounts")
last = batch.created_from
check(total == 100.0, "Boundary correction preserves all objects")
check(world.state.batches.LGRB.size() <= 7, "Finite number of windows")
dispose(world)
print("Batch boundaries: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://dq86k8bto42xl
+164
View File
@@ -0,0 +1,164 @@
extends SceneTree
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks += 1
if not condition:
failures += 1
push_error(message)
func fixture():
var world = load("res://world/owniverse/owniverse.gd").new()
var global: Node = root.get_node("Global")
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
world.entities[id] = entity
world.state.amount[id] = 0.0
world.state.stats.total_amount[id] = 0.0
if entity.group_as != "":
world.state.amount[entity.group_as] = 0.0
world.state.stats.total_amount[entity.group_as] = 0.0
world.entities["N"].life_product = {}
world.entities["N"].product_per_year_type = "H"
world.entities["N"].product_per_year_amount = 10.0
world.entities["N"].auto = 0.0
world.state.amount.N = 1.0
return world
func dispose(world) -> void:
for entity in world.entities.values():
entity.free()
world.free()
func seed_population(world, id: String, count: float, time: float = 1.0) -> void:
world.state.amount.N = 0.0
world.state.amount[id] = count
world.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}]
world.initialize_life_batches()
func sum_ethics(world, id: String) -> float:
var result: float = 0.0
for n in world.state.ethics.get(id, {}).values(): result += n
return result
func invariants(world) -> void:
var physical: Dictionary = world._physical_amounts(world.state)
for id in world.state.ethics:
check(sum_ethics(world, id) == physical.get(id, 0.0), "Ethics equal physical population for " + id)
for n in world.state.ethics[id].values():
if n < 0.0 or n != floorf(n): check(false, "Invalid ethic amount")
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Civilization state validates")
func run() -> void:
var global: Node = root.get_node("Global")
var baseline: Dictionary = global.CivilizationRules.duplicate(true)
var world = fixture()
seed_population(world, "C0.2", 60000.0)
world._evolve_batches(world.state, 1.0)
check(world.state.amount["C0.3"] > 0.0, "C0.2 actually progresses")
check(world.state.amount["C0.1"] > 0.0, "C0.2 actually degrades")
check(world.state.civilization_stats.get("wars", 0.0) == 0.0, "Table disables early wars")
check(world.state.amount["C0.2"] + world.state.amount["C0.3"] + world.state.amount["C0.1"] == 60000.0, "Ordinary evolution conserves population")
check(world.state.stats.total_amount["C0.2"] == 0.0, "Survivors do not count as new civilizations")
check(world.state.civilization_stats.worship > 0.0, "Worship derived from surviving ethics")
invariants(world)
var restored = fixture()
restored.state = bytes_to_var(var_to_bytes(world.state))
world._evolve_batches(world.state, 2100.0)
restored._evolve_batches(restored.state, 2100.0)
check(world.state == restored.state, "Save resumes deterministic ethical evolution")
invariants(world)
dispose(restored)
dispose(world)
world = fixture()
world.state.amount.RD = 1e8
seed_population(world, "C2.0", 60000.0)
world.state.amount["C1.9"] = 10000.0
world.initialize_life_batches()
world._evolve_batches(world.state, 1.0)
check(world.state.civilization_stats.wars > 0.0, "Wars execute when enabled")
check(world.state.civilization_stats.collateral > 0.0, "Wars affect previous-stage populations")
check(world.state.amount["C2.1"] > 0.0, "Winners and peaceful progress reach next stage")
invariants(world)
dispose(world)
world = fixture()
world.state.amount.RD = 1.0
world.state.binds.RD = {"HP": 5.0, "GG": 5.0, "AB": 5.0, "RP": 5.0}
seed_population(world, "C1.6", 60000.0)
for row: Dictionary in global.CivilizationRules["C1.6"].ethics.values(): row.degradation = "0"
world._evolve_batches(world.state, 1.0)
var after: Dictionary = world._physical_amounts(world.state)
check(world.state.civilization_stats.colonies == 20.0, "Colonization limited to real targets")
check(after.HP + after.GG + after.AB + after.RP == 0.0, "Colonized objects consumed once")
check(world.state.binds.RD.get("C1.4", 0.0) == 20.0, "Colonies retain target host and use two-stages-earlier level")
var totals: float = 0.0
for id in after:
if world.entities[id].group_as == "Planet": totals += after[id]
check(totals == 60020.0, "Colonization replaces planets, does not create extra worlds")
invariants(world)
dispose(world)
global.CivilizationRules = baseline.duplicate(true)
world = fixture()
world.state.amount.RD = 1e25
world.state.amount.BG = 1e25
world.state.amount.TWD = 1e25
world.state.amount.SNS = 1e25
world.state.amount.MGT = 1e25
world.state.amount.BH25 = 1e25
world.state.amount.SMBH = 1e26
for id in world.entities:
if world.entities[id].claim != "": world.state.amount[id] = 1e12
# Accelerated deterministic fixture traverses every enabled stage, including Ct.
for id in global.CivilizationRules:
if id == "Ct": continue
global.CivilizationRules[id].fights = false
global.CivilizationRules[id].colonizes = false
global.CivilizationRules[id].diplomacy = false
for row: Dictionary in global.CivilizationRules[id].ethics.values():
row.progress = "1"
row.degradation = "0"
world.entities[id].lifespan = 1.0
world.entities[id].lifespan_delta = 1.0
global.CivilizationRules[id].span = 1.0
seed_population(world, "C0.2", 6.0)
world._evolve_batches(world.state, 1000.0)
check(world.state.amount.Ct == 6.0, "Complete C0.2 -> C4.4 -> Ct chain runs")
check(not world.state.batches.has("Ct"), "Transcendence is terminal")
invariants(world)
dispose(world)
global.CivilizationRules = baseline
world = fixture()
world.state.amount.RD = 10000.0
world.state.amount.SCS = 1.0
world.state.amount["C2.4"] = 1.0
world.initialize_life_batches()
var span: float = world.state.batches["C2.4"][0].start_year
check(is_equal_approx(span, 20000.0 / 3.6), "Structure bonus shortens newly registered lifespan")
dispose(world)
var evolution = load("res://world/owniverse/civilization_evolution.gd")
var balanced: Dictionary = {"m": 10000.0, "l": 10000.0, "w": 10000.0, "d": 10000.0, "c": 10000.0, "b": 10000.0}
var neutral: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics)
var diplomatic: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics, true)
check(neutral == balanced, "Ordinary drift preserves balanced ethics")
check(diplomatic.d > diplomatic.w, "Diplomacy uses per-ethic influence")
check(evolution.total(diplomatic) == 60000.0, "Diplomacy conserves population")
world = fixture()
world.state.amount.RD = 1e15
world.state.amount.BG = 1e15
world.state.amount["C3.0"] = 6.0
world.initialize_life_batches()
check(world.state.amount["C3.0"] == 0.0 and world.state.amount["C2.9"] == 6.0, "Missing mandatory structures degrade existing civilizations")
invariants(world)
dispose(world)
print("Civilization evolution: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://u4ujq17gu2l5
+134
View File
@@ -0,0 +1,134 @@
extends SceneTree
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks += 1
if not condition:
failures += 1
push_error(message)
func fixture():
var world = load("res://world/owniverse/owniverse.gd").new()
var global: Node = root.get_node("Global")
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
world.entities[id] = entity
world.state.amount[id] = 0.0
world.state.stats.total_amount[id] = 0.0
if entity.group_as != "":
world.state.amount[entity.group_as] = 0.0
world.state.stats.total_amount[entity.group_as] = 0.0
world.entities["N"].life_product = {}
world.entities["N"].product_per_year_type = "H"
world.entities["N"].product_per_year_amount = 10.0
world.entities["N"].auto = 0.0
world.state.amount.N = 1.0
return world
func dispose(world) -> void:
for entity in world.entities.values():
entity.free()
world.free()
func run() -> void:
var world = fixture()
var global: Node = root.get_node("Global")
check(global.CivilizationRules.size() == 44, "Load all ethical civilization stages")
for row: Dictionary in global.CivilizationRules.values():
check(row.ethics.size() == 6 or row.span == 0.0, "Every ethical stage has six rules")
world.state.amount.RD = 12.0
world.state.amount["C0.2"] = 8.0
world.state.binds.RD = {"C2.0": 12.0, "C1.9": 3.0}
world.initialize_life_batches()
check(world.state.amount["C2.0"] == 12.0 and world.state.binds.RD["C2.0"] == 0.0, "Old space civilizations detach without loss")
check(world.state.binds.RD["C1.9"] == 3.0, "Planetary civilizations retain their host")
check(world.state.ethics["C0.2"] == {"m": 2.0, "l": 2.0, "w": 1.0, "d": 1.0, "c": 1.0, "b": 1.0}, "Migration uses equal ethics and deterministic remainder")
check(world.state.ethics["C2.0"].values().all(func(n): return n == 2.0), "Bound civilization ethics count physical population")
var before: Dictionary = world.state.duplicate(true)
world.initialize_life_batches()
check(world.state == before, "Repeated initialization preserves ethics")
world.state.ethics["C0.2"] = {"m": 8.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0}
world.initialize_life_batches()
check(world.state.ethics["C0.2"].m == 8.0, "Existing unequal distribution is retained")
world.state.amount["C0.2"] = 3.0
world.initialize_life_batches()
check(world.state.ethics["C0.2"].m == 3.0, "Losses reduce ethics without inventing population")
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Ethics state validates")
var restored = fixture()
restored.state = bytes_to_var(var_to_bytes(world.state))
restored.initialize_life_batches()
check(restored.state == world.state, "Save roundtrip preserves ethics and ownership")
dispose(restored)
dispose(world)
world = fixture()
world.state.amount.RD = 10.0
for row: Dictionary in global.CivilizationRules["C1.9"].ethics.values():
row.progress = "1"
row.degradation = "0"
row.colonization = "0"
world.state.binds.RD = {"C1.9": 10.0}
world.state.batches["C1.9"] = [{"amount": 10.0, "start_year": 1.0, "end_year": 1.0}]
world._evolve_batches(world.state, 1.0)
check(world.state.amount["C2.0"] == 10.0 and world.state.binds.RD.get("C2.0", 0.0) == 0.0, "Space successor does not inherit planetary binding")
dispose(world)
var capacity = load("res://world/owniverse/civilization_state.gd")
world = fixture()
world.state.amount.RD = 10.0
world.state.amount["C2.0"] = 8.0
var budget: Dictionary = capacity.stellar_budget(world, world.state, global.CivilizationRules)
check(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5.0) == 2.0, "Growth consumes only remaining stellar capacity")
check(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5.0) == 0.0, "A reservation cannot be spent twice")
world.state.amount["C2.0"] = 1.0
budget = capacity.stellar_budget(world, world.state, global.CivilizationRules)
check(capacity.admit_transition(budget, global.CivilizationRules, "C2.0", "C2.1", 1.0) == 1.0, "A transition reuses the source reservation")
check(budget.free.DwStar == 0.0, "C2.1 reserves ten dwarf stars")
world.state.amount["C2.0"] = 0.0
world.state.binds.SC = {"RD": 10.0}
budget = capacity.stellar_budget(world, world.state, global.CivilizationRules)
check(budget.capacity.DwStar == 20.0, "Stars inside structures are counted once")
dispose(world)
world = fixture()
world.state.amount.RD = 12.0
world.state.amount["C2.1"] = 2.0
world.state.amount["C2.0"] = 5.0
world.initialize_life_batches()
check(world.state.amount["C2.1"] == 1.0 and world.state.amount["C2.0"] == 2.0, "Shared budget supports advanced civilizations first")
check(world.state.binds.RD["C1.9"] == 4.0, "Excess cascades to supported planetary stage")
var snapshot: Dictionary = world.state.duplicate(true)
world.initialize_life_batches()
check(world.state == snapshot, "Capacity enforcement is idempotent")
world.state.amount.RD = 0.0
world.initialize_life_batches()
check(world.state.amount["C2.1"] == 0.0 and world.state.amount["C2.0"] == 0.0, "Vanished stars force all unsupported space civilizations down")
check(world._physical_amounts(world.state)["C1.9"] == 7.0, "Forced degradation conserves civilizations")
dispose(world)
world = fixture()
world.state.amount.LGW = 20.0
world.state.amount.SMBH = 2.0e22
world.state.amount["C4.2"] = 3.0
world.initialize_life_batches()
check(world.state.amount["C4.2"] == 2.0, "SMBH capacity uses stellar mass equivalent")
check(world._physical_amounts(world.state)["C1.9"] == 1.0, "Unsupported civilization degrades through missing habitat families")
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Capacity-limited state validates")
dispose(world)
world = fixture()
world.state.amount.RD = 2.0
for row: Dictionary in global.CivilizationRules["C1.9"].ethics.values():
row.progress = "1"
row.degradation = "0"
row.colonization = "0"
world.state.binds.RD = {"C1.9": 5.0}
world.state.batches["C1.9"] = [{"amount": 5.0, "start_year": 1.0, "end_year": 1.0}]
world._evolve_batches(world.state, 1.0)
check(world.state.amount["C2.0"] == 2.0 and world.state.binds.RD["C1.9"] == 3.0, "Unfunded transition retains the original planetary civilization")
dispose(world)
print("Civilization state: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://nvgiilyqitw
+96
View File
@@ -0,0 +1,96 @@
extends SceneTree
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks += 1
if not condition:
failures += 1
push_error(message)
func fixture():
var world = load("res://world/owniverse/owniverse.gd").new()
var global: Node = root.get_node("Global")
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
world.entities[id] = entity
world.state.amount[id] = 0.0
world.state.stats.total_amount[id] = 0.0
if entity.group_as != "":
world.state.amount[entity.group_as] = 0.0
world.state.stats.total_amount[entity.group_as] = 0.0
world.entities["N"].life_product = {}
world.entities["N"].product_per_year_type = "H"
world.entities["N"].product_per_year_amount = 10.0
world.entities["N"].auto = 0.0
world.state.amount.N = 1.0
return world
func dispose(world) -> void:
for entity in world.entities.values():
entity.free()
world.free()
func run() -> void:
var global: Node = root.get_node("Global")
var description = load("res://world/owniverse/item_description.gd")
var world = fixture()
world.state.amount.H = 8.0
world.state.amount.He = 2.0
for locale in ["en", "ru"]:
global.Locale = locale
var text: String = description.render(world, global, "RD")
check(text.contains("[b]") and not text.contains("%value"), "Localized template resolved: " + locale)
check(text.contains("#40cc40"), "Affordable cost green: " + locale)
check(text.contains("4.0000E+10"), "Lifespan substituted: " + locale)
text = description.render(world, global, "He")
check(text.contains("He: 20.00% / H: 80.00%") and not text.contains("%value"), "Helium metadata and mixture: " + locale)
text = description.render(world, global, "Shovelin", "Force")
check(text.count("]—[/color]") == 4, "Missing force levels use dash: " + locale)
text = description.render(world, global, "YD")
check(text.contains("#ff6060"), "Insufficient cost red: " + locale)
var reordered: Dictionary = {"text": "%value0% / %value1%", "value0": "lifespan", "value1": "amount"}
check(global.render_template(reordered, {"amount": "12", "lifespan": "99"}) == "99 / 12", "Metadata chooses variables, not fixed argument positions")
world.state.amount.RD = 2.0
world.state.binds.G = {"RD": 3.0, "HP": 7.0}
var values: Dictionary = description.variables(world, global, "RD", -2.0)
check(values.amount.contains("2") and values.amount.contains("(5)") and values.amount.contains("#888888"), "Free and total counts with gray binding total")
check(values.amount.contains("#ff6060") and values.amount.contains("−2.0"), "Real-time decrease red")
values = description.variables(world, global, "HP")
check(values.amount == "7", "Planet description includes bound planets")
world.state.amount.EON1 = 2.0
world.state.binds.G.EON2 = 3.0
var saved_state: Dictionary = world.state.duplicate(true)
var counters: Dictionary = world.get_item_amounts()
check(counters.HP == 7.0 and counters.LP == 5.0 and counters.Planet == 12.0, "Planet counters include bound objects and aggregate eons")
check(counters.RD == 2.0, "Star counters retain available quantity")
check(world.state == saved_state, "Reading counters does not mutate simulation")
check(world.get_item_amounts(true).HP == 0.0, "Produced counters remain independent of current population")
values = description.variables(world, global, "RD", 2.0)
check(values.amount.contains("#40cc40") and values.amount.contains("+2.0"), "Increase green")
var stage_script = load("res://stage/stage.gd")
check(stage_script.can_instantiate(), "Stage integration compiles")
var stage = stage_script.new()
stage.owniverse.free()
stage.owniverse = world
var label: RichTextLabel = RichTextLabel.new()
stage.g_item_descriptions = [label]
stage.description_targets[0] = {"id": "RD", "category": "Entity"}
stage._refresh_descriptions()
check(label.bbcode_enabled and label.text.contains("Красный карлик"), "Stage displays localized rich text")
stage._update_descriptions(0.25)
world.state.amount.RD += 1.0
stage._update_descriptions(0.25)
check(label.text.contains("+4.0"), "Selected description refreshes with rate per real second")
global.Locale = "en"
stage._update_descriptions(0.25)
check(label.text.contains("Red Dwarf"), "Selected description follows locale changes")
label.free()
stage.free()
dispose(world)
print("Descriptions: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://b1efrwyimgle7
+58
View File
@@ -0,0 +1,58 @@
extends SceneTree
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks += 1
if not condition:
failures += 1
push_error(message)
func fixture():
var world = load("res://world/owniverse/owniverse.gd").new()
var global: Node = root.get_node("Global")
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
world.entities[id] = entity
world.state.amount[id] = 0.0
world.state.stats.total_amount[id] = 0.0
if entity.group_as != "":
world.state.amount[entity.group_as] = 0.0
world.state.stats.total_amount[entity.group_as] = 0.0
world.entities["N"].life_product = {}
world.entities["N"].product_per_year_type = "H"
world.entities["N"].product_per_year_amount = 10.0
world.entities["N"].auto = 0.0
world.state.amount.N = 1.0
return world
func dispose(world) -> void:
for entity in world.entities.values():
entity.free()
world.free()
func run() -> void:
var world = fixture()
for id in world.entities:
var expected: Array = []
for key in world.entities:
if world.entities[key].group_as == id:
expected.append(key)
check(world._is_group(id) == (not expected.is_empty()), "Group classification: " + id)
check(world._members(id) == (expected if not expected.is_empty() else [id]), "Group members: " + id)
var members: Array = world._members("Star")
members.clear()
check(not world._members("Star").is_empty(), "Returned members are independent")
world.entities.N.group_as = "TestGroup"
world.rebuild_group_index()
check(world._members("TestGroup") == ["N"], "Explicit definition reload")
world.entities.N.free()
world.entities.erase("N")
check(not world._is_group("TestGroup"), "Removed definition invalidates index")
print("Group index: %d checks, %d failures" % [checks, failures])
dispose(world)
quit(1 if failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://bykrkembtacbw
+123
View File
@@ -0,0 +1,123 @@
extends SceneTree
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks += 1
if not condition:
failures += 1
push_error(message)
func fixture():
var world = load("res://world/owniverse/owniverse.gd").new()
var global: Node = root.get_node("Global")
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
world.entities[id] = entity
world.state.amount[id] = 0.0
world.state.stats.total_amount[id] = 0.0
if entity.group_as != "":
world.state.amount[entity.group_as] = 0.0
world.state.stats.total_amount[entity.group_as] = 0.0
world.entities["N"].life_product = {}
world.entities["N"].product_per_year_type = "H"
world.entities["N"].product_per_year_amount = 10.0
world.entities["N"].auto = 0.0
world.state.amount.N = 1.0
return world
func dispose(world) -> void:
for entity in world.entities.values():
entity.free()
world.free()
func run() -> void:
var world = fixture()
world.state.amount.N = 0.0
world.state.amount.HP = 40000.0
world.initialize_life_batches()
check(world.state.batches.HP.size() == 1, "Old HP receives life cohort")
check(world.state.batches.HP[0].start_year == 5e6, "HP uses source span")
var before: Dictionary = world.state.duplicate(true)
world.initialize_life_batches()
check(world.state == before, "Initialization is idempotent")
world._evolve_batches(world.state, 5e6 + 5e6 / 40.0)
check(world.state.amount.EON1 == 10.0, "HP abiogenesis follows source probability")
check(world.state.amount.RP == 100.0, "HP degradation produces RP")
check(world.state.amount.HP == 39890.0, "Unchanged planets retry, not disappear")
check(world.state.stats.total_amount.HP == 0.0, "Retry does not inflate creation statistics")
check(world.state.batches.HP.size() > 0, "Survivors get next attempt")
var physical: Dictionary = world._physical_amounts(world.state)
check(physical.HP + physical.EON1 + physical.RP == 40000.0, "Life conserves planets")
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Life save validates")
var restored = fixture()
restored.state = bytes_to_var(var_to_bytes(world.state))
world._evolve_batches(world.state, 11e6)
restored._evolve_batches(restored.state, 11e6)
check(world.state == restored.state, "Life continuation preserves cohorts and carries")
dispose(restored)
dispose(world)
world = fixture()
world.state.amount.RD = 1.0
world.state.binds.RD = {"EON5": 40000.0}
world.initialize_life_batches()
world._evolve_batches(world.state, 5e5 + 5e5 / 40.0)
check(world.state.binds.RD["C0.0"] == 10.0, "Intelligence emerges at the same host star")
check(world.state.amount["C0.0"] == 0.0, "Life transition keeps planet bound")
check(world.state.batches.has("C0.0"), "New C0.0 starts civilization evolution")
dispose(world)
world = fixture()
var progress: float = 0.0
var degraded: float = 0.0
var valid_outcomes: bool = true
for i in 4001:
var products: Dictionary = world._life_products(world.state, world.entities.HP, 1.0)
var total: float = 0.0
for value in products.values():
total += value
valid_outcomes = valid_outcomes and value >= 0.0 and value == floorf(value)
valid_outcomes = valid_outcomes and total == 1.0
progress += products.get("EON1", 0.0)
degraded += products.get("RP", 0.0)
check(valid_outcomes, "Every single-planet attempt has whole outcomes and conserves population")
check(progress == 1.0 and degraded == 10.0, "Rare outcomes accumulate deterministically")
dispose(world)
world = fixture()
world.state.time = {"datetime": 123.4, "year": 123.4}
world.state.binds.RD = {"HP": 2.0}
world.initialize_life_batches()
check(world.state.batches.HP[0].created_from == 123.4, "Old bound worlds begin life at load time")
world._destroy_planets(world.state, "RD", 1.0, 1)
world._evolve_batches(world.state, 1e7)
check(world._physical_amounts(world.state).HP == 0.0 and world.state.binds.RD.RP == 2.0, "Destroyed life does not resurrect from cohort")
dispose(world)
world = fixture()
world.state.time = {"datetime": 123.4, "year": 123.4}
world.state.amount["C0.0"] = 40000.0
world.initialize_life_batches()
check(world.state.batches["C0.0"][0].created_from == 123.4, "Civilization cohorts begin at the current time")
before = world.state.duplicate(true)
world.initialize_life_batches()
check(world.state == before, "Life batch initialization is idempotent")
world._evolve_batches(world.state, 2173.4)
check(world.state.amount["C0.1"] == 700.0 and world.state.amount.EON5 == 1000.0, "C0.0 progresses and degrades by Unity rates")
check(world.state.amount["C0.0"] == 38300.0, "C0.0 remainder stays alive")
dispose(world)
world = fixture()
world.state.amount.RD = 1.0
world.state.binds.RD = {"C0.1": 40000.0}
world.initialize_life_batches()
world._evolve_batches(world.state, 2050.0)
check(world.state.binds.RD["C0.2"] == 700.0, "C0.1 reaches ethical civilization without losing host")
check(world.state.binds.RD["C0.0"] == 1000.0, "C0.1 degradation follows Unity")
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Extended life save validates")
dispose(world)
print("Life: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://dghmnvnl7ks1b
+51
View File
@@ -0,0 +1,51 @@
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.get_item_amounts()
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.get_item_amounts(true).EON1 == 12.0 and world.get_item_amounts(true).LP == 16.0, "Historical counts retain individual and aggregate values")
check(world.state == before, "Presentation does not modify simulation state")
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.entities.values(): entity.free()
world.free()
scene.free()
print("Life icons: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://c4p1ad0ij32xo
+58
View File
@@ -0,0 +1,58 @@
extends SceneTree
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks += 1
if not condition:
failures += 1
push_error(message)
func fixture():
var world = load("res://world/owniverse/owniverse.gd").new()
var global: Node = root.get_node("Global")
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
world.entities[id] = entity
world.state.amount[id] = 0.0
world.state.stats.total_amount[id] = 0.0
if entity.group_as != "":
world.state.amount[entity.group_as] = 0.0
world.state.stats.total_amount[entity.group_as] = 0.0
world.entities["N"].life_product = {}
world.entities["N"].product_per_year_type = "H"
world.entities["N"].product_per_year_amount = 10.0
world.entities["N"].auto = 0.0
world.state.amount.N = 1.0
return world
func dispose(world) -> void:
for entity in world.entities.values():
entity.free()
world.free()
func run() -> void:
var world = fixture()
world.initialize_life_batches()
world.entities.N.cost = {"H": 2.0}
world.entities.N.bind = {"S": 3.0}
world.state.amount.H = 10.0
world.state.amount.S = 0.0
world.state.auto_resources = {"H": 10.0}
world.state.batches.H = [{"amount": 10.0, "start_year": 1.0, "end_year": 2.0}]
var before: Dictionary = world.state.duplicate(true)
check(not world._try_production(world.state, world.entities.N, 1.0, true), "Binding failure rejects production")
check(world.state == before, "Failure preserves costs, batches and budget")
world.state.amount.S = 10.0
var old_batches: Array = world.state.batches.H
check(world._try_production(world.state, world.entities.N, 2.0, true), "Funded production succeeds")
check(world.state.amount.H == 6.0 and world.state.amount.S == 4.0, "Exact cost and binding")
check(world.state.auto_resources.H == 6.0, "Exact automatic budget")
check(old_batches[0].amount == 10.0 and world.state.batches.H[0].amount == 6.0, "Consumed batches isolated")
print("Transaction: %d checks, %d failures" % [checks, failures])
dispose(world)
quit(1 if failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://byjixspismsnh
+78
View File
@@ -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.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.entities[id].group_as != "":
world.state.amount[world.entities[id].group_as] = 0.0
world.state.stats.total_amount[world.entities[id].group_as] = 0.0
return world
func dispose(world) -> void:
for entity in world.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._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._evolve_batches(fast.state, year)
legacy._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._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)
+1
View File
@@ -0,0 +1 @@
uid://dp7a88n0mmyb5
+262
View File
@@ -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.entities[id] = entity
world.state.amount[id] = 0.0
world.state.stats.total_amount[id] = 0.0
if entity.group_as != "":
world.state.amount[entity.group_as] = 0.0
world.state.stats.total_amount[entity.group_as] = 0.0
world.entities["N"].life_product = {}
world.entities["N"].product_per_year_type = "H"
world.entities["N"].product_per_year_amount = 10.0
world.entities["N"].auto = 0.0
world.state.amount.N = 1.0
return world
func dispose(world) -> void:
for entity in world.entities.values():
entity.free()
world.free()
func run() -> void:
var world = fixture()
for id in ["DSN", "SN", "BSN", "HN", "SGRB", "GRB", "LGRB", "HGRB"]:
check(world._evolution_products(world.state, world.entities[id], 3.0).blast == 3.0 * world.entities[id].blast, "Entity blast determines power: " + id)
world.entities.SN.output_and.blast = 999.0
world.entities.SN.blast = 4
check(world._evolution_products(world.state, world.entities.SN, 2.0).blast == 8.0, "Imported output cannot override or double blast power")
world.entities.SN.blast = 0
check(not world._evolution_products(world.state, world.entities.SN, 2.0).has("blast"), "Zero entity blast disables external event")
world.entities.SN.output_or = {"positive": "blast:999", "negative": "blast:999", "chance": 50.0}
check(not world._evolution_products(world.state, world.entities.SN, 2.0).has("blast"), "Alternative output cannot restore disabled blast")
dispose(world)
world = fixture()
var rd = world.entities.RD
check(rd.planets.HP.chance == 0.01, "Preserve fractional planet probability")
check(rd.planets.GG.amount == 2.0 and rd.planets.GG.chance == 0.08, "Read count and chance separately")
check(rd._parse_planet_string("HP:1:0.01") == {"HP": {"amount": 1.0, "chance": 0.01}}, "Accept a single planet term")
check(rd._parse_planet_string("").is_empty(), "Empty planet definitions stay empty")
for id in ["HP", "EON1", "C0.9", "C1.2"]:
check(world.entities[id].blastability, "Blast vulnerable: " + id)
for id in ["RP", "GG", "AB", "C1.3", "C1.4", "C1.5"]:
check(not world.entities[id].blastability, "Blast protected: " + id)
# Actual GI table: HRG -> SN, with 1% GRB; no random rounding.
var totals: Dictionary = {}
for i in 100:
var products: Dictionary = world._evolution_products(world.state, world.entities.HRG, 1.0)
for id in products:
totals[id] = totals.get(id, 0.0) + products[id]
check(totals.get("SN") == 99.0 and totals.get("GRB") == 1.0, "GI rare outcomes accumulate deterministically")
var restored = fixture()
restored.state = bytes_to_var(var_to_bytes(world.state))
check(world._evolution_products(world.state, world.entities.HRG, 37.0) == restored._evolution_products(restored.state, restored.entities.HRG, 37.0), "Evolution carry survives serialization")
dispose(restored)
# Real RD lifespan, four stars spread over the existing lifespan/40 window.
world.state.amount.N = 0.0
world.state.amount.RD = 4.0
world._append_batch(world.state, "RD", 4.0, 0.0, 0.0)
var lifespan: float = rd.lifespan
var width: float = rd.lifespan_delta
world._evolve_batches(world.state, lifespan)
check(world.state.amount.RD == 4.0, "Window start does not kill the whole batch")
world._evolve_batches(world.state, lifespan + width / 4.0)
check(world.state.amount.RD == 3.0 and world.state.amount.TWD == 1.0, "One quarter of a batch evolves")
world._evolve_batches(world.state, lifespan + width / 2.0)
check(world.state.amount.RD == 2.0 and world.state.amount.TWD == 2.0, "Half the batch evolves")
world._evolve_batches(world.state, lifespan + width)
check(world.state.amount.RD == 0.0 and world.state.amount.TWD == 4.0, "Window end completes evolution")
check(world.state.batches.RD.is_empty(), "Completed batch removed")
dispose(world)
# Creation accumulates independently for each star/planet pair.
world = fixture()
world.state.amount.RD = 100.0
for i in 100:
world._create_planets(world.state, "RD", 1.0, 0.0, 0.0, 1.0)
check(world.state.binds.RD.HP == 1.0, "100 RD at metallicity 1 produce one HP")
check(world.state.binds.RD.RP == 21.0, "Rocky planet probability retains fractions")
check(world.state.binds.RD.GG == 16.0 and world.state.binds.RD.AB == 120.0, "Planet multiplicity applied")
check(world.state.amount.HP == 0.0 and world.state.stats.total_amount.HP == 1.0, "Created planets are bound, counted once")
var saves: Node = root.get_node("SaveManager")
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Planet carry save validates")
restored = fixture()
restored.state = bytes_to_var(var_to_bytes(world.state))
world._create_planets(world.state, "RD", 37.0, 0.0, 0.0, 0.7)
restored._create_planets(restored.state, "RD", 37.0, 0.0, 0.0, 0.7)
check(world.state == restored.state, "Planet carry continues after save")
dispose(restored)
dispose(world)
# A partially dying dwarf sterilizes HP; survivors and RP move to its heir.
world = fixture()
world.state.amount.RD = 4.0
world.state.binds.RD = {"HP": 8.0, "RP": 4.0, "C1.2": 4.0, "C1.3": 4.0, "GG": 4.0}
world._append_batch(world.state, "RD", 4.0, 0.0, 0.0)
world._evolve_batches(world.state, lifespan + width / 2.0)
check(world.state.amount.RD == 2.0 and world.state.amount.TWD == 2.0, "Partial star transition with planets")
check(world.state.binds.RD.HP + world.state.binds.TWD.HP == 4.0, "Half of HP sterilized")
check(world.state.binds.RD["C1.3"] == 2.0 and world.state.binds.TWD["C1.3"] == 2.0, "Protected civilization splits between parent and heir")
check(world.state.binds.RD.RP + world.state.binds.TWD.RP == 10.0, "Sterilized planets become RP without loss")
check(world.state.amount.RP == 0.0 and world.state.amount["C1.3"] == 0.0, "Inherited planets never become free")
world._evolve_batches(world.state, lifespan + width)
check(world.state.binds.RD["C1.3"] == 0.0 and world.state.binds.TWD["C1.3"] == 4.0, "No planets left at extinct parent type")
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Inherited state validates")
dispose(world)
world = fixture()
world.state.amount.WS = 2.0
world.state.binds.WS = {"HP": 4.0, "C1.3": 4.0, "RP": 4.0, "GG": 4.0}
world._append_batch(world.state, "WS", 2.0, 0.0, 0.0)
world._evolve_batches(world.state, world.entities.WS.lifespan + world.entities.WS.lifespan_delta)
var physical: Dictionary = world._physical_amounts(world.state)
check(physical.HP == 0.0 and physical["C1.3"] == 0.0 and physical.RP == 0.0 and physical.GG == 0.0, "Level 2 removes all planets, including protected civilization")
check(world.state.amount.LRG == 2.0, "Giant evolves after local destruction")
dispose(world)
world = fixture()
world.state.amount.RD = 10.0
world.state.binds.RD = {"HP": 10.0, "C1.2": 10.0, "C1.3": 10.0, "RP": 0.0}
world._apply_blast(world.state, 5.0, {"blast": 1.0, "density": 1.0})
check(world.state.binds.RD.HP == 5.0 and world.state.binds.RD["C1.2"] == 5.0, "Blast damages vulnerable worlds proportionally")
check(world.state.binds.RD["C1.3"] == 10.0 and world.state.binds.RD.RP == 10.0, "Blast protects C1.3 and creates RP")
world._apply_blast(world.state, 1e6, {"blast": 1.0, "density": 1.0})
check(world.state.binds.RD.HP == 0.0 and world.state.binds.RD.RP == 20.0, "Overwhelming blast is capped at available planets")
check(not world.state.amount.has("blast"), "Blast is not a resource")
dispose(world)
world = fixture()
world.state.amount.RD = 1.0
world.state.binds.RD = {"HP": 1.0}
for i in 10:
world._apply_blast(world.state, 1.0, {"blast": 1.0, "density": 0.1})
check(world.state.binds.RD.HP == 0.0 and world.state.binds.RD.RP == 1.0, "Small blast effects accumulate across cycles")
dispose(world)
# Actual supernova output: one SN gives a neutron star, resources, and an event.
world = fixture()
world.state.amount.SN = 1.0
world._append_batch(world.state, "SN", 1.0, 0.0, 0.0)
world._evolve_batches(world.state, 2.0)
check(world.state.amount.SN == 0.0 and world.state.amount.LNS == 1.0, "Supernova produces a remnant")
check(world.state.amount.H == 8.0 and world.state.amount.He == 50.0 and world.state.amount.Met == 11.0, "Supernova returns table resources")
check(not world.state.amount.has("blast"), "Supernova dispatches blast output")
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Blast state validates")
dispose(world)
world = fixture()
world.state.amount.H = 1000.0
world.state.amount.Met = 100.0
check(world.produce("RD", 100.0) == 100.0, "Manual star creation succeeds")
check(world.state.binds.RD.HP >= 1.0, "Manual creation dispatches planet formation")
check(world.state.amount.H == 900.0, "Planet creation does not duplicate or spend stellar fuel")
dispose(world)
world = fixture()
world.state.amount.N = 0.0
world.state.amount.RD = 1.0
world.state.amount.H = 100.0
world.state.amount.Met = 100.0
world.state.auto_resources.H = 100.0
world.evolute(0.1)
check(world.state.amount.RD == 101.0 and world.state.amount.H == 0.0, "Automatic formation uses its resource budget")
check(world.state.binds.RD.HP >= 1.0, "Automatic formation also creates planets")
dispose(world)
world = fixture()
world.entities.N.auto = 1.0
# 1000 newly produced automatic H reserve 1 H, enough for 10 HBD.
world.evolute(100.0)
check(world.state.amount.HBD == 10.0, "Brown dwarfs form without existing ordinary stars")
check(world.state.amount.H == 999.0 and world.state.auto_resources.H == 999.0, "Brown dwarfs consume exactly their reserved hydrogen")
check(world.get_item_amounts().BD == 10.0 and world.state.batches.HBD.size() > 0, "Brown dwarfs appear in counter and evolution batches")
world.entities.N.product_per_year_amount = 0.0
world.evolute(100.0)
check(world.state.amount.HBD == 10.0, "Stored automatic budget is not taxed repeatedly")
dispose(world)
world = fixture()
world.entities.N.auto = 1.0
world.state.amount.RD = 1.0
world.evolute(50.0)
check(world.state.amount.HBD == 5.0 and is_equal_approx(world.state.auto_star_deficits.HBD_fuel, 0.5), "Individual dwarfs retain prepaid fractional fuel")
check(world.state.amount.H == 0.0, "Prepaid fuel is not available to ordinary stars")
var resumed = fixture()
resumed.entities.N.auto = 1.0
resumed.state = bytes_to_var(var_to_bytes(world.state))
world.evolute(50.0)
resumed.evolute(50.0)
check(world.state.amount.HBD == 10.0, "Accumulated quota eventually creates a whole batch")
check(world.state == resumed.state, "Brown dwarf quota survives serialization deterministically")
check(world.state.amount.RD == 1000.0 and world.state.amount.H == 0.0, "Shared automatic hydrogen is spent once")
world.state.batches.HBD = [{"amount": 10.0, "start_year": 101.0, "end_year": 101.0}]
world._evolve_batches(world.state, 101.0)
check(world.state.amount.HBD == 0.0 and world.state.amount.CBD == 10.0 and world.get_item_amounts().BD == 10.0, "Cooling preserves aggregate brown dwarf count")
dispose(resumed)
dispose(world)
world = fixture()
var saw_individual: bool = false
for i in range(250):
world.state.amount.H += 1.0
world.state.auto_resources.H = world.state.auto_resources.get("H", 0.0) + 1.0
var before: float = world.state.amount.HBD
world._produce_brown_dwarfs(world.state, 1.0, float(i), float(i + 1))
var born: float = world.state.amount.HBD - before
check(born == 0.0 or born == 1.0, "Small fuel increments produce individual dwarfs")
saw_individual = saw_individual or born == 1.0
check(saw_individual, "Random rounding does not wait for ten dwarfs")
resumed = fixture()
resumed.state = bytes_to_var(var_to_bytes(world.state))
for i in range(250, 500):
for simulation in [world, resumed]:
simulation.state.amount.H += 1.0
simulation.state.auto_resources.H += 1.0
simulation._produce_brown_dwarfs(simulation.state, 1.0, float(i), float(i + 1))
check(world.state == resumed.state, "Random sequence and prepaid fuel survive saving")
check(is_equal_approx(world.state.amount.H + world.state.amount.HBD * 0.1 + world.state.auto_star_deficits.HBD_fuel, 500.0), "Individual production conserves fuel including prepaid reserve")
check(is_equal_approx(world.state.amount.HBD + world.state.auto_star_deficits.HBD, 5.0), "Signed remainder preserves the 0.1 percent allocation")
var paused_count: float = world.state.amount.HBD
for i in 10: world._produce_brown_dwarfs(world.state, 0.0, 500.0, 501.0)
check(world.state.amount.HBD == paused_count, "No repeated random attempts without new fuel")
dispose(resumed)
dispose(world)
# Two simultaneous successors share surviving planets in the same 99:1 ratio.
world = fixture()
world.entities.HRG.destroy_level = 0
world.state.amount.HRG = 100.0
world.state.binds.HRG = {"C1.3": 100.0}
world.state.batches.HRG = [{"start_year": 0.1, "end_year": 0.1, "amount": 100.0}]
world._evolve_batches(world.state, 0.1)
check(world.state.binds.SN["C1.3"] == 99.0 and world.state.binds.GRB["C1.3"] == 1.0, "Planets split among actual alternative successors")
check(world.state.binds.HRG["C1.3"] == 0.0, "Alternative evolution leaves no orphan bindings")
dispose(world)
world = fixture()
world.state.amount.N = 0.0
world.state.amount.HRG = 1.0
world.state.batches.HRG = [{"start_year": 0.1, "end_year": 0.1, "amount": 1.0}]
world.evolute(3.0)
check(world.state.amount.LNS == 1.0 and world.state.amount.SN == 0.0, "Long step follows multiple stellar generations")
check(world.state.amount.S >= 99.0, "Short-lived newborn supernova emits S before disappearing")
dispose(world)
print("Stellar rules: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://ccegf5mell4ph
+91
View File
@@ -0,0 +1,91 @@
extends SceneTree
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks += 1
if not condition:
failures += 1
push_error(message)
func fixture():
var world = load("res://world/owniverse/owniverse.gd").new()
var global: Node = root.get_node("Global")
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
world.entities[id] = entity
world.state.amount[id] = 0.0
world.state.stats.total_amount[id] = 0.0
if entity.group_as != "":
world.state.amount[entity.group_as] = 0.0
world.state.stats.total_amount[entity.group_as] = 0.0
world.entities["N"].life_product = {}
world.entities["N"].product_per_year_type = "H"
world.entities["N"].product_per_year_amount = 10.0
world.entities["N"].auto = 0.0
world.state.amount.N = 1.0
return world
func dispose(world) -> void:
for entity in world.entities.values():
entity.free()
world.free()
func run() -> void:
var saves: Node = root.get_node("SaveManager")
var world = fixture()
world.evolute(0.09)
check(world.state.time.year == 0.0, "Do not process before a tenth boundary")
check(world.state.amount.H == 0.0, "No premature production")
world.evolute(0.01)
check(world.state.time.year == 0.1, "Process the first tenth")
check(world.state.amount.H == 1.0, "Production runs at 0.1 year")
world.evolute(0.2)
check(world.state.time.year == 0.3 and world.state.amount.H == 3.0, "Do not reprocess previous tenths")
var before: Dictionary = world.state.duplicate(true)
for invalid in [0.0, -1.0, NAN, INF, 1e308]:
world.evolute(invalid)
check(world.state == before, "Invalid/overflow interval rejected")
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Fractional save is valid")
var invalid_save: Dictionary = world.state.duplicate(true)
invalid_save.time.year = 0.25
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}) != "", "Reject inconsistent fractional clock")
dispose(world)
for fps in [30, 60, 144]:
world = fixture()
for frame in fps:
world.evolute(1.0 / fps)
check(world.state.time.year == 1.0, "One second at %d fps" % fps)
check(world.state.amount.H == 10.0, "Production across frames at %d fps" % fps)
dispose(world)
world = fixture()
world.evolute(0.05)
world.speed = 2
world.evolute(0.005)
check(world.state.time.year == 0.1, "Speed change preserves accumulated fraction")
world.speed = 100
check(world.speed == 5 and world.state.speed_in_year == 10000.0, "Upper speed limit unchanged")
world.speed = -10
check(world.speed == 1 and world.state.speed_in_year == 1.0, "Lower speed limit unchanged")
dispose(world)
world = fixture()
dispose(world)
world = fixture()
world.entities.N.output_and = {"He": 1.0}
world.entities.N.output_or = {}
world.state.batches.N = [{"start_year": 0.1, "end_year": 0.1, "amount": 1.0}]
world.evolute(0.1)
check(world.state.amount.N == 0.0 and world.state.amount.He == 1.0, "Existing evolution executes at fractional boundary")
dispose(world)
check(Clock.processed_year(0.099) == 0.0, "Do not round a genuine incomplete tenth forward")
check(Clock.processed_year(1e9 + 0.45) == 1e9 + 0.4, "Large ages preserve fractional precision")
print("Time state: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://bwon17tg8v8di
+55
View File
@@ -0,0 +1,55 @@
extends SceneTree
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(value: bool, message: String) -> void:
checks += 1
if not value:
failures += 1
push_error(message)
func run() -> void:
var scene = load("res://world/world.tscn").instantiate()
var stage = scene.get_node("Stage")
stage.owniverse.free()
stage.set_script(load("res://tests/fixtures/ui_stage.gd"))
root.add_child(scene)
await process_frame
await process_frame
stage.stage_page_scroller.scroll_horizontal = stage.PAGE_WIDTH
await process_frame
stage.owniverse.state.amount.H = 1000.0
stage.owniverse.state.amount.RD = 2.0
stage.owniverse.refresh_item_amounts()
stage._update_ui(0.0)
var hydrogen = stage.owniverse_items.H
var dwarf = stage.owniverse_items.RD
check(hydrogen.description.text == "1 000", "Visible page updates immediately on navigation")
check(dwarf.description.text == "", "Offscreen counters are not rendered")
stage.owniverse.state.amount.H = 2000.0
stage.owniverse.refresh_item_amounts()
check(stage.owniverse.item_amounts_dirty and stage.owniverse.items_to_update.H == 1000.0, "Simulation invalidates without recalculating display sums")
stage._update_ui(0.05)
check(hydrogen.description.text == "1 000", "No refresh before interval")
stage._update_ui(0.051)
check(hydrogen.description.text == "2 000" and not stage.owniverse.item_amounts_dirty, "Latest state appears at interval")
stage.item_activated(hydrogen)
check(hydrogen.description.text == root.get_node("Global").format_number(stage.owniverse.state.amount.H), "Player production updates counter immediately")
check(stage.owniverse.state.amount.H > 2000.0, "Production action actually executed")
dwarf.visible = true
stage.stage_page_scroller.scroll_horizontal = 2 * stage.PAGE_WIDTH
await process_frame
stage._update_ui(0.0)
check(dwarf.description.text == "2", "Newly visible page receives cached latest values")
stage.owniverse.state.time.year = 1234.5
stage._update_ui(0.1)
check(stage.date_time.text == "1 234.5", "Time uses throttled display with tenths")
for entity in stage.owniverse.entities.values(): entity.free()
stage.owniverse.free()
scene.free()
print("UI updates: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://c8bt2hxqm4hsf
+190
View File
@@ -0,0 +1,190 @@
extends SceneTree
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.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.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._append_batch(world.state, "RD", 400.0, 0.0, 0.0)
var end: float = world.state.batches.RD[0].end_year
world.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.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.modify_world_parameter("Bur", 9.0)
world._append_batch(world.state, "YD", 10.0, 0.0, 0.0)
check(is_equal_approx(world.state.batches.YD[0].start_year, world.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._withdraw_batches(world.state, "RD", world.state.time.year)
world.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.modify_world_parameter("Exp", 9.0)
check(world._evolution_products(world.state, world.entities.SN, 2.0).blast == 180.0, "Explosion power includes world explosiveness")
var products: Dictionary = world._evolution_products(world.state, world.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.entities.HRG.output_or.chance = 50.0
products = world._evolution_products(world.state, world.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_products(world.state, world.entities.HP, 1000000.0)
world.state.life_remainders.clear()
world.modify_world_parameter("Agg", 9.0)
var aggressive: Dictionary = world._life_products(world.state, world.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_products(world.state, world.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_products(world.state, world.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._append_batch(simulation.state, "RD", 1000000.0, 0.0, 0.0)
hotter.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 = world.CivilizationState.stellar_budget(world, target, global.CivilizationRules)
world.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)
+1
View File
@@ -0,0 +1 @@
uid://dmldg06dx3utl

Some files were not shown because too many files have changed in this diff Show More