Add core simulation modules for owniverse mechanics
- Introduced `life_evolution.gd` to manage life evolution processes. - Implemented `manual_production.gd` for handling production mechanics, including resource management and production steps. - Created `object_batches.gd` to manage batches of objects, including scaling and appending batches. - Developed `object_ledger.gd` for tracking physical amounts, available resources, and binding ownerships. - Added `simulation_module.gd` as a base class for simulation modules, providing a weak reference to the world. - Established `stellar_rules.gd` for managing stellar evolution and destruction mechanics. - Introduced `universe_environment.gd` to calculate environmental parameters based on the current state of the universe.
This commit is contained in:
+18
-18
@@ -106,10 +106,10 @@ func set_datetime(value: float):
|
|||||||
|
|
||||||
|
|
||||||
func set_owniverse_item_value() -> void:
|
func set_owniverse_item_value() -> void:
|
||||||
for item_id in owniverse.items_to_update:
|
for item_id in owniverse.presentation.pending_amounts:
|
||||||
if owniverse_items.has(item_id):
|
if owniverse_items.has(item_id):
|
||||||
owniverse_items[item_id].set_label(owniverse.items_to_update[item_id])
|
owniverse_items[item_id].set_label(owniverse.presentation.pending_amounts[item_id])
|
||||||
owniverse.items_to_update.clear()
|
owniverse.presentation.pending_amounts.clear()
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -127,7 +127,7 @@ func _on_indicator_pressed(indicator_name: String, page: int = 0):
|
|||||||
|
|
||||||
|
|
||||||
func _set_item_description(item: OwniverseItem) -> void:
|
func _set_item_description(item: OwniverseItem) -> void:
|
||||||
var category: String = "Entity" if owniverse.entities.has(item.item_id) else "Force"
|
var category: String = "Entity" if owniverse.catalog.entities.has(item.item_id) else "Force"
|
||||||
var id: String = item.item_id if category == "Entity" else item.item_name.capitalize()
|
var id: String = item.item_id if category == "Entity" else item.item_name.capitalize()
|
||||||
var page: int = clampi(item.item_page, 0, 4)
|
var page: int = clampi(item.item_page, 0, 4)
|
||||||
if description_targets.get(page, {}).get("id", "") != id:
|
if description_targets.get(page, {}).get("id", "") != id:
|
||||||
@@ -215,7 +215,7 @@ func item_activated(item: OwniverseItem) -> void:
|
|||||||
if item.item_id == "S" or item.item_id == "H":
|
if item.item_id == "S" or item.item_id == "H":
|
||||||
var _value = owniverse.state.amount[item.item_id]
|
var _value = owniverse.state.amount[item.item_id]
|
||||||
_value = 9 * _value if _value > 0 else 1
|
_value = 9 * _value if _value > 0 else 1
|
||||||
owniverse.produce_hs(item.item_id, _value)
|
owniverse.production.produce_hs(item.item_id, _value)
|
||||||
|
|
||||||
|
|
||||||
func _check_counters(delta: float) -> Array:
|
func _check_counters(delta: float) -> Array:
|
||||||
@@ -276,16 +276,16 @@ func _set_active_page_actions() -> void:
|
|||||||
if !g_active_items.has(pa): continue
|
if !g_active_items.has(pa): continue
|
||||||
|
|
||||||
var item_id: String = g_active_items[pa].item_id
|
var item_id: String = g_active_items[pa].item_id
|
||||||
var entity: OwniverseEntity = owniverse.entities[item_id]
|
var entity: OwniverseEntity = owniverse.catalog.entities[item_id]
|
||||||
|
|
||||||
#var amount = owniverse.get_available_to_produce(item_id)
|
#var amount = owniverse.get_available_to_produce(item_id)
|
||||||
|
|
||||||
#var amount = owniverse.get_available_to_produce(entity)
|
#var amount = owniverse.get_available_to_produce(entity)
|
||||||
var amount = owniverse.calculate_max_producible(entity)
|
var amount = owniverse.production.calculate_max_producible(entity)
|
||||||
#print(item_id, " : ", amount)
|
#print(item_id, " : ", amount)
|
||||||
if item_id == "SMBH":
|
if item_id == "SMBH":
|
||||||
var preview: Callable = func(requested: float) -> float:
|
var preview: Callable = func(requested: float) -> float:
|
||||||
return owniverse.calculate_max_producible(entity, requested)
|
return owniverse.production.calculate_max_producible(entity, requested)
|
||||||
page_actions[pa].set_actions(item_id, amount, preview)
|
page_actions[pa].set_actions(item_id, amount, preview)
|
||||||
else:
|
else:
|
||||||
page_actions[pa].set_actions(item_id, amount)
|
page_actions[pa].set_actions(item_id, amount)
|
||||||
@@ -298,7 +298,7 @@ func _page_action_pressed(page_id: int, action_amount: float) -> void:
|
|||||||
var item_id: String = g_active_items[page_id].item_id
|
var item_id: String = g_active_items[page_id].item_id
|
||||||
|
|
||||||
if action_amount > 0:
|
if action_amount > 0:
|
||||||
owniverse.produce(item_id, action_amount)
|
owniverse.production.produce(item_id, action_amount)
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -325,8 +325,8 @@ func create_new_universe() -> void:
|
|||||||
owniverse = Owniverse.new()
|
owniverse = Owniverse.new()
|
||||||
var inited_result: Dictionary = init_enitities()
|
var inited_result: Dictionary = init_enitities()
|
||||||
locked_items = inited_result.locked_items
|
locked_items = inited_result.locked_items
|
||||||
owniverse.new_state = owniverse.state
|
|
||||||
for entity in previous.entities.values():
|
for entity in previous.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
previous.free()
|
previous.free()
|
||||||
|
|
||||||
@@ -354,7 +354,7 @@ func create_new_universe() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _refresh_universe_ui() -> void:
|
func _refresh_universe_ui() -> void:
|
||||||
owniverse.refresh_item_amounts()
|
owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
|
||||||
_change_speed(0)
|
_change_speed(0)
|
||||||
set_datetime(owniverse.state.time.year)
|
set_datetime(owniverse.state.time.year)
|
||||||
set_owniverse_item_value()
|
set_owniverse_item_value()
|
||||||
@@ -437,9 +437,9 @@ func load_data() -> bool:
|
|||||||
var complete: Dictionary = owniverse.state.duplicate(true)
|
var complete: Dictionary = owniverse.state.duplicate(true)
|
||||||
complete.merge(restored, true)
|
complete.merge(restored, true)
|
||||||
owniverse.state = complete
|
owniverse.state = complete
|
||||||
owniverse.new_state = complete
|
|
||||||
owniverse.initialize_life_batches()
|
owniverse.evolution.initialize_life_batches()
|
||||||
owniverse.refresh_item_amounts()
|
owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
|
||||||
pending_page = clampi(int(game_data.get("ui", {}).get("current_page", 0)), 0, PAGE_COUNT - 1)
|
pending_page = clampi(int(game_data.get("ui", {}).get("current_page", 0)), 0, PAGE_COUNT - 1)
|
||||||
_restore_page.call_deferred()
|
_restore_page.call_deferred()
|
||||||
return true
|
return true
|
||||||
@@ -491,14 +491,14 @@ func init_enitities() -> Dictionary:
|
|||||||
'locked_items': [],
|
'locked_items': [],
|
||||||
}
|
}
|
||||||
owniverse.catalog.load_definitions(Global.OwniverseEntities)
|
owniverse.catalog.load_definitions(Global.OwniverseEntities)
|
||||||
for entity_id in owniverse.entities:
|
for entity_id in owniverse.catalog.entities:
|
||||||
var entity: OwniverseEntity = owniverse.entities[entity_id]
|
var entity: OwniverseEntity = owniverse.catalog.entities[entity_id]
|
||||||
|
|
||||||
owniverse.state.stats.total_amount[entity_id] = 0
|
owniverse.state.stats.total_amount[entity_id] = 0
|
||||||
owniverse.state.amount[entity_id] = 0
|
owniverse.state.amount[entity_id] = 0
|
||||||
if entity.lifespan > 0:
|
if entity.lifespan > 0:
|
||||||
owniverse.state.batches[entity.id] = []
|
owniverse.state.batches[entity.id] = []
|
||||||
owniverse.add_entity_to_batch(entity_id, 0, 0)
|
owniverse.batches.add_entity_to_batch(entity_id, 0, 0)
|
||||||
if entity.group_as != "":
|
if entity.group_as != "":
|
||||||
owniverse.state.amount[entity.group_as] = 0
|
owniverse.state.amount[entity.group_as] = 0
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -1,5 +1,11 @@
|
|||||||
# Проверки поэтапного переноса из Owniverse.U
|
# Проверки поэтапного переноса из Owniverse.U
|
||||||
|
|
||||||
|
## Модули симуляции
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_module_lifecycle.gd`
|
||||||
|
|
||||||
|
Проверяются отдельные экземпляры модулей и состояния для каждой вселенной, слабая обратная ссылка, освобождение модулей вместе с миром и защита от некорректного delta. Остальные тесты используют прямые API batches, ledger, production, automatic, stellar, life, environment и evolution; временные методы-переадресации Owniverse удалены. Описание границ модулей и порядка фаз: `../world/owniverse/README.md`.
|
||||||
|
|
||||||
## Математика распределений и каталог
|
## Математика распределений и каталог
|
||||||
|
|
||||||
`godot --headless --path Owniverse.GI --script res://tests/test_quantity_catalog.gd`
|
`godot --headless --path Owniverse.GI --script res://tests/test_quantity_catalog.gd`
|
||||||
@@ -43,7 +49,7 @@ godot --headless --path Owniverse.GI --script res://tests/test_ui_updates.gd
|
|||||||
|
|
||||||
Тест сохранений пишет только в уникальную временную папку `/tmp`, затем удаляет собственные файлы. Проверяются независимость снимка, работа кадров при записи, объединение запросов, диагностическая копия, сохранность предыдущего файла при ошибках и завершение очереди при уничтожении менеджера. Сообщения `Invalid save`, `Save blocked` и `Cannot write` в этом тесте ожидаемы.
|
Тест сохранений пишет только в уникальную временную папку `/tmp`, затем удаляет собственные файлы. Проверяются независимость снимка, работа кадров при записи, объединение запросов, диагностическая копия, сохранность предыдущего файла при ошибках и завершение очереди при уничтожении менеджера. Сообщения `Invalid save`, `Save blocked` и `Cannot write` в этом тесте ожидаемы.
|
||||||
|
|
||||||
Счётчики, время и кнопки производства обновляются раз в 0,1 секунды. Изменения симуляции помечают суммы для пересчёта; сами суммы вычисляются при обновлении интерфейса. Текст счётчиков применяется только к видимым элементам и только при изменении строки. Переход между страницами, выбор элемента и производство вызывают немедленное обновление. Описания сохраняют прежний интервал 0,25 секунды. Тест интерфейса использует игровую сцену с отключёнными загрузкой, сохранением и автоматической симуляцией, не обращаясь к сохранению игрока.
|
Тест интерфейса проверяет передачу количеств из presentation в контролы, очистку очереди после публикации, отложенное и принудительное обновление, производство по нажатию и формат времени. Stage в текущей реализации применяет очередь счётчиков и время каждый кадр; интервал 0,1 секунды используется для индикаторов. Рефакторинг этот порядок не меняет. Тест использует игровую сцену с отключёнными загрузкой, сохранением и автоматической симуляцией, не обращаясь к сохранению игрока.
|
||||||
|
|
||||||
## Время: шаг 0,1 игрового года
|
## Время: шаг 0,1 игрового года
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
extends "res://world/owniverse/owniverse.gd"
|
extends "res://world/owniverse/owniverse.gd"
|
||||||
|
|
||||||
# Keep the old materialized-batch path as an independent comparison mode.
|
# Compare with the materialized-batch path, without overriding the coordinator.
|
||||||
func _uses_short_lived_fast_path(_id: String) -> bool:
|
func _init() -> void:
|
||||||
return false
|
stellar.direct_completions = false
|
||||||
|
|||||||
@@ -17,21 +17,21 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0.0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0.0
|
||||||
if entity.group_as != "":
|
if entity.group_as != "":
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0.0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0.0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1.0
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ func run() -> void:
|
|||||||
var boundary: float = 41215289.79999999701976776
|
var boundary: float = 41215289.79999999701976776
|
||||||
for interval in [[boundary - 2.5, boundary + 2.25], [boundary, boundary + 2.0]]:
|
for interval in [[boundary - 2.5, boundary + 2.25], [boundary, boundary + 2.0]]:
|
||||||
world.state.batches.LGRB = []
|
world.state.batches.LGRB = []
|
||||||
world._append_batch(world.state, "LGRB", 100.0, interval[0], interval[1])
|
world.batches.append_batch(world.state, "LGRB", 100.0, interval[0], interval[1])
|
||||||
var total: float = 0.0
|
var total: float = 0.0
|
||||||
var last: float = -INF
|
var last: float = -INF
|
||||||
for batch: Dictionary in world.state.batches.LGRB:
|
for batch: Dictionary in world.state.batches.LGRB:
|
||||||
|
|||||||
@@ -17,21 +17,21 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0.0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0.0
|
||||||
if entity.group_as != "":
|
if entity.group_as != "":
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0.0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0.0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1.0
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ func seed_population(world, id: String, count: float, time: float = 1.0) -> void
|
|||||||
world.state.amount.N = 0.0
|
world.state.amount.N = 0.0
|
||||||
world.state.amount[id] = count
|
world.state.amount[id] = count
|
||||||
world.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}]
|
world.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}]
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
|
|
||||||
func sum_ethics(world, id: String) -> float:
|
func sum_ethics(world, id: String) -> float:
|
||||||
var result: float = 0.0
|
var result: float = 0.0
|
||||||
@@ -47,7 +47,7 @@ func sum_ethics(world, id: String) -> float:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
func invariants(world) -> void:
|
func invariants(world) -> void:
|
||||||
var physical: Dictionary = world._physical_amounts(world.state)
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
for id in world.state.ethics:
|
for id in world.state.ethics:
|
||||||
check(sum_ethics(world, id) == physical.get(id, 0.0), "Ethics equal physical population for " + id)
|
check(sum_ethics(world, id) == physical.get(id, 0.0), "Ethics equal physical population for " + id)
|
||||||
for n in world.state.ethics[id].values():
|
for n in world.state.ethics[id].values():
|
||||||
@@ -59,7 +59,7 @@ func run() -> void:
|
|||||||
var baseline: Dictionary = global.CivilizationRules.duplicate(true)
|
var baseline: Dictionary = global.CivilizationRules.duplicate(true)
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
seed_population(world, "C0.2", 60000.0)
|
seed_population(world, "C0.2", 60000.0)
|
||||||
world._evolve_batches(world.state, 1.0)
|
world.evolution.evolve_batches(world.state, 1.0)
|
||||||
check(world.state.amount["C0.3"] > 0.0, "C0.2 actually progresses")
|
check(world.state.amount["C0.3"] > 0.0, "C0.2 actually progresses")
|
||||||
check(world.state.amount["C0.1"] > 0.0, "C0.2 actually degrades")
|
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.civilization_stats.get("wars", 0.0) == 0.0, "Table disables early wars")
|
||||||
@@ -69,8 +69,8 @@ func run() -> void:
|
|||||||
invariants(world)
|
invariants(world)
|
||||||
var restored = fixture()
|
var restored = fixture()
|
||||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||||
world._evolve_batches(world.state, 2100.0)
|
world.evolution.evolve_batches(world.state, 2100.0)
|
||||||
restored._evolve_batches(restored.state, 2100.0)
|
restored.evolution.evolve_batches(restored.state, 2100.0)
|
||||||
check(world.state == restored.state, "Save resumes deterministic ethical evolution")
|
check(world.state == restored.state, "Save resumes deterministic ethical evolution")
|
||||||
invariants(world)
|
invariants(world)
|
||||||
dispose(restored)
|
dispose(restored)
|
||||||
@@ -80,8 +80,8 @@ func run() -> void:
|
|||||||
world.state.amount.RD = 1e8
|
world.state.amount.RD = 1e8
|
||||||
seed_population(world, "C2.0", 60000.0)
|
seed_population(world, "C2.0", 60000.0)
|
||||||
world.state.amount["C1.9"] = 10000.0
|
world.state.amount["C1.9"] = 10000.0
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
world._evolve_batches(world.state, 1.0)
|
world.evolution.evolve_batches(world.state, 1.0)
|
||||||
check(world.state.civilization_stats.wars > 0.0, "Wars execute when enabled")
|
check(world.state.civilization_stats.wars > 0.0, "Wars execute when enabled")
|
||||||
check(world.state.civilization_stats.collateral > 0.0, "Wars affect previous-stage populations")
|
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")
|
check(world.state.amount["C2.1"] > 0.0, "Winners and peaceful progress reach next stage")
|
||||||
@@ -93,14 +93,14 @@ func run() -> void:
|
|||||||
world.state.binds.RD = {"HP": 5.0, "GG": 5.0, "AB": 5.0, "RP": 5.0}
|
world.state.binds.RD = {"HP": 5.0, "GG": 5.0, "AB": 5.0, "RP": 5.0}
|
||||||
seed_population(world, "C1.6", 60000.0)
|
seed_population(world, "C1.6", 60000.0)
|
||||||
for row: Dictionary in global.CivilizationRules["C1.6"].ethics.values(): row.degradation = "0"
|
for row: Dictionary in global.CivilizationRules["C1.6"].ethics.values(): row.degradation = "0"
|
||||||
world._evolve_batches(world.state, 1.0)
|
world.evolution.evolve_batches(world.state, 1.0)
|
||||||
var after: Dictionary = world._physical_amounts(world.state)
|
var after: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
check(world.state.civilization_stats.colonies == 20.0, "Colonization limited to real targets")
|
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(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")
|
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
|
var totals: float = 0.0
|
||||||
for id in after:
|
for id in after:
|
||||||
if world.entities[id].group_as == "Planet": totals += after[id]
|
if world.catalog.entities[id].group_as == "Planet": totals += after[id]
|
||||||
check(totals == 60020.0, "Colonization replaces planets, does not create extra worlds")
|
check(totals == 60020.0, "Colonization replaces planets, does not create extra worlds")
|
||||||
invariants(world)
|
invariants(world)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
@@ -114,8 +114,8 @@ func run() -> void:
|
|||||||
world.state.amount.MGT = 1e25
|
world.state.amount.MGT = 1e25
|
||||||
world.state.amount.BH25 = 1e25
|
world.state.amount.BH25 = 1e25
|
||||||
world.state.amount.SMBH = 1e26
|
world.state.amount.SMBH = 1e26
|
||||||
for id in world.entities:
|
for id in world.catalog.entities:
|
||||||
if world.entities[id].claim != "": world.state.amount[id] = 1e12
|
if world.catalog.entities[id].claim != "": world.state.amount[id] = 1e12
|
||||||
# Accelerated deterministic fixture traverses every enabled stage, including Ct.
|
# Accelerated deterministic fixture traverses every enabled stage, including Ct.
|
||||||
for id in global.CivilizationRules:
|
for id in global.CivilizationRules:
|
||||||
if id == "Ct": continue
|
if id == "Ct": continue
|
||||||
@@ -125,11 +125,11 @@ func run() -> void:
|
|||||||
for row: Dictionary in global.CivilizationRules[id].ethics.values():
|
for row: Dictionary in global.CivilizationRules[id].ethics.values():
|
||||||
row.progress = "1"
|
row.progress = "1"
|
||||||
row.degradation = "0"
|
row.degradation = "0"
|
||||||
world.entities[id].lifespan = 1.0
|
world.catalog.entities[id].lifespan = 1.0
|
||||||
world.entities[id].lifespan_delta = 1.0
|
world.catalog.entities[id].lifespan_delta = 1.0
|
||||||
global.CivilizationRules[id].span = 1.0
|
global.CivilizationRules[id].span = 1.0
|
||||||
seed_population(world, "C0.2", 6.0)
|
seed_population(world, "C0.2", 6.0)
|
||||||
world._evolve_batches(world.state, 1000.0)
|
world.evolution.evolve_batches(world.state, 1000.0)
|
||||||
check(world.state.amount.Ct == 6.0, "Complete C0.2 -> C4.4 -> Ct chain runs")
|
check(world.state.amount.Ct == 6.0, "Complete C0.2 -> C4.4 -> Ct chain runs")
|
||||||
check(not world.state.batches.has("Ct"), "Transcendence is terminal")
|
check(not world.state.batches.has("Ct"), "Transcendence is terminal")
|
||||||
invariants(world)
|
invariants(world)
|
||||||
@@ -140,7 +140,7 @@ func run() -> void:
|
|||||||
world.state.amount.RD = 10000.0
|
world.state.amount.RD = 10000.0
|
||||||
world.state.amount.SCS = 1.0
|
world.state.amount.SCS = 1.0
|
||||||
world.state.amount["C2.4"] = 1.0
|
world.state.amount["C2.4"] = 1.0
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
var span: float = world.state.batches["C2.4"][0].start_year
|
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")
|
check(is_equal_approx(span, 20000.0 / 3.6), "Structure bonus shortens newly registered lifespan")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
@@ -156,7 +156,7 @@ func run() -> void:
|
|||||||
world.state.amount.RD = 1e15
|
world.state.amount.RD = 1e15
|
||||||
world.state.amount.BG = 1e15
|
world.state.amount.BG = 1e15
|
||||||
world.state.amount["C3.0"] = 6.0
|
world.state.amount["C3.0"] = 6.0
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.amount["C3.0"] == 0.0 and world.state.amount["C2.9"] == 6.0, "Missing mandatory structures degrade existing civilizations")
|
check(world.state.amount["C3.0"] == 0.0 and world.state.amount["C2.9"] == 6.0, "Missing mandatory structures degrade existing civilizations")
|
||||||
invariants(world)
|
invariants(world)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|||||||
@@ -17,21 +17,21 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0.0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0.0
|
||||||
if entity.group_as != "":
|
if entity.group_as != "":
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0.0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0.0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1.0
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
@@ -44,24 +44,24 @@ func run() -> void:
|
|||||||
world.state.amount.RD = 12.0
|
world.state.amount.RD = 12.0
|
||||||
world.state.amount["C0.2"] = 8.0
|
world.state.amount["C0.2"] = 8.0
|
||||||
world.state.binds.RD = {"C2.0": 12.0, "C1.9": 3.0}
|
world.state.binds.RD = {"C2.0": 12.0, "C1.9": 3.0}
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.amount["C2.0"] == 12.0 and world.state.binds.RD["C2.0"] == 0.0, "Old space civilizations detach without loss")
|
check(world.state.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.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["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")
|
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)
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state == before, "Repeated initialization preserves ethics")
|
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.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()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.ethics["C0.2"].m == 8.0, "Existing unequal distribution is retained")
|
check(world.state.ethics["C0.2"].m == 8.0, "Existing unequal distribution is retained")
|
||||||
world.state.amount["C0.2"] = 3.0
|
world.state.amount["C0.2"] = 3.0
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.ethics["C0.2"].m == 3.0, "Losses reduce ethics without inventing population")
|
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")
|
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()
|
var restored = fixture()
|
||||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||||
restored.initialize_life_batches()
|
restored.evolution.initialize_life_batches()
|
||||||
check(restored.state == world.state, "Save roundtrip preserves ethics and ownership")
|
check(restored.state == world.state, "Save roundtrip preserves ethics and ownership")
|
||||||
dispose(restored)
|
dispose(restored)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
@@ -73,7 +73,7 @@ func run() -> void:
|
|||||||
row.colonization = "0"
|
row.colonization = "0"
|
||||||
world.state.binds.RD = {"C1.9": 10.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.state.batches["C1.9"] = [{"amount": 10.0, "start_year": 1.0, "end_year": 1.0}]
|
||||||
world._evolve_batches(world.state, 1.0)
|
world.evolution.evolve_batches(world.state, 1.0)
|
||||||
check(world.state.amount["C2.0"] == 10.0 and world.state.binds.RD.get("C2.0", 0.0) == 0.0, "Space successor does not inherit planetary binding")
|
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)
|
dispose(world)
|
||||||
var capacity = load("res://world/owniverse/civilization_state.gd")
|
var capacity = load("res://world/owniverse/civilization_state.gd")
|
||||||
@@ -97,25 +97,25 @@ func run() -> void:
|
|||||||
world.state.amount.RD = 12.0
|
world.state.amount.RD = 12.0
|
||||||
world.state.amount["C2.1"] = 2.0
|
world.state.amount["C2.1"] = 2.0
|
||||||
world.state.amount["C2.0"] = 5.0
|
world.state.amount["C2.0"] = 5.0
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.amount["C2.1"] == 1.0 and world.state.amount["C2.0"] == 2.0, "Shared budget supports advanced civilizations first")
|
check(world.state.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")
|
check(world.state.binds.RD["C1.9"] == 4.0, "Excess cascades to supported planetary stage")
|
||||||
var snapshot: Dictionary = world.state.duplicate(true)
|
var snapshot: Dictionary = world.state.duplicate(true)
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state == snapshot, "Capacity enforcement is idempotent")
|
check(world.state == snapshot, "Capacity enforcement is idempotent")
|
||||||
world.state.amount.RD = 0.0
|
world.state.amount.RD = 0.0
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.amount["C2.1"] == 0.0 and world.state.amount["C2.0"] == 0.0, "Vanished stars force all unsupported space civilizations down")
|
check(world.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")
|
check(world.ledger.physical_amounts(world.state)["C1.9"] == 7.0, "Forced degradation conserves civilizations")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.LGW = 20.0
|
world.state.amount.LGW = 20.0
|
||||||
world.state.amount.SMBH = 2.0e22
|
world.state.amount.SMBH = 2.0e22
|
||||||
world.state.amount["C4.2"] = 3.0
|
world.state.amount["C4.2"] = 3.0
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.amount["C4.2"] == 2.0, "SMBH capacity uses stellar mass equivalent")
|
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(world.ledger.physical_amounts(world.state)["C1.9"] == 1.0, "Unsupported civilization degrades through missing habitat families")
|
||||||
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Capacity-limited state validates")
|
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)
|
dispose(world)
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ func run() -> void:
|
|||||||
row.colonization = "0"
|
row.colonization = "0"
|
||||||
world.state.binds.RD = {"C1.9": 5.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.state.batches["C1.9"] = [{"amount": 5.0, "start_year": 1.0, "end_year": 1.0}]
|
||||||
world._evolve_batches(world.state, 1.0)
|
world.evolution.evolve_batches(world.state, 1.0)
|
||||||
check(world.state.amount["C2.0"] == 2.0 and world.state.binds.RD["C1.9"] == 3.0, "Unfunded transition retains the original planetary civilization")
|
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)
|
dispose(world)
|
||||||
print("Civilization state: %d checks, %d failures" % [checks, failures])
|
print("Civilization state: %d checks, %d failures" % [checks, failures])
|
||||||
|
|||||||
@@ -17,21 +17,21 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0.0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0.0
|
||||||
if entity.group_as != "":
|
if entity.group_as != "":
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0.0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0.0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1.0
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
@@ -65,11 +65,11 @@ func run() -> void:
|
|||||||
world.state.amount.EON1 = 2.0
|
world.state.amount.EON1 = 2.0
|
||||||
world.state.binds.G.EON2 = 3.0
|
world.state.binds.G.EON2 = 3.0
|
||||||
var saved_state: Dictionary = world.state.duplicate(true)
|
var saved_state: Dictionary = world.state.duplicate(true)
|
||||||
var counters: Dictionary = world.get_item_amounts()
|
var counters: Dictionary = world.presentation.amounts(world.state, world.catalog)
|
||||||
check(counters.HP == 7.0 and counters.LP == 5.0 and counters.Planet == 12.0, "Planet counters include bound objects and aggregate eons")
|
check(counters.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(counters.RD == 2.0, "Star counters retain available quantity")
|
||||||
check(world.state == saved_state, "Reading counters does not mutate simulation")
|
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")
|
check(world.presentation.amounts(world.state, world.catalog, true).HP == 0.0, "Produced counters remain independent of current population")
|
||||||
values = description.variables(world, global, "RD", 2.0)
|
values = description.variables(world, global, "RD", 2.0)
|
||||||
check(values.amount.contains("#40cc40") and values.amount.contains("+2.0"), "Increase green")
|
check(values.amount.contains("#40cc40") and values.amount.contains("+2.0"), "Increase green")
|
||||||
var stage_script = load("res://stage/stage.gd")
|
var stage_script = load("res://stage/stage.gd")
|
||||||
|
|||||||
+19
-19
@@ -17,42 +17,42 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0.0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0.0
|
||||||
if entity.group_as != "":
|
if entity.group_as != "":
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0.0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0.0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1.0
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
for id in world.entities:
|
for id in world.catalog.entities:
|
||||||
var expected: Array = []
|
var expected: Array = []
|
||||||
for key in world.entities:
|
for key in world.catalog.entities:
|
||||||
if world.entities[key].group_as == id:
|
if world.catalog.entities[key].group_as == id:
|
||||||
expected.append(key)
|
expected.append(key)
|
||||||
check(world._is_group(id) == (not expected.is_empty()), "Group classification: " + id)
|
check(world.catalog.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)
|
check(world.catalog.members(id) == (expected if not expected.is_empty() else [id]), "Group members: " + id)
|
||||||
var members: Array = world._members("Star")
|
var members: Array = world.catalog.members("Star")
|
||||||
members.clear()
|
members.clear()
|
||||||
check(not world._members("Star").is_empty(), "Returned members are independent")
|
check(not world.catalog.members("Star").is_empty(), "Returned members are independent")
|
||||||
world.entities.N.group_as = "TestGroup"
|
world.catalog.entities.N.group_as = "TestGroup"
|
||||||
world.rebuild_group_index()
|
world.catalog.rebuild_group_index()
|
||||||
check(world._members("TestGroup") == ["N"], "Explicit definition reload")
|
check(world.catalog.members("TestGroup") == ["N"], "Explicit definition reload")
|
||||||
world.entities.N.free()
|
world.catalog.entities.N.free()
|
||||||
world.entities.erase("N")
|
world.catalog.entities.erase("N")
|
||||||
check(not world._is_group("TestGroup"), "Removed definition invalidates index")
|
check(not world.catalog.is_group("TestGroup"), "Removed definition invalidates index")
|
||||||
print("Group index: %d checks, %d failures" % [checks, failures])
|
print("Group index: %d checks, %d failures" % [checks, failures])
|
||||||
dispose(world)
|
dispose(world)
|
||||||
quit(1 if failures else 0)
|
quit(1 if failures else 0)
|
||||||
|
|||||||
+24
-24
@@ -17,21 +17,21 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0.0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0.0
|
||||||
if entity.group_as != "":
|
if entity.group_as != "":
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0.0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0.0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1.0
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
@@ -39,25 +39,25 @@ func run() -> void:
|
|||||||
var world = fixture()
|
var world = fixture()
|
||||||
world.state.amount.N = 0.0
|
world.state.amount.N = 0.0
|
||||||
world.state.amount.HP = 40000.0
|
world.state.amount.HP = 40000.0
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.batches.HP.size() == 1, "Old HP receives life cohort")
|
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")
|
check(world.state.batches.HP[0].start_year == 5e6, "HP uses source span")
|
||||||
var before: Dictionary = world.state.duplicate(true)
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state == before, "Initialization is idempotent")
|
check(world.state == before, "Initialization is idempotent")
|
||||||
world._evolve_batches(world.state, 5e6 + 5e6 / 40.0)
|
world.evolution.evolve_batches(world.state, 5e6 + 5e6 / 40.0)
|
||||||
check(world.state.amount.EON1 == 10.0, "HP abiogenesis follows source probability")
|
check(world.state.amount.EON1 == 10.0, "HP abiogenesis follows source probability")
|
||||||
check(world.state.amount.RP == 100.0, "HP degradation produces RP")
|
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.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.stats.total_amount.HP == 0.0, "Retry does not inflate creation statistics")
|
||||||
check(world.state.batches.HP.size() > 0, "Survivors get next attempt")
|
check(world.state.batches.HP.size() > 0, "Survivors get next attempt")
|
||||||
var physical: Dictionary = world._physical_amounts(world.state)
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
check(physical.HP + physical.EON1 + physical.RP == 40000.0, "Life conserves planets")
|
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")
|
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()
|
var restored = fixture()
|
||||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||||
world._evolve_batches(world.state, 11e6)
|
world.evolution.evolve_batches(world.state, 11e6)
|
||||||
restored._evolve_batches(restored.state, 11e6)
|
restored.evolution.evolve_batches(restored.state, 11e6)
|
||||||
check(world.state == restored.state, "Life continuation preserves cohorts and carries")
|
check(world.state == restored.state, "Life continuation preserves cohorts and carries")
|
||||||
dispose(restored)
|
dispose(restored)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
@@ -65,8 +65,8 @@ func run() -> void:
|
|||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 1.0
|
world.state.amount.RD = 1.0
|
||||||
world.state.binds.RD = {"EON5": 40000.0}
|
world.state.binds.RD = {"EON5": 40000.0}
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
world._evolve_batches(world.state, 5e5 + 5e5 / 40.0)
|
world.evolution.evolve_batches(world.state, 5e5 + 5e5 / 40.0)
|
||||||
check(world.state.binds.RD["C0.0"] == 10.0, "Intelligence emerges at the same host star")
|
check(world.state.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.amount["C0.0"] == 0.0, "Life transition keeps planet bound")
|
||||||
check(world.state.batches.has("C0.0"), "New C0.0 starts civilization evolution")
|
check(world.state.batches.has("C0.0"), "New C0.0 starts civilization evolution")
|
||||||
@@ -77,7 +77,7 @@ func run() -> void:
|
|||||||
var degraded: float = 0.0
|
var degraded: float = 0.0
|
||||||
var valid_outcomes: bool = true
|
var valid_outcomes: bool = true
|
||||||
for i in 4001:
|
for i in 4001:
|
||||||
var products: Dictionary = world._life_products(world.state, world.entities.HP, 1.0)
|
var products: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1.0)
|
||||||
var total: float = 0.0
|
var total: float = 0.0
|
||||||
for value in products.values():
|
for value in products.values():
|
||||||
total += value
|
total += value
|
||||||
@@ -92,29 +92,29 @@ func run() -> void:
|
|||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.time = {"datetime": 123.4, "year": 123.4}
|
world.state.time = {"datetime": 123.4, "year": 123.4}
|
||||||
world.state.binds.RD = {"HP": 2.0}
|
world.state.binds.RD = {"HP": 2.0}
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.batches.HP[0].created_from == 123.4, "Old bound worlds begin life at load time")
|
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.stellar.destroy_planets(world.state, "RD", 1.0, 1)
|
||||||
world._evolve_batches(world.state, 1e7)
|
world.evolution.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")
|
check(world.ledger.physical_amounts(world.state).HP == 0.0 and world.state.binds.RD.RP == 2.0, "Destroyed life does not resurrect from cohort")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.time = {"datetime": 123.4, "year": 123.4}
|
world.state.time = {"datetime": 123.4, "year": 123.4}
|
||||||
world.state.amount["C0.0"] = 40000.0
|
world.state.amount["C0.0"] = 40000.0
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.batches["C0.0"][0].created_from == 123.4, "Civilization cohorts begin at the current time")
|
check(world.state.batches["C0.0"][0].created_from == 123.4, "Civilization cohorts begin at the current time")
|
||||||
before = world.state.duplicate(true)
|
before = world.state.duplicate(true)
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state == before, "Life batch initialization is idempotent")
|
check(world.state == before, "Life batch initialization is idempotent")
|
||||||
world._evolve_batches(world.state, 2173.4)
|
world.evolution.evolve_batches(world.state, 2173.4)
|
||||||
check(world.state.amount["C0.1"] == 700.0 and world.state.amount.EON5 == 1000.0, "C0.0 progresses and degrades by Unity rates")
|
check(world.state.amount["C0.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")
|
check(world.state.amount["C0.0"] == 38300.0, "C0.0 remainder stays alive")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 1.0
|
world.state.amount.RD = 1.0
|
||||||
world.state.binds.RD = {"C0.1": 40000.0}
|
world.state.binds.RD = {"C0.1": 40000.0}
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
world._evolve_batches(world.state, 2050.0)
|
world.evolution.evolve_batches(world.state, 2050.0)
|
||||||
check(world.state.binds.RD["C0.2"] == 700.0, "C0.1 reaches ethical civilization without losing host")
|
check(world.state.binds.RD["C0.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(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")
|
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Extended life save validates")
|
||||||
|
|||||||
+11
-11
@@ -28,10 +28,10 @@ func run() -> void:
|
|||||||
world.state.stats.total_amount["C0.0"] = 3.0
|
world.state.stats.total_amount["C0.0"] = 3.0
|
||||||
var before: Dictionary = world.state.duplicate(true)
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
stage._refresh_universe_ui()
|
stage._refresh_universe_ui()
|
||||||
var counts: Dictionary = world.get_item_amounts()
|
var counts: Dictionary = world.presentation.amounts(world.state, world.catalog)
|
||||||
check(counts.EON1 == 12.0 and counts.EON2 == 4.0 and counts.LP == 16.0, "Individual eons and their total include bound planets")
|
check(counts.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(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.presentation.amounts(world.state, world.catalog, true).EON1 == 12.0 and world.presentation.amounts(world.state, world.catalog, true).LP == 16.0, "Historical counts retain individual and aggregate values")
|
||||||
check(world.state == before, "Presentation does not modify simulation state")
|
check(world.state == before, "Presentation does not modify simulation state")
|
||||||
var pending: Array = ["EON1", "EON2", "EON3", "C0.0"]
|
var pending: Array = ["EON1", "EON2", "EON3", "C0.0"]
|
||||||
var pending_before: Array = pending.duplicate()
|
var pending_before: Array = pending.duplicate()
|
||||||
@@ -40,16 +40,16 @@ func run() -> void:
|
|||||||
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
|
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
|
||||||
check(decision.visible.has(id), "Unlock projection includes detail and alias: " + id)
|
check(decision.visible.has(id), "Unlock projection includes detail and alias: " + id)
|
||||||
check(pending == pending_before and world.state == before, "Unlock evaluation leaves inputs untouched")
|
check(pending == pending_before and world.state == before, "Unlock evaluation leaves inputs untouched")
|
||||||
var snapshot: Dictionary = world.items_to_update.duplicate()
|
var snapshot: Dictionary = world.presentation.pending_amounts.duplicate()
|
||||||
world.defer_item_updates = true
|
world.presentation.deferred_updates = true
|
||||||
world.state.amount.EON1 += 1.0
|
world.state.amount.EON1 += 1.0
|
||||||
world.refresh_item_amounts()
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
check(world.item_amounts_dirty and world.items_to_update == snapshot, "Deferred refresh retains pending snapshot")
|
check(world.presentation.dirty and world.presentation.pending_amounts == snapshot, "Deferred refresh retains pending snapshot")
|
||||||
world.refresh_item_amounts(true)
|
world.presentation.refresh(world.state, world.catalog, true)
|
||||||
check(not world.item_amounts_dirty and world.items_to_update.EON1 == 13.0, "Forced refresh publishes latest amounts")
|
check(not world.presentation.dirty and world.presentation.pending_amounts.EON1 == 13.0, "Forced refresh publishes latest amounts")
|
||||||
world.state.amount.EON1 -= 1.0
|
world.state.amount.EON1 -= 1.0
|
||||||
world.defer_item_updates = false
|
world.presentation.deferred_updates = false
|
||||||
world.refresh_item_amounts()
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
|
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
|
||||||
check(stage.owniverse_items[id].visible, "Unlock both levels: " + id)
|
check(stage.owniverse_items[id].visible, "Unlock both levels: " + id)
|
||||||
check(stage.owniverse_items[id].description.text != "", "Populate the icon counter: " + id)
|
check(stage.owniverse_items[id].description.text != "", "Populate the icon counter: " + id)
|
||||||
@@ -61,7 +61,7 @@ func run() -> void:
|
|||||||
world.state.binds.RD.EON2 = 0.0
|
world.state.binds.RD.EON2 = 0.0
|
||||||
stage._refresh_universe_ui()
|
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")
|
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()
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
scene.free()
|
scene.free()
|
||||||
print("Life icons: %d checks, %d failures" % [checks, failures])
|
print("Life icons: %d checks, %d failures" % [checks, failures])
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(condition: bool, message: String) -> void:
|
||||||
|
checks += 1
|
||||||
|
if not condition:
|
||||||
|
failures += 1
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var World = load("res://world/owniverse/owniverse.gd")
|
||||||
|
var first = World.new()
|
||||||
|
var second = World.new()
|
||||||
|
var names: Array = ["batches", "ledger", "production", "automatic", "stellar", "life", "environment", "evolution"]
|
||||||
|
var references: Array = []
|
||||||
|
for name in names:
|
||||||
|
check(first.get(name) != second.get(name), "World has its own " + name)
|
||||||
|
check(first.get(name).world == first, "Module refers to its owner: " + name)
|
||||||
|
references.append(weakref(first.get(name)))
|
||||||
|
first.state.amount.H = 123.0
|
||||||
|
first.presentation.pending_amounts.H = 123.0
|
||||||
|
check(not second.state.amount.has("H") and second.presentation.pending_amounts.is_empty(), "State and pending presentation are isolated")
|
||||||
|
first.free()
|
||||||
|
for reference in references:
|
||||||
|
check(reference.get_ref() == null, "Module is released with its world")
|
||||||
|
check(second.evolution.world == second, "Other world remains usable")
|
||||||
|
var snapshot: Dictionary = second.state.duplicate(true)
|
||||||
|
second.evolute(NAN)
|
||||||
|
second.evolute(-1.0)
|
||||||
|
check(second.state == snapshot, "Entry point rejects invalid time deltas")
|
||||||
|
second.free()
|
||||||
|
print("Module lifecycle: %d checks, %d failures" % [checks, failures])
|
||||||
|
quit(1 if failures else 0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d2xk4ldldsgo3
|
||||||
@@ -17,39 +17,39 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0.0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0.0
|
||||||
if entity.group_as != "":
|
if entity.group_as != "":
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0.0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0.0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1.0
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
world.entities.N.cost = {"H": 2.0}
|
world.catalog.entities.N.cost = {"H": 2.0}
|
||||||
world.entities.N.bind = {"S": 3.0}
|
world.catalog.entities.N.bind = {"S": 3.0}
|
||||||
world.state.amount.H = 10.0
|
world.state.amount.H = 10.0
|
||||||
world.state.amount.S = 0.0
|
world.state.amount.S = 0.0
|
||||||
world.state.auto_resources = {"H": 10.0}
|
world.state.auto_resources = {"H": 10.0}
|
||||||
world.state.batches.H = [{"amount": 10.0, "start_year": 1.0, "end_year": 2.0}]
|
world.state.batches.H = [{"amount": 10.0, "start_year": 1.0, "end_year": 2.0}]
|
||||||
var before: Dictionary = world.state.duplicate(true)
|
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(not world.production.try_production(world.state, world.catalog.entities.N, 1.0, true), "Binding failure rejects production")
|
||||||
check(world.state == before, "Failure preserves costs, batches and budget")
|
check(world.state == before, "Failure preserves costs, batches and budget")
|
||||||
world.state.amount.S = 10.0
|
world.state.amount.S = 10.0
|
||||||
var old_batches: Array = world.state.batches.H
|
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.production.try_production(world.state, world.catalog.entities.N, 2.0, true), "Funded production succeeds")
|
||||||
check(world.state.amount.H == 6.0 and world.state.amount.S == 4.0, "Exact cost and binding")
|
check(world.state.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(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")
|
check(old_batches[0].amount == 10.0 and world.state.batches.H[0].amount == 6.0, "Consumed batches isolated")
|
||||||
|
|||||||
@@ -14,16 +14,16 @@ func check(condition: bool, message: String) -> void:
|
|||||||
func fixture(legacy: bool):
|
func fixture(legacy: bool):
|
||||||
var world = load("res://tests/fixtures/legacy_stellar_events.gd" if legacy else "res://world/owniverse/owniverse.gd").new()
|
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:
|
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.catalog.entities[id] = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0.0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0.0
|
||||||
if world.entities[id].group_as != "":
|
if world.catalog.entities[id].group_as != "":
|
||||||
world.state.amount[world.entities[id].group_as] = 0.0
|
world.state.amount[world.catalog.entities[id].group_as] = 0.0
|
||||||
world.state.stats.total_amount[world.entities[id].group_as] = 0.0
|
world.state.stats.total_amount[world.catalog.entities[id].group_as] = 0.0
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values(): entity.free()
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func equivalent(a: Variant, b: Variant) -> bool:
|
func equivalent(a: Variant, b: Variant) -> bool:
|
||||||
@@ -55,11 +55,11 @@ func run() -> void:
|
|||||||
fast.state.batches[parent] = [{"amount": 20000.0, "start_year": 100.3, "end_year": 3000.7}]
|
fast.state.batches[parent] = [{"amount": 20000.0, "start_year": 100.3, "end_year": 3000.7}]
|
||||||
# Existing short-lived populations test integration with new arrivals.
|
# Existing short-lived populations test integration with new arrivals.
|
||||||
fast.state.amount.SN = 3.0
|
fast.state.amount.SN = 3.0
|
||||||
fast._append_batch(fast.state, "SN", 3.0, 100.5, 100.5)
|
fast.batches.append_batch(fast.state, "SN", 3.0, 100.5, 100.5)
|
||||||
if year == 104.75: fast.state.energy = 7.0
|
if year == 104.75: fast.state.energy = 7.0
|
||||||
legacy.state = fast.state.duplicate(true)
|
legacy.state = fast.state.duplicate(true)
|
||||||
fast._evolve_batches(fast.state, year)
|
fast.evolution.evolve_batches(fast.state, year)
|
||||||
legacy._evolve_batches(legacy.state, year)
|
legacy.evolution.evolve_batches(legacy.state, year)
|
||||||
for field in fast.state:
|
for field in fast.state:
|
||||||
check(equivalent(fast.state[field], legacy.state[field]), "%s at %s: %s matches materialized events" % [parent, year, field])
|
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"]:
|
for id in ["SN", "DSN", "BSN", "HN", "GRB", "SGRB", "LGRB", "HGRB"]:
|
||||||
@@ -68,7 +68,7 @@ func run() -> void:
|
|||||||
dispose(legacy)
|
dispose(legacy)
|
||||||
var fast = fixture(false)
|
var fast = fixture(false)
|
||||||
var pending: Dictionary = {}
|
var pending: Dictionary = {}
|
||||||
fast._append_batch(fast.state, "SN", 10000.0, 0.25, 1667.25, pending, 1667.25)
|
fast.batches.append_batch(fast.state, "SN", 10000.0, 0.25, 1667.25, pending, 1667.25)
|
||||||
check(fast.state.batches.SN.size() <= 2 and pending.SN.size() == 1, "Completed windows aggregate without transient batch allocation")
|
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
|
var total: float = pending.SN[0].amount
|
||||||
for batch in fast.state.batches.SN: total += batch.amount
|
for batch in fast.state.batches.SN: total += batch.amount
|
||||||
|
|||||||
+51
-51
@@ -16,72 +16,72 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0.0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0.0
|
||||||
if entity.group_as != "":
|
if entity.group_as != "":
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0.0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0.0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1.0
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
for id in ["DSN", "SN", "BSN", "HN", "SGRB", "GRB", "LGRB", "HGRB"]:
|
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)
|
check(world.stellar.evolution_products(world.state, world.catalog.entities[id], 3.0).blast == 3.0 * world.catalog.entities[id].blast, "Entity blast determines power: " + id)
|
||||||
world.entities.SN.output_and.blast = 999.0
|
world.catalog.entities.SN.output_and.blast = 999.0
|
||||||
world.entities.SN.blast = 4
|
world.catalog.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")
|
check(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).blast == 8.0, "Imported output cannot override or double blast power")
|
||||||
world.entities.SN.blast = 0
|
world.catalog.entities.SN.blast = 0
|
||||||
check(not world._evolution_products(world.state, world.entities.SN, 2.0).has("blast"), "Zero entity blast disables external event")
|
check(not world.stellar.evolution_products(world.state, world.catalog.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}
|
world.catalog.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")
|
check(not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).has("blast"), "Alternative output cannot restore disabled blast")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
world = fixture()
|
world = fixture()
|
||||||
var rd = world.entities.RD
|
var rd = world.catalog.entities.RD
|
||||||
check(rd.planets.HP.chance == 0.01, "Preserve fractional planet probability")
|
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.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("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")
|
check(rd._parse_planet_string("").is_empty(), "Empty planet definitions stay empty")
|
||||||
for id in ["HP", "EON1", "C0.9", "C1.2"]:
|
for id in ["HP", "EON1", "C0.9", "C1.2"]:
|
||||||
check(world.entities[id].blastability, "Blast vulnerable: " + id)
|
check(world.catalog.entities[id].blastability, "Blast vulnerable: " + id)
|
||||||
for id in ["RP", "GG", "AB", "C1.3", "C1.4", "C1.5"]:
|
for id in ["RP", "GG", "AB", "C1.3", "C1.4", "C1.5"]:
|
||||||
check(not world.entities[id].blastability, "Blast protected: " + id)
|
check(not world.catalog.entities[id].blastability, "Blast protected: " + id)
|
||||||
|
|
||||||
# Actual GI table: HRG -> SN, with 1% GRB; no random rounding.
|
# Actual GI table: HRG -> SN, with 1% GRB; no random rounding.
|
||||||
var totals: Dictionary = {}
|
var totals: Dictionary = {}
|
||||||
for i in 100:
|
for i in 100:
|
||||||
var products: Dictionary = world._evolution_products(world.state, world.entities.HRG, 1.0)
|
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1.0)
|
||||||
for id in products:
|
for id in products:
|
||||||
totals[id] = totals.get(id, 0.0) + products[id]
|
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")
|
check(totals.get("SN") == 99.0 and totals.get("GRB") == 1.0, "GI rare outcomes accumulate deterministically")
|
||||||
var restored = fixture()
|
var restored = fixture()
|
||||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
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")
|
check(world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 37.0) == restored.stellar.evolution_products(restored.state, restored.catalog.entities.HRG, 37.0), "Evolution carry survives serialization")
|
||||||
dispose(restored)
|
dispose(restored)
|
||||||
|
|
||||||
# Real RD lifespan, four stars spread over the existing lifespan/40 window.
|
# Real RD lifespan, four stars spread over the existing lifespan/40 window.
|
||||||
world.state.amount.N = 0.0
|
world.state.amount.N = 0.0
|
||||||
world.state.amount.RD = 4.0
|
world.state.amount.RD = 4.0
|
||||||
world._append_batch(world.state, "RD", 4.0, 0.0, 0.0)
|
world.batches.append_batch(world.state, "RD", 4.0, 0.0, 0.0)
|
||||||
var lifespan: float = rd.lifespan
|
var lifespan: float = rd.lifespan
|
||||||
var width: float = rd.lifespan_delta
|
var width: float = rd.lifespan_delta
|
||||||
world._evolve_batches(world.state, lifespan)
|
world.evolution.evolve_batches(world.state, lifespan)
|
||||||
check(world.state.amount.RD == 4.0, "Window start does not kill the whole batch")
|
check(world.state.amount.RD == 4.0, "Window start does not kill the whole batch")
|
||||||
world._evolve_batches(world.state, lifespan + width / 4.0)
|
world.evolution.evolve_batches(world.state, lifespan + width / 4.0)
|
||||||
check(world.state.amount.RD == 3.0 and world.state.amount.TWD == 1.0, "One quarter of a batch evolves")
|
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)
|
world.evolution.evolve_batches(world.state, lifespan + width / 2.0)
|
||||||
check(world.state.amount.RD == 2.0 and world.state.amount.TWD == 2.0, "Half the batch evolves")
|
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)
|
world.evolution.evolve_batches(world.state, lifespan + width)
|
||||||
check(world.state.amount.RD == 0.0 and world.state.amount.TWD == 4.0, "Window end completes evolution")
|
check(world.state.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")
|
check(world.state.batches.RD.is_empty(), "Completed batch removed")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
@@ -89,7 +89,7 @@ func run() -> void:
|
|||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 100.0
|
world.state.amount.RD = 100.0
|
||||||
for i in 100:
|
for i in 100:
|
||||||
world._create_planets(world.state, "RD", 1.0, 0.0, 0.0, 1.0)
|
world.stellar.create_planets(world.state, "RD", 1.0, 0.0, 0.0, 1.0)
|
||||||
check(world.state.binds.RD.HP == 1.0, "100 RD at metallicity 1 produce one HP")
|
check(world.state.binds.RD.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.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.binds.RD.GG == 16.0 and world.state.binds.RD.AB == 120.0, "Planet multiplicity applied")
|
||||||
@@ -98,8 +98,8 @@ func run() -> void:
|
|||||||
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Planet carry save validates")
|
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Planet carry save validates")
|
||||||
restored = fixture()
|
restored = fixture()
|
||||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||||
world._create_planets(world.state, "RD", 37.0, 0.0, 0.0, 0.7)
|
world.stellar.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)
|
restored.stellar.create_planets(restored.state, "RD", 37.0, 0.0, 0.0, 0.7)
|
||||||
check(world.state == restored.state, "Planet carry continues after save")
|
check(world.state == restored.state, "Planet carry continues after save")
|
||||||
dispose(restored)
|
dispose(restored)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
@@ -108,14 +108,14 @@ func run() -> void:
|
|||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 4.0
|
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.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.batches.append_batch(world.state, "RD", 4.0, 0.0, 0.0)
|
||||||
world._evolve_batches(world.state, lifespan + width / 2.0)
|
world.evolution.evolve_batches(world.state, lifespan + width / 2.0)
|
||||||
check(world.state.amount.RD == 2.0 and world.state.amount.TWD == 2.0, "Partial star transition with planets")
|
check(world.state.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.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["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.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")
|
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)
|
world.evolution.evolve_batches(world.state, lifespan + width)
|
||||||
check(world.state.binds.RD["C1.3"] == 0.0 and world.state.binds.TWD["C1.3"] == 4.0, "No planets left at extinct parent type")
|
check(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")
|
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Inherited state validates")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
@@ -123,9 +123,9 @@ func run() -> void:
|
|||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.WS = 2.0
|
world.state.amount.WS = 2.0
|
||||||
world.state.binds.WS = {"HP": 4.0, "C1.3": 4.0, "RP": 4.0, "GG": 4.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.batches.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)
|
world.evolution.evolve_batches(world.state, world.catalog.entities.WS.lifespan + world.catalog.entities.WS.lifespan_delta)
|
||||||
var physical: Dictionary = world._physical_amounts(world.state)
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
check(physical.HP == 0.0 and physical["C1.3"] == 0.0 and physical.RP == 0.0 and physical.GG == 0.0, "Level 2 removes all planets, including protected civilization")
|
check(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")
|
check(world.state.amount.LRG == 2.0, "Giant evolves after local destruction")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
@@ -133,10 +133,10 @@ func run() -> void:
|
|||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 10.0
|
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.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})
|
world.stellar.apply_blast(world.state, 5.0, {"blast": 1.0, "density": 1.0})
|
||||||
check(world.state.binds.RD.HP == 5.0 and world.state.binds.RD["C1.2"] == 5.0, "Blast damages vulnerable worlds proportionally")
|
check(world.state.binds.RD.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")
|
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})
|
world.stellar.apply_blast(world.state, 1e6, {"blast": 1.0, "density": 1.0})
|
||||||
check(world.state.binds.RD.HP == 0.0 and world.state.binds.RD.RP == 20.0, "Overwhelming blast is capped at available planets")
|
check(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")
|
check(not world.state.amount.has("blast"), "Blast is not a resource")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
@@ -145,15 +145,15 @@ func run() -> void:
|
|||||||
world.state.amount.RD = 1.0
|
world.state.amount.RD = 1.0
|
||||||
world.state.binds.RD = {"HP": 1.0}
|
world.state.binds.RD = {"HP": 1.0}
|
||||||
for i in 10:
|
for i in 10:
|
||||||
world._apply_blast(world.state, 1.0, {"blast": 1.0, "density": 0.1})
|
world.stellar.apply_blast(world.state, 1.0, {"blast": 1.0, "density": 0.1})
|
||||||
check(world.state.binds.RD.HP == 0.0 and world.state.binds.RD.RP == 1.0, "Small blast effects accumulate across cycles")
|
check(world.state.binds.RD.HP == 0.0 and world.state.binds.RD.RP == 1.0, "Small blast effects accumulate across cycles")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
# Actual supernova output: one SN gives a neutron star, resources, and an event.
|
# Actual supernova output: one SN gives a neutron star, resources, and an event.
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.SN = 1.0
|
world.state.amount.SN = 1.0
|
||||||
world._append_batch(world.state, "SN", 1.0, 0.0, 0.0)
|
world.batches.append_batch(world.state, "SN", 1.0, 0.0, 0.0)
|
||||||
world._evolve_batches(world.state, 2.0)
|
world.evolution.evolve_batches(world.state, 2.0)
|
||||||
check(world.state.amount.SN == 0.0 and world.state.amount.LNS == 1.0, "Supernova produces a remnant")
|
check(world.state.amount.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(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(not world.state.amount.has("blast"), "Supernova dispatches blast output")
|
||||||
@@ -163,7 +163,7 @@ func run() -> void:
|
|||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.H = 1000.0
|
world.state.amount.H = 1000.0
|
||||||
world.state.amount.Met = 100.0
|
world.state.amount.Met = 100.0
|
||||||
check(world.produce("RD", 100.0) == 100.0, "Manual star creation succeeds")
|
check(world.production.produce("RD", 100.0) == 100.0, "Manual star creation succeeds")
|
||||||
check(world.state.binds.RD.HP >= 1.0, "Manual creation dispatches planet formation")
|
check(world.state.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")
|
check(world.state.amount.H == 900.0, "Planet creation does not duplicate or spend stellar fuel")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
@@ -180,25 +180,25 @@ func run() -> void:
|
|||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.entities.N.auto = 1.0
|
world.catalog.entities.N.auto = 1.0
|
||||||
# 1000 newly produced automatic H reserve 1 H, enough for 10 HBD.
|
# 1000 newly produced automatic H reserve 1 H, enough for 10 HBD.
|
||||||
world.evolute(100.0)
|
world.evolute(100.0)
|
||||||
check(world.state.amount.HBD == 10.0, "Brown dwarfs form without existing ordinary stars")
|
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.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")
|
check(world.presentation.amounts(world.state, world.catalog).BD == 10.0 and world.state.batches.HBD.size() > 0, "Brown dwarfs appear in counter and evolution batches")
|
||||||
world.entities.N.product_per_year_amount = 0.0
|
world.catalog.entities.N.product_per_year_amount = 0.0
|
||||||
world.evolute(100.0)
|
world.evolute(100.0)
|
||||||
check(world.state.amount.HBD == 10.0, "Stored automatic budget is not taxed repeatedly")
|
check(world.state.amount.HBD == 10.0, "Stored automatic budget is not taxed repeatedly")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.entities.N.auto = 1.0
|
world.catalog.entities.N.auto = 1.0
|
||||||
world.state.amount.RD = 1.0
|
world.state.amount.RD = 1.0
|
||||||
world.evolute(50.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.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")
|
check(world.state.amount.H == 0.0, "Prepaid fuel is not available to ordinary stars")
|
||||||
var resumed = fixture()
|
var resumed = fixture()
|
||||||
resumed.entities.N.auto = 1.0
|
resumed.catalog.entities.N.auto = 1.0
|
||||||
resumed.state = bytes_to_var(var_to_bytes(world.state))
|
resumed.state = bytes_to_var(var_to_bytes(world.state))
|
||||||
world.evolute(50.0)
|
world.evolute(50.0)
|
||||||
resumed.evolute(50.0)
|
resumed.evolute(50.0)
|
||||||
@@ -206,8 +206,8 @@ func run() -> void:
|
|||||||
check(world.state == resumed.state, "Brown dwarf quota survives serialization deterministically")
|
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")
|
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.state.batches.HBD = [{"amount": 10.0, "start_year": 101.0, "end_year": 101.0}]
|
||||||
world._evolve_batches(world.state, 101.0)
|
world.evolution.evolve_batches(world.state, 101.0)
|
||||||
check(world.state.amount.HBD == 0.0 and world.state.amount.CBD == 10.0 and world.get_item_amounts().BD == 10.0, "Cooling preserves aggregate brown dwarf count")
|
check(world.state.amount.HBD == 0.0 and world.state.amount.CBD == 10.0 and world.presentation.amounts(world.state, world.catalog).BD == 10.0, "Cooling preserves aggregate brown dwarf count")
|
||||||
dispose(resumed)
|
dispose(resumed)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ func run() -> void:
|
|||||||
world.state.amount.H += 1.0
|
world.state.amount.H += 1.0
|
||||||
world.state.auto_resources.H = world.state.auto_resources.get("H", 0.0) + 1.0
|
world.state.auto_resources.H = world.state.auto_resources.get("H", 0.0) + 1.0
|
||||||
var before: float = world.state.amount.HBD
|
var before: float = world.state.amount.HBD
|
||||||
world._produce_brown_dwarfs(world.state, 1.0, float(i), float(i + 1))
|
world.automatic.produce_brown_dwarfs(world.state, 1.0, float(i), float(i + 1))
|
||||||
var born: float = world.state.amount.HBD - before
|
var born: float = world.state.amount.HBD - before
|
||||||
check(born == 0.0 or born == 1.0, "Small fuel increments produce individual dwarfs")
|
check(born == 0.0 or born == 1.0, "Small fuel increments produce individual dwarfs")
|
||||||
saw_individual = saw_individual or born == 1.0
|
saw_individual = saw_individual or born == 1.0
|
||||||
@@ -228,23 +228,23 @@ func run() -> void:
|
|||||||
for simulation in [world, resumed]:
|
for simulation in [world, resumed]:
|
||||||
simulation.state.amount.H += 1.0
|
simulation.state.amount.H += 1.0
|
||||||
simulation.state.auto_resources.H += 1.0
|
simulation.state.auto_resources.H += 1.0
|
||||||
simulation._produce_brown_dwarfs(simulation.state, 1.0, float(i), float(i + 1))
|
simulation.automatic.produce_brown_dwarfs(simulation.state, 1.0, float(i), float(i + 1))
|
||||||
check(world.state == resumed.state, "Random sequence and prepaid fuel survive saving")
|
check(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.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")
|
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
|
var paused_count: float = world.state.amount.HBD
|
||||||
for i in 10: world._produce_brown_dwarfs(world.state, 0.0, 500.0, 501.0)
|
for i in 10: world.automatic.produce_brown_dwarfs(world.state, 0.0, 500.0, 501.0)
|
||||||
check(world.state.amount.HBD == paused_count, "No repeated random attempts without new fuel")
|
check(world.state.amount.HBD == paused_count, "No repeated random attempts without new fuel")
|
||||||
dispose(resumed)
|
dispose(resumed)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
# Two simultaneous successors share surviving planets in the same 99:1 ratio.
|
# Two simultaneous successors share surviving planets in the same 99:1 ratio.
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.entities.HRG.destroy_level = 0
|
world.catalog.entities.HRG.destroy_level = 0
|
||||||
world.state.amount.HRG = 100.0
|
world.state.amount.HRG = 100.0
|
||||||
world.state.binds.HRG = {"C1.3": 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.state.batches.HRG = [{"start_year": 0.1, "end_year": 0.1, "amount": 100.0}]
|
||||||
world._evolve_batches(world.state, 0.1)
|
world.evolution.evolve_batches(world.state, 0.1)
|
||||||
check(world.state.binds.SN["C1.3"] == 99.0 and world.state.binds.GRB["C1.3"] == 1.0, "Planets split among actual alternative successors")
|
check(world.state.binds.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")
|
check(world.state.binds.HRG["C1.3"] == 0.0, "Alternative evolution leaves no orphan bindings")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|||||||
+8
-8
@@ -17,21 +17,21 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0.0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0.0
|
||||||
if entity.group_as != "":
|
if entity.group_as != "":
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0.0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0.0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1.0
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
@@ -80,8 +80,8 @@ func run() -> void:
|
|||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.entities.N.output_and = {"He": 1.0}
|
world.catalog.entities.N.output_and = {"He": 1.0}
|
||||||
world.entities.N.output_or = {}
|
world.catalog.entities.N.output_or = {}
|
||||||
world.state.batches.N = [{"start_year": 0.1, "end_year": 0.1, "amount": 1.0}]
|
world.state.batches.N = [{"start_year": 0.1, "end_year": 0.1, "amount": 1.0}]
|
||||||
world.evolute(0.1)
|
world.evolute(0.1)
|
||||||
check(world.state.amount.N == 0.0 and world.state.amount.He == 1.0, "Existing evolution executes at fractional boundary")
|
check(world.state.amount.N == 0.0 and world.state.amount.He == 1.0, "Existing evolution executes at fractional boundary")
|
||||||
|
|||||||
+20
-15
@@ -24,31 +24,36 @@ func run() -> void:
|
|||||||
await process_frame
|
await process_frame
|
||||||
stage.owniverse.state.amount.H = 1000.0
|
stage.owniverse.state.amount.H = 1000.0
|
||||||
stage.owniverse.state.amount.RD = 2.0
|
stage.owniverse.state.amount.RD = 2.0
|
||||||
stage.owniverse.refresh_item_amounts()
|
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog)
|
||||||
stage._update_ui(0.0)
|
stage.set_owniverse_item_value()
|
||||||
var hydrogen = stage.owniverse_items.H
|
var hydrogen = stage.owniverse_items.H
|
||||||
var dwarf = stage.owniverse_items.RD
|
var dwarf = stage.owniverse_items.RD
|
||||||
check(hydrogen.description.text == "1 000", "Visible page updates immediately on navigation")
|
check(hydrogen.description.text == "1 000", "Pending quantities reach controls")
|
||||||
check(dwarf.description.text == "", "Offscreen counters are not rendered")
|
check(dwarf.description.text == "2", "Other page receives its current quantity")
|
||||||
|
check(stage.owniverse.presentation.pending_amounts.is_empty(), "Published queue is consumed by Stage")
|
||||||
|
stage.owniverse.presentation.deferred_updates = true
|
||||||
stage.owniverse.state.amount.H = 2000.0
|
stage.owniverse.state.amount.H = 2000.0
|
||||||
stage.owniverse.refresh_item_amounts()
|
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog)
|
||||||
check(stage.owniverse.item_amounts_dirty and stage.owniverse.items_to_update.H == 1000.0, "Simulation invalidates without recalculating display sums")
|
check(stage.owniverse.presentation.dirty and stage.owniverse.presentation.pending_amounts.is_empty(), "Deferred refresh marks dirty without republishing")
|
||||||
stage._update_ui(0.05)
|
stage.set_owniverse_item_value()
|
||||||
check(hydrogen.description.text == "1 000", "No refresh before interval")
|
check(hydrogen.description.text == "1 000", "Labels retain last published quantity")
|
||||||
stage._update_ui(0.051)
|
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog, true)
|
||||||
check(hydrogen.description.text == "2 000" and not stage.owniverse.item_amounts_dirty, "Latest state appears at interval")
|
stage.set_owniverse_item_value()
|
||||||
|
check(hydrogen.description.text == "2 000" and not stage.owniverse.presentation.dirty, "Forced refresh publishes latest state")
|
||||||
|
stage.owniverse.presentation.deferred_updates = false
|
||||||
stage.item_activated(hydrogen)
|
stage.item_activated(hydrogen)
|
||||||
check(hydrogen.description.text == root.get_node("Global").format_number(stage.owniverse.state.amount.H), "Player production updates counter immediately")
|
stage.set_owniverse_item_value()
|
||||||
|
check(hydrogen.description.text == root.get_node("Global").format_number(stage.owniverse.state.amount.H), "Player production publishes the new quantity")
|
||||||
check(stage.owniverse.state.amount.H > 2000.0, "Production action actually executed")
|
check(stage.owniverse.state.amount.H > 2000.0, "Production action actually executed")
|
||||||
dwarf.visible = true
|
dwarf.visible = true
|
||||||
stage.stage_page_scroller.scroll_horizontal = 2 * stage.PAGE_WIDTH
|
stage.stage_page_scroller.scroll_horizontal = 2 * stage.PAGE_WIDTH
|
||||||
await process_frame
|
await process_frame
|
||||||
stage._update_ui(0.0)
|
stage._refresh_universe_ui()
|
||||||
check(dwarf.description.text == "2", "Newly visible page receives cached latest values")
|
check(dwarf.description.text == "2", "Newly visible page receives cached latest values")
|
||||||
stage.owniverse.state.time.year = 1234.5
|
stage.owniverse.state.time.year = 1234.5
|
||||||
stage._update_ui(0.1)
|
stage._refresh_universe_ui()
|
||||||
check(stage.date_time.text == "1 234.5", "Time uses throttled display with tenths")
|
check(stage.date_time.text == "1 234.5", "Time displays tenths with thousands separator")
|
||||||
for entity in stage.owniverse.entities.values(): entity.free()
|
for entity in stage.owniverse.catalog.entities.values(): entity.free()
|
||||||
stage.owniverse.free()
|
stage.owniverse.free()
|
||||||
scene.free()
|
scene.free()
|
||||||
print("UI updates: %d checks, %d failures" % [checks, failures])
|
print("UI updates: %d checks, %d failures" % [checks, failures])
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||||
|
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
|
||||||
|
|
||||||
const Parameters = preload("res://world/owniverse/world_parameters.gd")
|
const Parameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
const Descriptions = preload("res://world/owniverse/item_description.gd")
|
const Descriptions = preload("res://world/owniverse/item_description.gd")
|
||||||
@@ -18,7 +20,7 @@ func fixture():
|
|||||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
for id in root.get_node("Global").OwniverseEntities:
|
for id in root.get_node("Global").OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0.0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0.0
|
||||||
if entity.group_as != "":
|
if entity.group_as != "":
|
||||||
@@ -27,49 +29,49 @@ func fixture():
|
|||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values(): entity.free()
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
world.state.amount.RD = 400.0
|
world.state.amount.RD = 400.0
|
||||||
world._append_batch(world.state, "RD", 400.0, 0.0, 0.0)
|
world.batches.append_batch(world.state, "RD", 400.0, 0.0, 0.0)
|
||||||
var end: float = world.state.batches.RD[0].end_year
|
var end: float = world.state.batches.RD[0].end_year
|
||||||
world.modify_world_parameter("Bur", 9.0)
|
world.environment.modify_world_parameter("Bur", 9.0)
|
||||||
check(is_equal_approx(world.state.batches.RD[0].end_year, end / 10.0), "Burnability accelerates existing cohorts")
|
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
|
var count: float = world.state.batches.RD[0].amount
|
||||||
world.modify_world_parameter("Bur", -9.0)
|
world.environment.modify_world_parameter("Bur", -9.0)
|
||||||
check(is_equal_approx(world.state.batches.RD[0].end_year, end) and world.state.batches.RD[0].amount == count, "Changing burnability back preserves population and lifetime")
|
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.environment.modify_world_parameter("Bur", 9.0)
|
||||||
world._append_batch(world.state, "YD", 10.0, 0.0, 0.0)
|
world.batches.append_batch(world.state, "YD", 10.0, 0.0, 0.0)
|
||||||
check(is_equal_approx(world.state.batches.YD[0].start_year, world.entities.YD.lifespan / 10.0), "New stars use the same burnability")
|
check(is_equal_approx(world.state.batches.YD[0].start_year, world.catalog.entities.YD.lifespan / 10.0), "New stars use the same burnability")
|
||||||
world.state.time = {"datetime": end / 10.0 - 0.1, "year": end / 10.0 - 0.1}
|
world.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.batches.withdraw_batches(world.state, "RD", world.state.time.year)
|
||||||
world.modify_world_parameter("Bur", -18.0)
|
world.environment.modify_world_parameter("Bur", -18.0)
|
||||||
check(world.state.batches.RD[0].end_year >= world.state.time.year, "Slowing a partially evolved cohort keeps its remaining window in the future")
|
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)
|
world.environment.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")
|
check(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).blast == 180.0, "Explosion power includes world explosiveness")
|
||||||
var products: Dictionary = world._evolution_products(world.state, world.entities.HRG, 1000.0)
|
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1000.0)
|
||||||
check(products.get("GRB", 0.0) == 100.0 and products.get("SN", 0.0) == 900.0, "Explosiveness raises rare-event probability without duplicating stars")
|
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
|
world.catalog.entities.HRG.output_or.chance = 50.0
|
||||||
products = world._evolution_products(world.state, world.entities.HRG, 100.0)
|
products = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 100.0)
|
||||||
check(products.get("GRB", 0.0) == 100.0 and products.get("SN", 0.0) == 0.0, "Rare-event probability is bounded at one")
|
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)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
var ordinary: Dictionary = world._life_products(world.state, world.entities.HP, 1000000.0)
|
var ordinary: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
|
||||||
world.state.life_remainders.clear()
|
world.state.life_remainders.clear()
|
||||||
world.modify_world_parameter("Agg", 9.0)
|
world.environment.modify_world_parameter("Agg", 9.0)
|
||||||
var aggressive: Dictionary = world._life_products(world.state, world.entities.HP, 1000000.0)
|
var aggressive: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
|
||||||
check(aggressive.EON1 > ordinary.EON1, "Aggression accelerates primitive evolution")
|
check(aggressive.EON1 > ordinary.EON1, "Aggression accelerates primitive evolution")
|
||||||
world.state.life_remainders.clear()
|
world.state.life_remainders.clear()
|
||||||
world.state.world_parameters.Order = 10.0
|
world.state.world_parameters.Order = 10.0
|
||||||
var orderly: Dictionary = world._life_products(world.state, world.entities.HP, 1000000.0)
|
var orderly: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
|
||||||
check(orderly.RP < aggressive.RP, "Order reduces degradation")
|
check(orderly.RP < aggressive.RP, "Order reduces degradation")
|
||||||
world.state.life_remainders.clear()
|
world.state.life_remainders.clear()
|
||||||
world.state.world_parameters.Enlightenment = 10.0
|
world.state.world_parameters.Enlightenment = 10.0
|
||||||
var enlightened: Dictionary = world._life_products(world.state, world.entities.HP, 1000000.0)
|
var enlightened: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
|
||||||
check(enlightened.EON1 > orderly.EON1, "Enlightenment increases primitive progress")
|
check(enlightened.EON1 > orderly.EON1, "Enlightenment increases primitive progress")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
@@ -77,8 +79,8 @@ func run() -> void:
|
|||||||
var hotter = fixture()
|
var hotter = fixture()
|
||||||
for simulation in [world, hotter]:
|
for simulation in [world, hotter]:
|
||||||
simulation.state.amount.RD = 1000000.0
|
simulation.state.amount.RD = 1000000.0
|
||||||
simulation._append_batch(simulation.state, "RD", 1000000.0, 0.0, 0.0)
|
simulation.batches.append_batch(simulation.state, "RD", 1000000.0, 0.0, 0.0)
|
||||||
hotter.modify_world_parameter("Bur", 9.0)
|
hotter.environment.modify_world_parameter("Bur", 9.0)
|
||||||
world.evolute(1000.0)
|
world.evolute(1000.0)
|
||||||
hotter.evolute(1000.0)
|
hotter.evolute(1000.0)
|
||||||
check(hotter.state.amount.S > world.state.amount.S, "Burnability changes actual space production")
|
check(hotter.state.amount.S > world.state.amount.S, "Burnability changes actual space production")
|
||||||
@@ -96,8 +98,8 @@ func run() -> void:
|
|||||||
var outcomes: Array = []
|
var outcomes: Array = []
|
||||||
for target in [baseline, advanced]:
|
for target in [baseline, advanced]:
|
||||||
var expired: Dictionary = {"C0.2": [{"amount": 1000000.0, "from": 0.0, "to": 1.0}]}
|
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)
|
var budget: Dictionary = CivilizationState.stellar_budget(world, target, global.CivilizationRules)
|
||||||
world.CivilizationEvolution.prepare(world, target, expired, 1.0, budget, global.CivilizationRules)
|
CivilizationEvolution.prepare(world, target, expired, 1.0, budget, global.CivilizationRules)
|
||||||
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0.0))
|
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0.0))
|
||||||
check(outcomes[1] > outcomes[0], "Aggression changes actual ethical civilization transitions")
|
check(outcomes[1] > outcomes[0], "Aggression changes actual ethical civilization transitions")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func update(world) -> void:
|
|||||||
var source: Dictionary = world.state
|
var source: Dictionary = world.state
|
||||||
var energy: float = clampf(source.energy / Parameters.INITIAL_ENERGY, 0.0, 1.0)
|
var energy: float = clampf(source.energy / Parameters.INITIAL_ENERGY, 0.0, 1.0)
|
||||||
for bar in energy_bars: bar.scale.x = energy
|
for bar in energy_bars: bar.scale.x = energy
|
||||||
var density_value: float = world.stellar_environment(source).density
|
var density_value: float = world.environment.stellar_environment(source).density
|
||||||
var density: Dictionary = Parameters.density_display(density_value)
|
var density: Dictionary = Parameters.density_display(density_value)
|
||||||
warning_border.self_modulate = Parameters.density_warning_color(density_value)
|
warning_border.self_modulate = Parameters.density_warning_color(density_value)
|
||||||
for indicator in density_indicators:
|
for indicator in density_indicators:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
Метод `members(id)` возвращает копию списка. После изменения group_as нужно вызвать `rebuild_group_index()`; добавление или удаление определения автоматически обнаруживается по изменению размера словаря. Полная перезагрузка определений выполняется созданием нового каталога.
|
Метод `members(id)` возвращает копию списка. После изменения group_as нужно вызвать `rebuild_group_index()`; добавление или удаление определения автоматически обнаруживается по изменению размера словаря. Полная перезагрузка определений выполняется созданием нового каталога.
|
||||||
|
|
||||||
Owniverse сохраняет совместимые свойства entities/entity_groups/entity_event_groups и методы-обёртки. Они используют те же данные каталога, без дублирования. Stage загружает каталог, затем отдельно инициализирует количество, статистику и UI. Эволюция цивилизаций обращается непосредственно к QuantityMath.
|
Stage загружает каталог, затем отдельно инициализирует количество, статистику и UI. Доступ к определениям идёт через `world.catalog.entities`, к группам — через `world.catalog.members(id)`. Совместимые свойства и методы-переадресации из Owniverse удалены. Эволюция цивилизаций обращается непосредственно к QuantityMath.
|
||||||
|
|
||||||
Формулы, порядок обхода определений, формат сохранения и правила симуляции не изменены. Проверки модулей: `tests/test_quantity_catalog.gd`; интеграционные проверки перечислены в `tests/README.md`.
|
Формулы, порядок обхода определений, формат сохранения и правила симуляции не изменены. Проверки модулей: `tests/test_quantity_catalog.gd`; интеграционные проверки перечислены в `tests/README.md`.
|
||||||
|
|
||||||
@@ -23,4 +23,49 @@ Owniverse сохраняет совместимые свойства entities/en
|
|||||||
- `unlocks(state, catalog, locked)` проверяет условия по историческим количествам и их отображаемым суммам. Возвращает remaining (ещё закрытые сущности) и visible (открываемые сущности и их count_as). Переданный список не изменяется. Уже открытые иконки повторно не блокируются.
|
- `unlocks(state, catalog, locked)` проверяет условия по историческим количествам и их отображаемым суммам. Возвращает remaining (ещё закрытые сущности) и visible (открываемые сущности и их count_as). Переданный список не изменяется. Уже открытые иконки повторно не блокируются.
|
||||||
- `refresh` управляет снимком pending_amounts и флагом dirty, учитывает отложенное обновление и принудительную публикацию. Сам модуль не запускает таймеры и не меняет частоту обновления.
|
- `refresh` управляет снимком pending_amounts и флагом dirty, учитывает отложенное обновление и принудительную публикацию. Сам модуль не запускает таймеры и не меняет частоту обновления.
|
||||||
|
|
||||||
Owniverse делегирует существующие get_item_amounts/refresh_item_amounts и свойства обновления экземпляру presentation. Stage применяет результат разблокировки к контролам и хранит список закрытых иконок. Снимки и флаги принадлежат отдельному миру и не включаются в сохранение.
|
Stage обращается непосредственно к `world.presentation`, применяет результат разблокировки к контролам и хранит список закрытых иконок. Снимки и флаги принадлежат отдельному миру и не включаются в сохранение.
|
||||||
|
|
||||||
|
## Партии, учёт, производство и эволюция
|
||||||
|
|
||||||
|
| Модуль / свойство мира | Ответственность |
|
||||||
|
| --- | --- |
|
||||||
|
| `object_batches.gd` / `batches` | Окна рождения и эволюции, частичное выбытие, остатки округления, масштабирование партий, интеграл времени жизни, прямое завершение короткоживущих партий |
|
||||||
|
| `object_ledger.gd` / `ledger` | Свободные и связанные количества, списание и зачисление, статистика выпуска, целочисленность, резервирование и передача привязок |
|
||||||
|
| `manual_production.gd` / `production` | Предпросмотр доступного количества, транзакция оплаты и привязок, ручное производство, объединение чёрных дыр в SMBH |
|
||||||
|
| `automatic_production.gd` / `automatic` | Выпуск за период и жизнь новорождённых объектов, разделение ресурсов на обычные и автоматические, энергия, квоты звёзд и бурых карликов |
|
||||||
|
| `stellar_rules.gd` / `stellar` | Продукты звёздной эволюции, создание планет, выбор звёздных преемников, два уровня разрушения и глобальные бласты |
|
||||||
|
| `life_evolution.gd` / `life` | Прогресс, деградация и повторная попытка планетарной жизни |
|
||||||
|
| `civilization_evolution.gd`, `civilization_state.gd` | Существующие правила цивилизаций: этики, войны, колонизация, лимиты и места обитания |
|
||||||
|
| `universe_environment.gd` / `environment` | Плотность, металличность, доля попаданий бластов, изменение параметров мира с сохранением возраста звёзд |
|
||||||
|
| `evolution_coordinator.gd` / `evolution` | Порядок фаз шага, последовательные поколения и публикация нового состояния |
|
||||||
|
|
||||||
|
В `owniverse.gd` остались создание состояния и модулей, скорость и проверка входного delta перед запуском эволюции. Дублирующее поле new_state и закомментированная старая реализация удалены. Единственный источник текущего состояния — `world.state`.
|
||||||
|
|
||||||
|
### Порядок шага
|
||||||
|
|
||||||
|
1. Определить обработанную десятую года. При продвижении времени восстановить недостающие партии жизни и проверить вместимость.
|
||||||
|
2. Скопировать состояние, посчитать физические количества и производство за период.
|
||||||
|
3. Обработать поколения до конца интервала. Для каждого поколения: локальные разрушения → выбытие партий всех типов → план переходов → резервирование передачи привязок → зачисление преемников и их партий → производство новорождённых → применение привязок и округление → завершение переходов цивилизаций → глобальные бласты → проверка вместимости.
|
||||||
|
4. Обновить групповые количества, ограничить автоматические ресурсы и продвинуть параметры мира.
|
||||||
|
5. Опубликовать новое состояние и обновить представление количеств.
|
||||||
|
|
||||||
|
Резервирование привязок использует замороженную таблицу, чтобы одновременные переходы не зависели от порядка обработки владельцев. Коммит передач происходит после зачисления преемников. Оплата производства остаётся атомарной: при нехватке ресурсов исходное состояние не меняется.
|
||||||
|
|
||||||
|
### Зависимости и жизненный цикл
|
||||||
|
|
||||||
|
Модули симуляции получают свой Owniverse через `simulation_module.gd`. Они работают с переданным target либо с актуальным `world.state`; ссылка на старое состояние не кэшируется. Обращение к другим подсистемам явное, например `world.ledger.credit(...)` или `world.batches.append_batch(...)`. Внутренние вызовы идут напрямую.
|
||||||
|
|
||||||
|
Ссылка модуля на мир слабая, поэтому цикл владения не возникает. Owniverse владеет модулями, каждый новый мир получает собственные экземпляры. Использовать модуль после освобождения его мира нельзя. В сохранение попадает только состояние; модули, каталог и UI туда не сериализуются. Определения OwniverseEntity по-прежнему освобождает владелец сцены.
|
||||||
|
|
||||||
|
### Примеры нового API
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var available = world.production.calculate_max_producible(world.catalog.entities["YD"])
|
||||||
|
world.production.produce("YD", 10.0)
|
||||||
|
var physical = world.ledger.physical_amounts(world.state)
|
||||||
|
var displayed = world.presentation.amounts(world.state, world.catalog)
|
||||||
|
world.environment.modify_world_parameter("Bur", 1.0)
|
||||||
|
world.evolute(delta)
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверки проверяют прежние формулы через новый API; сравнение ускоренного и обычного путей выполняется через `stellar.direct_completions`. Дополнительный тест `test_module_lifecycle.gd` проверяет изоляцию миров и освобождение модулей.
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
extends "res://world/owniverse/simulation_module.gd"
|
||||||
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
|
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
|
||||||
|
func yearly_products(entity: OwniverseEntity) -> Dictionary:
|
||||||
|
var products: Dictionary = {}
|
||||||
|
if not entity.life_product.is_empty() and entity.lifespan > 0.0:
|
||||||
|
for resource in entity.life_product:
|
||||||
|
products[resource] = maxf(0.0, entity.life_product[resource]) / entity.lifespan
|
||||||
|
elif entity.product_per_year_type != "":
|
||||||
|
products[entity.product_per_year_type] = maxf(0.0, entity.product_per_year_amount)
|
||||||
|
return products
|
||||||
|
|
||||||
|
|
||||||
|
func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Dictionary,
|
||||||
|
distribution: Dictionary, from_year: float, to_year: float) -> void:
|
||||||
|
var basic: Dictionary = {}
|
||||||
|
var automatic: Dictionary = {}
|
||||||
|
for id in quantities:
|
||||||
|
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||||
|
var object_years: float = world.batches.object_years(previous, id, quantities[id], from_year, to_year)
|
||||||
|
var auto_share: float = clampf(entity.auto, 0.0, 1.0)
|
||||||
|
var products: Dictionary = yearly_products(entity)
|
||||||
|
for product in products:
|
||||||
|
var amount: float = object_years * products[product] * WorldParameters.production(previous, entity)
|
||||||
|
basic[product] = basic.get(product, 0.0) + amount * (1.0 - auto_share)
|
||||||
|
automatic[product] = automatic.get(product, 0.0) + amount * auto_share
|
||||||
|
# Basic and automatic output each retain their unproduced remainder.
|
||||||
|
if not target.has("resource_remainders"):
|
||||||
|
target.resource_remainders = {"basic": {}, "automatic": {}}
|
||||||
|
var pending: Dictionary = target.resource_remainders
|
||||||
|
var requests: Dictionary = {}
|
||||||
|
for id in basic:
|
||||||
|
pending.basic[id] = pending.basic.get(id, 0.0) + basic[id]
|
||||||
|
pending.automatic[id] = pending.automatic.get(id, 0.0) + automatic[id]
|
||||||
|
var totals: Dictionary = {}
|
||||||
|
for channel in ["basic", "automatic"]:
|
||||||
|
for id in pending[channel]:
|
||||||
|
totals[id] = maxf(0.0, pending.basic.get(id, 0.0) + pending.automatic.get(id, 0.0))
|
||||||
|
for id in totals:
|
||||||
|
if id == "S" or id == "H":
|
||||||
|
requests[id] = floorf(totals[id])
|
||||||
|
var funded: Dictionary = QuantityMath.allocate_whole(requests, floorf(maxf(0.0, previous.energy)))
|
||||||
|
automatic = {}
|
||||||
|
target.energy = previous.energy
|
||||||
|
for id in totals:
|
||||||
|
var amount: float = floorf(totals[id])
|
||||||
|
if id == "S" or id == "H":
|
||||||
|
amount = funded.get(id, 0.0)
|
||||||
|
target.energy -= amount
|
||||||
|
# Split the actual whole output, retaining the allocation error for next time.
|
||||||
|
var auto_ratio: float = clampf(pending.automatic.get(id, 0.0) / totals[id], 0.0, 1.0) if totals[id] > 0.0 else 0.0
|
||||||
|
var auto_amount: float = floorf(amount * auto_ratio)
|
||||||
|
pending.automatic[id] = pending.automatic.get(id, 0.0) - auto_amount
|
||||||
|
pending.basic[id] = pending.basic.get(id, 0.0) - (amount - auto_amount)
|
||||||
|
automatic[id] = auto_amount
|
||||||
|
amount = world.ledger.credit(target, id, amount)
|
||||||
|
world.batches.append_batch(target, id, amount, from_year, to_year)
|
||||||
|
|
||||||
|
# Keep unused automatic resources across cycles; they are part of free amounts.
|
||||||
|
var budget: Dictionary = previous.get("auto_resources", {}).duplicate(true)
|
||||||
|
for id in automatic:
|
||||||
|
budget[id] = budget.get(id, 0.0) + automatic[id]
|
||||||
|
for id in budget:
|
||||||
|
budget[id] = minf(maxf(0.0, budget[id]), world.ledger.available(target, id))
|
||||||
|
target.auto_resources = budget
|
||||||
|
produce_brown_dwarfs(target, automatic.get("H", 0.0), from_year, to_year)
|
||||||
|
budget = ordinary_star_budget(target)
|
||||||
|
|
||||||
|
# One common multiplier preserves the initial count ratios, even under scarcity.
|
||||||
|
var candidates: Dictionary = {}
|
||||||
|
for id in distribution:
|
||||||
|
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||||
|
if entity.group_as != "Star":
|
||||||
|
continue
|
||||||
|
var has_cost: bool = false
|
||||||
|
for resource in entity.cost:
|
||||||
|
if entity.cost[resource] > 0.0:
|
||||||
|
has_cost = true
|
||||||
|
if has_cost:
|
||||||
|
candidates[id] = distribution[id]
|
||||||
|
candidates = QuantityMath.normalize_distribution(candidates)
|
||||||
|
if candidates.is_empty():
|
||||||
|
return
|
||||||
|
var costs: Dictionary = {}
|
||||||
|
var needs: Dictionary = {}
|
||||||
|
for id in candidates:
|
||||||
|
if auto_capacity(target, world.catalog.entities[id], budget) <= 0.0:
|
||||||
|
continue
|
||||||
|
for resource in world.catalog.entities[id].cost:
|
||||||
|
var cost: float = maxf(0.0, world.catalog.entities[id].cost[resource]) * candidates[id]
|
||||||
|
costs[resource] = costs.get(resource, 0.0) + cost
|
||||||
|
needs[resource] = needs.get(resource, 0.0) + cost
|
||||||
|
for resource in world.catalog.entities[id].bind:
|
||||||
|
needs[resource] = needs.get(resource, 0.0) + maxf(0.0, world.catalog.entities[id].bind[resource]) * candidates[id]
|
||||||
|
var scale: float = INF
|
||||||
|
for resource in costs:
|
||||||
|
if costs[resource] > 0.0:
|
||||||
|
scale = minf(scale, budget.get(resource, 0.0) / costs[resource])
|
||||||
|
for resource in needs:
|
||||||
|
if needs[resource] > 0.0:
|
||||||
|
scale = minf(scale, world.ledger.available(target, resource) / needs[resource])
|
||||||
|
var concrete_needs: Dictionary = world.ledger.expand_requirements(target, needs)
|
||||||
|
for resource in concrete_needs:
|
||||||
|
if concrete_needs[resource] > 0.0:
|
||||||
|
scale = minf(scale, world.ledger.available(target, resource) / concrete_needs[resource])
|
||||||
|
if not is_finite(scale) or scale <= 0.0:
|
||||||
|
return
|
||||||
|
# Rare types are served first; signed count deficits compensate later cycles.
|
||||||
|
if not target.has("auto_star_deficits"):
|
||||||
|
target.auto_star_deficits = {}
|
||||||
|
var deficits: Dictionary = target.auto_star_deficits
|
||||||
|
var order: Array = candidates.keys()
|
||||||
|
order.sort_custom(func(a: String, b: String) -> bool:
|
||||||
|
return candidates[a] < candidates[b] if candidates[a] != candidates[b] else a < b)
|
||||||
|
var created: Dictionary = {}
|
||||||
|
var metallicity: float = world.environment.stellar_environment(target).metallicity
|
||||||
|
var total_created: float = 0.0
|
||||||
|
for id in order:
|
||||||
|
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||||
|
var step: float = world.production.production_step(entity)
|
||||||
|
if not is_finite(step):
|
||||||
|
continue
|
||||||
|
var quota: float = maxf(0.0, candidates[id] * scale + deficits.get(id, 0.0))
|
||||||
|
var desired: float = floorf(quota / step) * step
|
||||||
|
var actual: float = minf(desired, auto_capacity(target, entity, budget))
|
||||||
|
if actual <= 0.0:
|
||||||
|
continue
|
||||||
|
if not world.production.try_production(target, entity, actual, true):
|
||||||
|
continue
|
||||||
|
budget = ordinary_star_budget(target)
|
||||||
|
created[id] = actual
|
||||||
|
total_created += actual
|
||||||
|
for id in candidates:
|
||||||
|
# Credit only actual production, so retained resources aren't counted twice.
|
||||||
|
deficits[id] = deficits.get(id, 0.0) + candidates[id] * total_created - created.get(id, 0.0)
|
||||||
|
for id in created:
|
||||||
|
world.ledger.credit(target, id, created[id])
|
||||||
|
world.batches.append_batch(target, id, created[id], from_year, to_year)
|
||||||
|
world.stellar.create_planets(target, id, created[id], from_year, to_year, metallicity)
|
||||||
|
|
||||||
|
|
||||||
|
func produce_brown_dwarfs(target: Dictionary, new_hydrogen: float, from_year: float, to_year: float) -> void:
|
||||||
|
if not world.catalog.entities.has("HBD"):
|
||||||
|
return
|
||||||
|
var entity: OwniverseEntity = world.catalog.entities.HBD
|
||||||
|
var cost: float = entity.cost.get("H", 0.0)
|
||||||
|
if cost <= 0.0:
|
||||||
|
return
|
||||||
|
var quota: float = target.auto_star_deficits.get("HBD", 0.0) + maxf(0.0, new_hydrogen) * 0.001 / cost
|
||||||
|
var fuel: float = maxf(0.0, target.auto_star_deficits.get("HBD_fuel", 0.0))
|
||||||
|
var available: float = minf(target.auto_resources.get("H", 0.0), world.ledger.available(target, "H"))
|
||||||
|
quota = minf(quota, (available + fuel) / cost)
|
||||||
|
var expected: float = maxf(0.0, snappedf(quota, 1e-9))
|
||||||
|
var amount: float = floorf(expected)
|
||||||
|
# Do not retry a fractional chance on frames with no new automatic fuel.
|
||||||
|
if new_hydrogen > 0.0 and expected > amount:
|
||||||
|
var random_state: float = target.auto_star_deficits.get("HBD_random", 1234567.0)
|
||||||
|
random_state = fmod(random_state * 48271.0, 2147483647.0)
|
||||||
|
target.auto_star_deficits.HBD_random = random_state
|
||||||
|
if random_state / 2147483647.0 < expected - amount:
|
||||||
|
amount += 1.0
|
||||||
|
amount = minf(amount, floorf(snappedf((available + fuel) / cost, 1e-9)))
|
||||||
|
var charge: float = ceilf(maxf(0.0, snappedf(amount * cost - fuel, 1e-9)))
|
||||||
|
if amount > 0.0 and charge <= available:
|
||||||
|
world.ledger.take_available(target, "H", charge)
|
||||||
|
target.auto_resources.H = maxf(0.0, target.auto_resources.get("H", 0.0) - charge)
|
||||||
|
target.auto_star_deficits.HBD_fuel = maxf(0.0, snappedf(fuel + charge - amount * cost, 1e-9))
|
||||||
|
quota -= amount
|
||||||
|
world.ledger.credit(target, "HBD", amount)
|
||||||
|
world.batches.append_batch(target, "HBD", amount, from_year, to_year)
|
||||||
|
target.auto_star_deficits.HBD = quota
|
||||||
|
|
||||||
|
|
||||||
|
func ordinary_star_budget(target: Dictionary) -> Dictionary:
|
||||||
|
var budget: Dictionary = target.auto_resources.duplicate()
|
||||||
|
if world.catalog.entities.has("HBD"):
|
||||||
|
var reserved: float = maxf(0.0, target.auto_star_deficits.get("HBD", 0.0) * world.catalog.entities.HBD.cost.get("H", 0.0) - target.auto_star_deficits.get("HBD_fuel", 0.0))
|
||||||
|
budget.H = maxf(0.0, budget.get("H", 0.0) - reserved)
|
||||||
|
return budget
|
||||||
|
|
||||||
|
|
||||||
|
func auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Dictionary) -> float:
|
||||||
|
var capacity: float = INF
|
||||||
|
var needs: Dictionary = {}
|
||||||
|
for resource in entity.cost:
|
||||||
|
var cost: float = maxf(0.0, entity.cost[resource])
|
||||||
|
needs[resource] = cost
|
||||||
|
if cost > 0.0:
|
||||||
|
capacity = minf(capacity, floorf(budget.get(resource, 0.0) / cost))
|
||||||
|
for resource in entity.bind:
|
||||||
|
needs[resource] = needs.get(resource, 0.0) + maxf(0.0, entity.bind[resource])
|
||||||
|
for resource in needs:
|
||||||
|
if needs[resource] > 0.0:
|
||||||
|
capacity = minf(capacity, floorf(world.ledger.available(source, resource) / needs[resource]))
|
||||||
|
var concrete: Dictionary = world.ledger.expand_requirements(source, needs)
|
||||||
|
for resource in concrete:
|
||||||
|
if concrete[resource] > 0.0:
|
||||||
|
capacity = minf(capacity, floorf(world.ledger.available(source, resource) / concrete[resource]))
|
||||||
|
var step: float = world.production.production_step(entity)
|
||||||
|
return floorf(capacity / step) * step if is_finite(capacity) and is_finite(step) else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func produce_newborn_lifetime(target: Dictionary, id: String, count: float,
|
||||||
|
born_from: float, born_to: float, to_year: float, before_batches: Array,
|
||||||
|
remaining_object_years: float = NAN) -> void:
|
||||||
|
var products: Dictionary = yearly_products(world.catalog.entities[id])
|
||||||
|
if products.is_empty() or count <= 0.0:
|
||||||
|
return
|
||||||
|
var object_years: float = remaining_object_years if is_finite(remaining_object_years) else count * maxf(0.0, to_year - (born_from + born_to) * 0.5)
|
||||||
|
var previous_amounts: Dictionary = {}
|
||||||
|
for old: Dictionary in before_batches:
|
||||||
|
var start: float = old.get("created_from", -INF)
|
||||||
|
if not previous_amounts.has(start):
|
||||||
|
previous_amounts[start] = old.amount
|
||||||
|
for batch: Dictionary in target.batches.get(id, []):
|
||||||
|
var added: float = batch.amount - previous_amounts.get(batch.get("created_from", INF), 0.0)
|
||||||
|
object_years -= maxf(0.0, added) * world.batches.expired_years(to_year, batch.start_year, batch.end_year)
|
||||||
|
object_years = maxf(0.0, object_years)
|
||||||
|
for resource in products:
|
||||||
|
var output: float = object_years * products[resource] * WorldParameters.production(target, world.catalog.entities[id])
|
||||||
|
if resource in ["H", "S"]:
|
||||||
|
output = minf(output, target.energy)
|
||||||
|
var actual: float = world.ledger.credit(target, resource, output)
|
||||||
|
if resource in ["H", "S"]:
|
||||||
|
target.energy = maxf(0.0, target.energy - actual)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cty3i0pb8ihwf
|
||||||
@@ -72,7 +72,7 @@ static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop
|
|||||||
var removed: Dictionary = take(world, pop, count)
|
var removed: Dictionary = take(world, pop, count)
|
||||||
var actual: float = total(removed)
|
var actual: float = total(removed)
|
||||||
if actual > 0.0:
|
if actual > 0.0:
|
||||||
world._scale_batches(target, plan.id, (old_total - actual) / old_total)
|
world.batches.scale_batches(target, plan.id, (old_total - actual) / old_total)
|
||||||
plan.amount += actual
|
plan.amount += actual
|
||||||
plan.to = maxf(plan.to, year)
|
plan.to = maxf(plan.to, year)
|
||||||
return removed
|
return removed
|
||||||
@@ -164,7 +164,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
|||||||
if not incoming.has(destination): incoming[destination] = {}
|
if not incoming.has(destination): incoming[destination] = {}
|
||||||
add(incoming[destination], pop)
|
add(incoming[destination], pop)
|
||||||
products[destination] = total(pop)
|
products[destination] = total(pop)
|
||||||
if State.is_space_civilization(id, rules) and world.entities[destination].group_as == "Planet":
|
if State.is_space_civilization(id, rules) and world.catalog.entities[destination].group_as == "Planet":
|
||||||
planetary[destination] = planetary.get(destination, 0.0) + total(pop)
|
planetary[destination] = planetary.get(destination, 0.0) + total(pop)
|
||||||
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": maxf(plan.from, plan.to), "products": products}]
|
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": maxf(plan.from, plan.to), "products": products}]
|
||||||
return {"remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies}
|
return {"remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies}
|
||||||
@@ -175,10 +175,10 @@ static func finish(world, target: Dictionary, transaction: Dictionary, year: flo
|
|||||||
if not target.ethics.has(id): target.ethics[id] = {}
|
if not target.ethics.has(id): target.ethics[id] = {}
|
||||||
add(target.ethics[id], transaction.incoming[id])
|
add(target.ethics[id], transaction.incoming[id])
|
||||||
# Space civilizations that fall back to planetary stages need a stellar host.
|
# Space civilizations that fall back to planetary stages need a stellar host.
|
||||||
var physical: Dictionary = world._physical_amounts(target)
|
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
var hosts: Dictionary = {}
|
var hosts: Dictionary = {}
|
||||||
for id in physical:
|
for id in physical:
|
||||||
if world.entities[id].event_group == "DwStar" and physical[id] > 0.0: hosts[id] = physical[id]
|
if world.catalog.entities[id].event_group == "DwStar" and physical[id] > 0.0: hosts[id] = physical[id]
|
||||||
for id in transaction.planetary:
|
for id in transaction.planetary:
|
||||||
var shares: Dictionary = split(target, "return:" + id, minf(transaction.planetary[id], target.amount.get(id, 0.0)), hosts)
|
var shares: Dictionary = split(target, "return:" + id, minf(transaction.planetary[id], target.amount.get(id, 0.0)), hosts)
|
||||||
for owner in shares:
|
for owner in shares:
|
||||||
@@ -194,7 +194,7 @@ static func finish(world, target: Dictionary, transaction: Dictionary, year: flo
|
|||||||
target.civilization_stats.worship = worship
|
target.civilization_stats.worship = worship
|
||||||
|
|
||||||
static func colonize(world, target: Dictionary, colonies: Array, year: float, rules: Dictionary) -> void:
|
static func colonize(world, target: Dictionary, colonies: Array, year: float, rules: Dictionary) -> void:
|
||||||
var physical: Dictionary = world._physical_amounts(target)
|
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
var stock: Dictionary = {}
|
var stock: Dictionary = {}
|
||||||
for id in ["GG", "AB", "RP", "HP"]: stock[id] = physical.get(id, 0.0)
|
for id in ["GG", "AB", "RP", "HP"]: stock[id] = physical.get(id, 0.0)
|
||||||
var requests: Dictionary = {}
|
var requests: Dictionary = {}
|
||||||
@@ -212,17 +212,17 @@ static func colonize(world, target: Dictionary, colonies: Array, year: float, ru
|
|||||||
var slots: Dictionary = {"": target.amount.get(id, 0.0)}
|
var slots: Dictionary = {"": target.amount.get(id, 0.0)}
|
||||||
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0.0)
|
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0.0)
|
||||||
var taken: Dictionary = QuantityMath.allocate_whole(slots, amount)
|
var taken: Dictionary = QuantityMath.allocate_whole(slots, amount)
|
||||||
world._scale_batches(target, id, 1.0 - amount / stock[id])
|
world.batches.scale_batches(target, id, 1.0 - amount / stock[id])
|
||||||
stock[id] -= amount
|
stock[id] -= amount
|
||||||
for owner in taken:
|
for owner in taken:
|
||||||
if taken[owner] <= 0.0: continue
|
if taken[owner] <= 0.0: continue
|
||||||
if owner == "": target.amount[id] -= taken[owner]
|
if owner == "": target.amount[id] -= taken[owner]
|
||||||
else: target.binds[owner][id] -= taken[owner]
|
else: target.binds[owner][id] -= taken[owner]
|
||||||
var created: float = world._credit(target, colony.stage, taken[owner])
|
var created: float = world.ledger.credit(target, colony.stage, taken[owner])
|
||||||
if owner != "":
|
if owner != "":
|
||||||
target.amount[colony.stage] -= created
|
target.amount[colony.stage] -= created
|
||||||
target.binds[owner][colony.stage] = target.binds[owner].get(colony.stage, 0.0) + created
|
target.binds[owner][colony.stage] = target.binds[owner].get(colony.stage, 0.0) + created
|
||||||
world._append_batch(target, colony.stage, amount, year, year)
|
world.batches.append_batch(target, colony.stage, amount, year, year)
|
||||||
if not target.ethics.has(colony.stage): target.ethics[colony.stage] = {}
|
if not target.ethics.has(colony.stage): target.ethics[colony.stage] = {}
|
||||||
add(target.ethics[colony.stage], population)
|
add(target.ethics[colony.stage], population)
|
||||||
target.civilization_stats.colonies = target.civilization_stats.get("colonies", 0.0) + count
|
target.civilization_stats.colonies = target.civilization_stats.get("colonies", 0.0) + count
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ static func is_space_civilization(id: String, rules: Dictionary) -> bool:
|
|||||||
static func reconcile(world, target: Dictionary, rules: Dictionary) -> void:
|
static func reconcile(world, target: Dictionary, rules: Dictionary) -> void:
|
||||||
if not target.has("ethics"):
|
if not target.has("ethics"):
|
||||||
target.ethics = {}
|
target.ethics = {}
|
||||||
var physical: Dictionary = world._physical_amounts(target)
|
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
for id in rules:
|
for id in rules:
|
||||||
var count: float = physical.get(id, 0.0)
|
var count: float = physical.get(id, 0.0)
|
||||||
var current: Dictionary = target.ethics.get(id, {})
|
var current: Dictionary = target.ethics.get(id, {})
|
||||||
@@ -40,10 +40,10 @@ static func detach_space_civilizations(target: Dictionary, rules: Dictionary) ->
|
|||||||
# A shared budget per stellar family, including stars bound into structures.
|
# A shared budget per stellar family, including stars bound into structures.
|
||||||
# Reservations affect capacity, not the physical stock or its production.
|
# Reservations affect capacity, not the physical stock or its production.
|
||||||
static func stellar_budget(world, target: Dictionary, rules: Dictionary) -> Dictionary:
|
static func stellar_budget(world, target: Dictionary, rules: Dictionary) -> Dictionary:
|
||||||
var physical: Dictionary = world._physical_amounts(target)
|
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
var capacity: Dictionary = {}
|
var capacity: Dictionary = {}
|
||||||
for id in physical:
|
for id in physical:
|
||||||
var entity = world.entities[id]
|
var entity = world.catalog.entities[id]
|
||||||
for home in ["DwStar", "GiStar", "WD", "NS", "MGT", "BH"]:
|
for home in ["DwStar", "GiStar", "WD", "NS", "MGT", "BH"]:
|
||||||
if entity.event_group == home or entity.count_as == home or id == home:
|
if entity.event_group == home or entity.count_as == home or id == home:
|
||||||
capacity[home] = capacity.get(home, 0.0) + physical[id]
|
capacity[home] = capacity.get(home, 0.0) + physical[id]
|
||||||
@@ -129,10 +129,10 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
|||||||
if excess <= 0.0:
|
if excess <= 0.0:
|
||||||
continue
|
continue
|
||||||
var previous: String = stages[index - 1] if index > 0 else "C1.9"
|
var previous: String = stages[index - 1] if index > 0 else "C1.9"
|
||||||
world._scale_batches(target, id, supported / count)
|
world.batches.scale_batches(target, id, supported / count)
|
||||||
target.amount[id] = supported
|
target.amount[id] = supported
|
||||||
world._credit(target, previous, excess)
|
world.ledger.credit(target, previous, excess)
|
||||||
world._append_batch(target, previous, excess, year, year)
|
world.batches.append_batch(target, previous, excess, year, year)
|
||||||
var removed: Dictionary = QuantityMath.allocate_whole(target.ethics[id], excess)
|
var removed: Dictionary = QuantityMath.allocate_whole(target.ethics[id], excess)
|
||||||
if not target.ethics.has(previous):
|
if not target.ethics.has(previous):
|
||||||
target.ethics[previous] = {}
|
target.ethics[previous] = {}
|
||||||
@@ -141,9 +141,9 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
|||||||
target.ethics[previous][ethic] = target.ethics[previous].get(ethic, 0.0) + removed[ethic]
|
target.ethics[previous][ethic] = target.ethics[previous].get(ethic, 0.0) + removed[ethic]
|
||||||
if previous == "C1.9":
|
if previous == "C1.9":
|
||||||
var stars: Dictionary = {}
|
var stars: Dictionary = {}
|
||||||
var physical: Dictionary = world._physical_amounts(target)
|
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
for star in physical:
|
for star in physical:
|
||||||
if world.entities[star].event_group == "DwStar" and physical[star] > 0.0:
|
if world.catalog.entities[star].event_group == "DwStar" and physical[star] > 0.0:
|
||||||
stars[star] = physical[star]
|
stars[star] = physical[star]
|
||||||
var weights: Dictionary = QuantityMath.normalize_distribution(stars)
|
var weights: Dictionary = QuantityMath.normalize_distribution(stars)
|
||||||
# Worlds are aggregates: a star may host several inhabited planets.
|
# Worlds are aggregates: a star may host several inhabited planets.
|
||||||
@@ -164,16 +164,16 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
|||||||
# Unity's structure chain: direct Claim resets the required structure count;
|
# Unity's structure chain: direct Claim resets the required structure count;
|
||||||
# intervening stages require ten times more units of the preceding structure.
|
# intervening stages require ten times more units of the preceding structure.
|
||||||
static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dictionary:
|
static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dictionary:
|
||||||
var physical: Dictionary = world._physical_amounts(target)
|
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
var structures: Array = []
|
var structures: Array = []
|
||||||
var capacity: Dictionary = {}
|
var capacity: Dictionary = {}
|
||||||
for id in world.entities:
|
for id in world.catalog.entities:
|
||||||
if world.entities[id].claim != "":
|
if world.catalog.entities[id].claim != "":
|
||||||
structures.append(id)
|
structures.append(id)
|
||||||
capacity[id] = physical.get(id, 0.0)
|
capacity[id] = physical.get(id, 0.0)
|
||||||
structures.sort_custom(func(a, b):
|
structures.sort_custom(func(a, b):
|
||||||
var left: float = world.entities[a].civ_bonus
|
var left: float = world.catalog.entities[a].civ_bonus
|
||||||
var right: float = world.entities[b].civ_bonus
|
var right: float = world.catalog.entities[b].civ_bonus
|
||||||
return left > right if left != right else str(a) < str(b))
|
return left > right if left != right else str(a) < str(b))
|
||||||
var stages: Array = []
|
var stages: Array = []
|
||||||
for id in rules:
|
for id in rules:
|
||||||
@@ -186,7 +186,7 @@ static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dict
|
|||||||
for id in stages:
|
for id in stages:
|
||||||
var direct: String = ""
|
var direct: String = ""
|
||||||
for structure in structures:
|
for structure in structures:
|
||||||
if world.entities[structure].claim == id: direct = structure
|
if world.catalog.entities[structure].claim == id: direct = structure
|
||||||
if direct != "":
|
if direct != "":
|
||||||
anchor = direct
|
anchor = direct
|
||||||
units = 1.0
|
units = 1.0
|
||||||
@@ -216,7 +216,7 @@ static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
|
|||||||
if structure == budget.anchors[id]: allowed = true
|
if structure == budget.anchors[id]: allowed = true
|
||||||
if not allowed or budget.capacity[structure] <= 0.0: continue
|
if not allowed or budget.capacity[structure] <= 0.0: continue
|
||||||
var amount: float = left
|
var amount: float = left
|
||||||
if world.entities[structure].claim == id:
|
if world.catalog.entities[structure].claim == id:
|
||||||
amount = minf(amount, maxf(0.0, budget.capacity[structure] - used.get(structure, 0.0)))
|
amount = minf(amount, maxf(0.0, budget.capacity[structure] - used.get(structure, 0.0)))
|
||||||
used[structure] = used.get(structure, 0.0) + amount
|
used[structure] = used.get(structure, 0.0) + amount
|
||||||
if amount > 0.0: homes[structure] = amount
|
if amount > 0.0: homes[structure] = amount
|
||||||
@@ -225,7 +225,7 @@ static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
|
|||||||
target.civilization_homes[id] = homes
|
target.civilization_homes[id] = homes
|
||||||
|
|
||||||
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> float:
|
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> float:
|
||||||
if not rules.has(id): return world.entities[id].lifespan
|
if not rules.has(id): return world.catalog.entities[id].lifespan
|
||||||
var base: float = rules[id].span
|
var base: float = rules[id].span
|
||||||
if not is_space_civilization(id, rules): return base
|
if not is_space_civilization(id, rules): return base
|
||||||
assign_homes(world, target, rules)
|
assign_homes(world, target, rules)
|
||||||
@@ -233,5 +233,5 @@ static func effective_span(world, target: Dictionary, id: String, rules: Diction
|
|||||||
if population <= 0.0: return base
|
if population <= 0.0: return base
|
||||||
var weighted: float = population
|
var weighted: float = population
|
||||||
for home in target.civilization_homes.get(id, {}):
|
for home in target.civilization_homes.get(id, {}):
|
||||||
weighted += target.civilization_homes[id][home] * (maxf(0.0, world.entities[home].civ_bonus) - 1.0)
|
weighted += target.civilization_homes[id][home] * (maxf(0.0, world.catalog.entities[home].civ_bonus) - 1.0)
|
||||||
return base * population / weighted if weighted > 0.0 else base
|
return base * population / weighted if weighted > 0.0 else base
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
extends "res://world/owniverse/simulation_module.gd"
|
||||||
|
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
|
||||||
|
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||||
|
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||||
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
|
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
|
||||||
|
func initialize_life_batches() -> void:
|
||||||
|
CivilizationState.detach_space_civilizations(world.state, Global.CivilizationRules)
|
||||||
|
CivilizationState.enforce_capacity(world, world.state, Global.CivilizationRules, world.state.time.datetime)
|
||||||
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
if world.catalog.entities[id].life_rule.is_empty():
|
||||||
|
continue
|
||||||
|
var tracked: float = 0.0
|
||||||
|
for batch: Dictionary in world.state.batches.get(id, []):
|
||||||
|
tracked += batch.amount
|
||||||
|
world.batches.append_batch(world.state, id, maxf(0.0, physical.get(id, 0.0) - tracked), world.state.time.datetime, world.state.time.datetime)
|
||||||
|
|
||||||
|
|
||||||
|
func evolute_to_datetime(datetime: float) -> void:
|
||||||
|
# Reconcile only when simulation advances.
|
||||||
|
# Fractional frame updates cannot change capacity or populations.
|
||||||
|
var processed_year: float = SimulationTime.processed_year(datetime)
|
||||||
|
if processed_year > world.state.time.year:
|
||||||
|
initialize_life_batches()
|
||||||
|
var previous: Dictionary = world.state
|
||||||
|
var next: Dictionary = previous.duplicate_deep()
|
||||||
|
next.version = SimulationTime.STATE_VERSION
|
||||||
|
next.speed = clampi(int(previous.speed), 1, Global.SPEED_LIMIT)
|
||||||
|
next.speed_in_year = pow(10.0, next.speed - 1)
|
||||||
|
var from_year: float = previous.time.year
|
||||||
|
var to_year: float = processed_year
|
||||||
|
if to_year > from_year:
|
||||||
|
var quantities: Dictionary = world.ledger.physical_amounts(previous)
|
||||||
|
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
|
||||||
|
next.distribution = distribution
|
||||||
|
|
||||||
|
world.automatic.produce_for_period(previous, next, quantities, distribution, from_year, to_year)
|
||||||
|
evolve_batches(next, to_year)
|
||||||
|
world.ledger.refresh_groups(next)
|
||||||
|
world.ledger.clamp_auto_resources(next)
|
||||||
|
WorldParameters.advance(previous, next, Global.CivilizationRules, to_year - from_year)
|
||||||
|
next.time = {"datetime": datetime, "year": to_year}
|
||||||
|
|
||||||
|
# Publish only after every phase has finished; previous stays untouched.
|
||||||
|
world.state = next
|
||||||
|
if to_year > from_year:
|
||||||
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
|
|
||||||
|
|
||||||
|
func evolve_batches(target: Dictionary, to_year: float) -> void:
|
||||||
|
# A long cycle may contain several generations; preserve actual birth intervals.
|
||||||
|
var completed: Dictionary = {}
|
||||||
|
while evolve_batch_generation(target, to_year, completed):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func evolve_batch_generation(target: Dictionary, to_year: float, completed: Dictionary = {}) -> bool:
|
||||||
|
# Apply direct completions at the original next-generation boundary, so
|
||||||
|
# binding transfers, the blast environment and capacity checks keep order.
|
||||||
|
var completed_now: Dictionary = completed.duplicate(true)
|
||||||
|
completed.clear()
|
||||||
|
var quantities: Dictionary = world.ledger.physical_amounts(target)
|
||||||
|
var expired: Dictionary = {}
|
||||||
|
var preview: Dictionary = {"batches": {}}
|
||||||
|
for id in target.batches:
|
||||||
|
if world.catalog.entities.has(id) and world.catalog.entities[id].destroy_level > 0:
|
||||||
|
preview.batches[id] = target.batches[id].duplicate(true)
|
||||||
|
for id in target.batches.keys():
|
||||||
|
if not world.catalog.entities.has(id) or world.catalog.entities[id].destroy_level <= 0:
|
||||||
|
continue
|
||||||
|
var dying: float = 0.0
|
||||||
|
for part: Dictionary in completed_now.get(id, []):
|
||||||
|
dying += part.amount
|
||||||
|
for part: Dictionary in world.batches.withdraw_batches(preview, id, to_year):
|
||||||
|
dying += part.amount
|
||||||
|
if quantities.get(id, 0.0) > 0.0:
|
||||||
|
world.stellar.destroy_planets(target, id, dying / quantities[id], world.catalog.entities[id].destroy_level)
|
||||||
|
quantities = world.ledger.physical_amounts(target)
|
||||||
|
# Withdraw all types before creating successors: iteration order cannot affect them.
|
||||||
|
for id in target.batches.keys():
|
||||||
|
if world.catalog.entities.has(id):
|
||||||
|
expired[id] = completed_now.get(id, []) + world.batches.withdraw_batches(target, id, to_year)
|
||||||
|
var has_expired: bool = false
|
||||||
|
for parts: Array in expired.values():
|
||||||
|
if not parts.is_empty():
|
||||||
|
has_expired = true
|
||||||
|
break
|
||||||
|
if not has_expired:
|
||||||
|
return false
|
||||||
|
var fractions: Dictionary = {}
|
||||||
|
var outputs: Dictionary = {}
|
||||||
|
var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules)
|
||||||
|
var civilization_transaction: Dictionary = CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules)
|
||||||
|
for id in expired:
|
||||||
|
var total: float = 0.0
|
||||||
|
for part: Dictionary in expired[id]:
|
||||||
|
total += part.amount
|
||||||
|
fractions[id] = clampf(total / quantities[id], 0.0, 1.0) if quantities.get(id, 0.0) > 0.0 else 0.0
|
||||||
|
var combined: Dictionary = {}
|
||||||
|
for part: Dictionary in expired[id]:
|
||||||
|
if not part.has("products"):
|
||||||
|
part.products = world.stellar.evolution_products(target, world.catalog.entities[id], part.amount)
|
||||||
|
for successor in part.products.keys():
|
||||||
|
if Global.CivilizationRules.has(id):
|
||||||
|
continue
|
||||||
|
var requested: float = part.products[successor]
|
||||||
|
var admitted: float = CivilizationState.admit_transition(star_budget, Global.CivilizationRules, id, successor, requested)
|
||||||
|
part.products[successor] = admitted
|
||||||
|
if admitted < requested:
|
||||||
|
part.products[id] = part.products.get(id, 0.0) + requested - admitted
|
||||||
|
for successor in part.products:
|
||||||
|
combined[successor] = combined.get(successor, 0.0) + part.products[successor]
|
||||||
|
outputs[id] = {}
|
||||||
|
if total > 0.0:
|
||||||
|
for successor in combined:
|
||||||
|
outputs[id][successor] = combined[successor] / total
|
||||||
|
|
||||||
|
var incoming: Dictionary = world.ledger.transfer_evolution(target, fractions, outputs)
|
||||||
|
|
||||||
|
var new_owners: Dictionary = {}
|
||||||
|
var blast_power: float = 0.0
|
||||||
|
var environment: Dictionary = world.environment.stellar_environment(target)
|
||||||
|
for id in expired:
|
||||||
|
for part: Dictionary in expired[id]:
|
||||||
|
for successor in part.products:
|
||||||
|
var amount: float = part.products[successor]
|
||||||
|
if successor == "blast":
|
||||||
|
blast_power += amount
|
||||||
|
continue
|
||||||
|
if not world.catalog.entities.has(successor):
|
||||||
|
continue
|
||||||
|
if successor == id and (not world.catalog.entities[id].life_rule.is_empty() or Global.CivilizationRules.has(id)):
|
||||||
|
target.amount[id] = target.amount.get(id, 0.0) + amount
|
||||||
|
else:
|
||||||
|
amount = world.ledger.credit(target, successor, amount)
|
||||||
|
var before_batches: Array = target.batches.get(successor, []).duplicate(true)
|
||||||
|
var cutoff: float = to_year if world.stellar.uses_short_lived_fast_path(successor) else -INF
|
||||||
|
var integral: float = world.batches.append_batch(target, successor, amount, part.from, part.to, completed, cutoff)
|
||||||
|
world.automatic.produce_newborn_lifetime(target, successor, amount, part.from, part.to, to_year, before_batches, integral)
|
||||||
|
new_owners[successor] = new_owners.get(successor, 0.0) + amount
|
||||||
|
world.ledger.accept_transfers(target, incoming)
|
||||||
|
world.ledger.bind_new_owners(target, new_owners)
|
||||||
|
world.ledger.round_object_allocations(target)
|
||||||
|
CivilizationEvolution.finish(world, target, civilization_transaction, to_year, Global.CivilizationRules)
|
||||||
|
world.stellar.apply_blast(target, blast_power, environment)
|
||||||
|
CivilizationState.enforce_capacity(world, target, Global.CivilizationRules, to_year)
|
||||||
|
return true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bok6yra83r83c
|
||||||
@@ -18,8 +18,8 @@ static func colored(text: String, color: String) -> String:
|
|||||||
|
|
||||||
static func matches(world, id: String) -> Array:
|
static func matches(world, id: String) -> Array:
|
||||||
var result: Array = []
|
var result: Array = []
|
||||||
for key in world.entities:
|
for key in world.catalog.entities:
|
||||||
var entity = world.entities[key]
|
var entity = world.catalog.entities[key]
|
||||||
if key == id or entity.count_as == id or entity.group_as == id:
|
if key == id or entity.count_as == id or entity.group_as == id:
|
||||||
if not world.catalog.is_group(key):
|
if not world.catalog.is_group(key):
|
||||||
result.append(key)
|
result.append(key)
|
||||||
@@ -28,7 +28,7 @@ static func matches(world, id: String) -> Array:
|
|||||||
static func quantities(world, id: String) -> Dictionary:
|
static func quantities(world, id: String) -> Dictionary:
|
||||||
var free: float = 0.0
|
var free: float = 0.0
|
||||||
var total: float = 0.0
|
var total: float = 0.0
|
||||||
var physical: Dictionary = world._physical_amounts(world.state)
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
for key in matches(world, id):
|
for key in matches(world, id):
|
||||||
free += world.state.amount.get(key, 0.0)
|
free += world.state.amount.get(key, 0.0)
|
||||||
total += physical.get(key, 0.0)
|
total += physical.get(key, 0.0)
|
||||||
@@ -38,15 +38,15 @@ static func quantities(world, id: String) -> Dictionary:
|
|||||||
return {"free": free, "total": total}
|
return {"free": free, "total": total}
|
||||||
|
|
||||||
static func costs(world, global: Node, id: String) -> String:
|
static func costs(world, global: Node, id: String) -> String:
|
||||||
if not world.entities.has(id):
|
if not world.catalog.entities.has(id):
|
||||||
return ""
|
return ""
|
||||||
var lines: PackedStringArray = []
|
var lines: PackedStringArray = []
|
||||||
for requirements in [world.entities[id].cost, world.entities[id].bind]:
|
for requirements in [world.catalog.entities[id].cost, world.catalog.entities[id].bind]:
|
||||||
for resource in requirements:
|
for resource in requirements:
|
||||||
var required: float = requirements[resource]
|
var required: float = requirements[resource]
|
||||||
if required < 0.0:
|
if required < 0.0:
|
||||||
continue
|
continue
|
||||||
var available: float = floorf(world._available(world.state, resource))
|
var available: float = floorf(world.ledger.available(world.state, resource))
|
||||||
var name: String = str(global.get_template(resource, "Entity", "Name").get("text", resource))
|
var name: String = str(global.get_template(resource, "Entity", "Name").get("text", resource))
|
||||||
lines.append(colored(name + ": " + number(required) + " (" + number(available) + ")", GREEN if available >= required else RED))
|
lines.append(colored(name + ": " + number(required) + " (" + number(available) + ")", GREEN if available >= required else RED))
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
@@ -55,25 +55,25 @@ static func variables(world, global: Node, id: String, rate: float = 0.0) -> Dic
|
|||||||
var counts: Dictionary = quantities(world, id)
|
var counts: Dictionary = quantities(world, id)
|
||||||
var show_total: bool = false
|
var show_total: bool = false
|
||||||
for key in matches(world, id):
|
for key in matches(world, id):
|
||||||
show_total = show_total or world.entities[key].group_as == "Planet"
|
show_total = show_total or world.catalog.entities[key].group_as == "Planet"
|
||||||
var amount: String = number(counts.total if show_total else counts.free) if world.entities.has(id) else "—"
|
var amount: String = number(counts.total if show_total else counts.free) if world.catalog.entities.has(id) else "—"
|
||||||
if not show_total and counts.total > counts.free:
|
if not show_total and counts.total > counts.free:
|
||||||
amount += " " + colored("(" + number(counts.total) + ")", GREY)
|
amount += " " + colored("(" + number(counts.total) + ")", GREY)
|
||||||
if rate != 0.0:
|
if rate != 0.0:
|
||||||
amount += " | " + colored(("−" if rate < 0.0 else "+") + number(absf(rate), 1), RED if rate < 0.0 else GREEN)
|
amount += " | " + colored(("−" if rate < 0.0 else "+") + number(absf(rate), 1), RED if rate < 0.0 else GREEN)
|
||||||
var physical: Dictionary = world._physical_amounts(world.state)
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
var voids: float = 0.0
|
var voids: float = 0.0
|
||||||
for key in physical:
|
for key in physical:
|
||||||
voids += physical[key] * world.entities[key].voids
|
voids += physical[key] * world.catalog.entities[key].voids
|
||||||
var he: float = world.state.amount.get("He", 0.0)
|
var he: float = world.state.amount.get("He", 0.0)
|
||||||
var h: float = world.state.amount.get("H", 0.0)
|
var h: float = world.state.amount.get("H", 0.0)
|
||||||
var mixture: String = "—"
|
var mixture: String = "—"
|
||||||
if he + h > 0.0:
|
if he + h > 0.0:
|
||||||
mixture = "He: " + number(100.0 * he / (he + h), 2) + "% / H: " + number(100.0 * h / (he + h), 2) + "%"
|
mixture = "He: " + number(100.0 * he / (he + h), 2) + "% / H: " + number(100.0 * h / (he + h), 2) + "%"
|
||||||
var environment: Dictionary = world.stellar_environment(world.state)
|
var environment: Dictionary = world.environment.stellar_environment(world.state)
|
||||||
return {
|
return {
|
||||||
"amount": amount, "colored costs": costs(world, global, id),
|
"amount": amount, "colored costs": costs(world, global, id),
|
||||||
"lifespan": number(world.entities[id].lifespan) if world.entities.has(id) else "—",
|
"lifespan": number(world.catalog.entities[id].lifespan) if world.catalog.entities.has(id) else "—",
|
||||||
"voids": number(voids), "he / h": "\n" + mixture,
|
"voids": number(voids), "he / h": "\n" + mixture,
|
||||||
"metallicity": number(environment.metallicity * 100.0, 2) + "%",
|
"metallicity": number(environment.metallicity * 100.0, 2) + "%",
|
||||||
"энергия": number(world.state.energy), "плотность": number(environment.density, 6),
|
"энергия": number(world.state.energy), "плотность": number(environment.density, 6),
|
||||||
@@ -85,7 +85,7 @@ static func render(world, global: Node, id: String, category: String = "Entity",
|
|||||||
var values: Dictionary = variables(world, global, id, rate)
|
var values: Dictionary = variables(world, global, id, rate)
|
||||||
if category == "Indicator":
|
if category == "Indicator":
|
||||||
if id == "Density":
|
if id == "Density":
|
||||||
id = Parameters.density_display(world.stellar_environment(world.state).density).mode
|
id = Parameters.density_display(world.environment.stellar_environment(world.state).density).mode
|
||||||
var scales: Dictionary = {"Burnability": "Bur", "Explosiveness": "Exp", "Aggression": "Agg"}
|
var scales: Dictionary = {"Burnability": "Bur", "Explosiveness": "Exp", "Aggression": "Agg"}
|
||||||
if scales.has(id):
|
if scales.has(id):
|
||||||
var level: float = Parameters.value(world.state, scales[id])
|
var level: float = Parameters.value(world.state, scales[id])
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
extends "res://world/owniverse/simulation_module.gd"
|
||||||
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
|
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
|
||||||
|
func life_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary:
|
||||||
|
var rule: Dictionary = entity.life_rule
|
||||||
|
var pending: Dictionary = {}
|
||||||
|
var modifiers: Dictionary = WorldParameters.life_modifiers(target)
|
||||||
|
var probabilities: Dictionary = {
|
||||||
|
"progress": float(rule.progress) * modifiers.progress * WorldParameters.value(target, "Enlightenment"),
|
||||||
|
"degradation": float(rule.degradation) * modifiers.degradation,
|
||||||
|
}
|
||||||
|
var probability_sum: float = maxf(1.0, probabilities.progress + probabilities.degradation)
|
||||||
|
if not target.has("life_remainders"):
|
||||||
|
target.life_remainders = {}
|
||||||
|
for channel in ["progress", "degradation"]:
|
||||||
|
var key: String = entity.id + ":" + channel
|
||||||
|
var expected: float = count * probabilities[channel] / probability_sum + target.life_remainders.get(key, 0.0)
|
||||||
|
target.life_remainders[key] = expected
|
||||||
|
pending[channel] = floorf(snappedf(expected, 1.0e-12))
|
||||||
|
var assigned: Dictionary = QuantityMath.allocate_whole(pending, count)
|
||||||
|
for channel in assigned:
|
||||||
|
var key: String = entity.id + ":" + channel
|
||||||
|
target.life_remainders[key] = maxf(0.0, target.life_remainders[key] - assigned[channel])
|
||||||
|
var result: Dictionary = {entity.id: count - assigned.progress - assigned.degradation}
|
||||||
|
result[rule.next] = assigned.progress
|
||||||
|
var weights: Dictionary = {}
|
||||||
|
var weight_sum: float = 0.0
|
||||||
|
for term in str(rule.fall).split("^"):
|
||||||
|
var pair: PackedStringArray = term.split(":")
|
||||||
|
weights[pair[0]] = float(pair[1])
|
||||||
|
weight_sum += float(pair[1])
|
||||||
|
var requests: Dictionary = {}
|
||||||
|
for id in weights:
|
||||||
|
var key: String = entity.id + ":fall:" + id
|
||||||
|
var expected: float = assigned.degradation * weights[id] / weight_sum + target.life_remainders.get(key, 0.0)
|
||||||
|
target.life_remainders[key] = expected
|
||||||
|
requests[id] = maxf(0.0, floorf(snappedf(expected, 1.0e-12)))
|
||||||
|
var falls: Dictionary = QuantityMath.allocate_whole(requests, assigned.degradation)
|
||||||
|
var distributed: float = 0.0
|
||||||
|
for id in falls:
|
||||||
|
distributed += falls[id]
|
||||||
|
result[id] = result.get(id, 0.0) + falls[id]
|
||||||
|
var key: String = entity.id + ":fall:" + id
|
||||||
|
target.life_remainders[key] -= falls[id]
|
||||||
|
# Fill sub-unit fall quotas deterministically, retaining signed allocation errors.
|
||||||
|
while distributed < assigned.degradation:
|
||||||
|
var selected: String = ""
|
||||||
|
var largest: float = -INF
|
||||||
|
for id in weights:
|
||||||
|
var rest: float = target.life_remainders[entity.id + ":fall:" + id]
|
||||||
|
if rest > largest:
|
||||||
|
largest = rest
|
||||||
|
selected = id
|
||||||
|
result[selected] = result.get(selected, 0.0) + 1.0
|
||||||
|
target.life_remainders[entity.id + ":fall:" + selected] -= 1.0
|
||||||
|
distributed += 1.0
|
||||||
|
return result
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://chr76vnysheee
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
extends "res://world/owniverse/simulation_module.gd"
|
||||||
|
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||||
|
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
|
||||||
|
func try_production(target: Dictionary, entity: OwniverseEntity, amount: float,
|
||||||
|
automatic: bool = false) -> bool:
|
||||||
|
if amount <= 0.0 or amount != floorf(amount):
|
||||||
|
return false
|
||||||
|
var trial: Dictionary = target.duplicate()
|
||||||
|
trial.amount = target.amount.duplicate()
|
||||||
|
trial.binds = target.binds.duplicate(true)
|
||||||
|
trial.auto_resources = target.auto_resources.duplicate()
|
||||||
|
trial.batches = target.batches.duplicate()
|
||||||
|
var copied_batches: Dictionary = {}
|
||||||
|
for resource in entity.cost:
|
||||||
|
var cost: float = roundf(maxf(0.0, entity.cost[resource]) * amount)
|
||||||
|
if automatic and trial.auto_resources.get(resource, 0.0) < cost:
|
||||||
|
return false
|
||||||
|
var before: Dictionary = world.ledger.physical_amounts(trial)
|
||||||
|
var taken: Dictionary = world.ledger.take_available(trial, resource, cost)
|
||||||
|
var total: float = 0.0
|
||||||
|
for quantity in taken.values():
|
||||||
|
total += quantity
|
||||||
|
if total != cost:
|
||||||
|
return false
|
||||||
|
if automatic:
|
||||||
|
trial.auto_resources[resource] = maxf(0.0, trial.auto_resources.get(resource, 0.0) - cost)
|
||||||
|
for id in taken:
|
||||||
|
# Isolate only batches actually touched by this transaction.
|
||||||
|
if trial.batches.has(id) and not copied_batches.has(id):
|
||||||
|
trial.batches[id] = trial.batches[id].duplicate(true)
|
||||||
|
copied_batches[id] = true
|
||||||
|
var factor: float = 1.0 - taken[id] / before[id] if before.get(id, 0.0) > 0.0 else 0.0
|
||||||
|
world.batches.scale_batches(trial, id, factor)
|
||||||
|
if not world.ledger.bind_new_owners(trial, {entity.id: amount}, true):
|
||||||
|
return false
|
||||||
|
world.ledger.refresh_groups(trial)
|
||||||
|
world.ledger.clamp_auto_resources(trial)
|
||||||
|
for field in ["amount", "binds", "batches", "auto_resources"]:
|
||||||
|
if trial.has(field):
|
||||||
|
target[field] = trial[field]
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func smbh_sources() -> Dictionary:
|
||||||
|
var masses: Dictionary = {}
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||||
|
if entity.count_as == "BH" and entity.mass > 0.0:
|
||||||
|
masses[id] = floorf(maxf(0.0, world.state.amount.get(id, 0.0))) * entity.mass
|
||||||
|
return masses
|
||||||
|
|
||||||
|
|
||||||
|
func smbh_available() -> float:
|
||||||
|
var mass: float = 0.0
|
||||||
|
for value in smbh_sources().values():
|
||||||
|
mass += value
|
||||||
|
return floorf(mass)
|
||||||
|
|
||||||
|
|
||||||
|
func plan_smbh(desired_amount: float) -> Dictionary:
|
||||||
|
var empty: Dictionary = {"amount": 0.0, "taken": {}}
|
||||||
|
if desired_amount <= 0.0 or not is_finite(desired_amount):
|
||||||
|
return empty
|
||||||
|
var total_mass: float = smbh_available()
|
||||||
|
if total_mass <= 0.0:
|
||||||
|
return empty
|
||||||
|
var requested: float = minf(floorf(desired_amount), total_mass)
|
||||||
|
if requested <= 0.0:
|
||||||
|
return empty
|
||||||
|
var masses: Dictionary = smbh_sources()
|
||||||
|
var distribution: Dictionary = QuantityMath.normalize_distribution(masses)
|
||||||
|
var taken: Dictionary = {}
|
||||||
|
var order: Array = []
|
||||||
|
var consumed_mass: float = 0.0
|
||||||
|
for id in distribution:
|
||||||
|
var expected: float = requested * distribution[id] / world.catalog.entities[id].mass
|
||||||
|
taken[id] = minf(floorf(expected), floorf(world.state.amount[id]))
|
||||||
|
consumed_mass += taken[id] * world.catalog.entities[id].mass
|
||||||
|
order.append({"id": id, "remainder": expected - taken[id]})
|
||||||
|
order.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
return a.remainder > b.remainder if a.remainder != b.remainder else str(a.id) < str(b.id))
|
||||||
|
# Consume whole BH only; all excess mass becomes SMBH, never disappears.
|
||||||
|
for candidate: Dictionary in order:
|
||||||
|
if consumed_mass >= requested:
|
||||||
|
break
|
||||||
|
var id: String = candidate.id
|
||||||
|
if taken[id] < floorf(world.state.amount[id]):
|
||||||
|
taken[id] += 1.0
|
||||||
|
consumed_mass += world.catalog.entities[id].mass
|
||||||
|
if consumed_mass <= 0.0 or consumed_mass != floorf(consumed_mass):
|
||||||
|
return empty
|
||||||
|
return {"amount": consumed_mass, "taken": taken}
|
||||||
|
|
||||||
|
|
||||||
|
func produce_smbh(desired_amount: float) -> float:
|
||||||
|
var plan: Dictionary = plan_smbh(desired_amount)
|
||||||
|
var consumed_mass: float = plan.amount
|
||||||
|
var taken: Dictionary = plan.taken
|
||||||
|
if consumed_mass <= 0.0:
|
||||||
|
return 0.0
|
||||||
|
var next: Dictionary = world.state.duplicate(true)
|
||||||
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
|
for id in taken:
|
||||||
|
if taken[id] <= 0.0:
|
||||||
|
continue
|
||||||
|
next.amount[id] -= taken[id]
|
||||||
|
var fraction: float = taken[id] / physical[id]
|
||||||
|
world.batches.scale_batches(next, id, 1.0 - fraction)
|
||||||
|
# Release the consumed owners' share of any resources they held.
|
||||||
|
for resource in next.binds.get(id, {}):
|
||||||
|
var released: float = next.binds[id][resource] * fraction
|
||||||
|
next.binds[id][resource] -= released
|
||||||
|
next.amount[resource] = next.amount.get(resource, 0.0) + released
|
||||||
|
world.ledger.credit(next, "SMBH", consumed_mass)
|
||||||
|
world.batches.append_batch(next, "SMBH", consumed_mass, world.state.time.datetime, world.state.time.datetime)
|
||||||
|
world.ledger.round_object_allocations(next)
|
||||||
|
world.ledger.refresh_groups(next)
|
||||||
|
world.ledger.clamp_auto_resources(next)
|
||||||
|
world.state = next
|
||||||
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
|
return consumed_mass
|
||||||
|
|
||||||
|
|
||||||
|
func produce(item_id: String, desired_amount: float) -> float:
|
||||||
|
if item_id == "SMBH":
|
||||||
|
return produce_smbh(desired_amount)
|
||||||
|
# Use special production for S and H
|
||||||
|
if item_id == "S" or item_id == "H":
|
||||||
|
return produce_hs(item_id, desired_amount)
|
||||||
|
|
||||||
|
var entity: OwniverseEntity = world.catalog.entities[item_id]
|
||||||
|
|
||||||
|
if desired_amount <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
var max_producible = floorf(calculate_max_producible(entity, desired_amount))
|
||||||
|
var budget: Dictionary = CivilizationState.stellar_budget(world, world.state, Global.CivilizationRules)
|
||||||
|
max_producible = CivilizationState.admit_transition(budget, Global.CivilizationRules, "", item_id, max_producible)
|
||||||
|
|
||||||
|
if max_producible <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
var metallicity: float = world.environment.stellar_environment(world.state).metallicity
|
||||||
|
if not try_production(world.state, entity, max_producible):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
# Add produced entities
|
||||||
|
world.ledger.process_entity(item_id, max_producible)
|
||||||
|
world.batches.add_entity_to_batch(item_id, max_producible, world.state.time.datetime)
|
||||||
|
world.stellar.create_planets(world.state, item_id, max_producible, world.state.time.datetime, world.state.time.datetime, metallicity)
|
||||||
|
world.ledger.refresh_groups(world.state)
|
||||||
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
|
return max_producible
|
||||||
|
|
||||||
|
|
||||||
|
func produce_hs(entity_id: String, value: float) -> float:
|
||||||
|
if value <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
var to_produce: float = floorf(value)
|
||||||
|
to_produce = minf(to_produce, floorf(world.state.energy))
|
||||||
|
|
||||||
|
if to_produce <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
world.state.energy -= to_produce
|
||||||
|
world.ledger.process_entity(entity_id, to_produce)
|
||||||
|
|
||||||
|
return to_produce
|
||||||
|
|
||||||
|
|
||||||
|
func production_step(entity: OwniverseEntity) -> float:
|
||||||
|
return QuantityMath.production_step(entity.cost.values() + entity.bind.values())
|
||||||
|
|
||||||
|
|
||||||
|
func calculate_max_producible(entity: OwniverseEntity, desired_amount: float = 1.0E150) -> float:
|
||||||
|
if entity.id == "SMBH":
|
||||||
|
return plan_smbh(desired_amount).amount
|
||||||
|
var requirements: Dictionary = {}
|
||||||
|
for resource in entity.cost:
|
||||||
|
requirements[resource] = maxf(0.0, entity.cost[resource])
|
||||||
|
for resource in entity.bind:
|
||||||
|
requirements[resource] = requirements.get(resource, 0.0) + maxf(0.0, entity.bind[resource])
|
||||||
|
var has_requirements: bool = false
|
||||||
|
for quantity in requirements.values():
|
||||||
|
if quantity > 0.0:
|
||||||
|
has_requirements = true
|
||||||
|
break
|
||||||
|
# No recipe means unavailable for production, not unlimited free objects.
|
||||||
|
if not has_requirements:
|
||||||
|
return 0.0
|
||||||
|
var available: float = maxf(0.0, desired_amount)
|
||||||
|
for resource in requirements:
|
||||||
|
if requirements[resource] > 0.0:
|
||||||
|
available = minf(available, floorf(world.ledger.available(world.state, resource) / requirements[resource]))
|
||||||
|
var concrete: Dictionary = world.ledger.expand_requirements(world.state, requirements)
|
||||||
|
for resource in concrete:
|
||||||
|
if concrete[resource] > 0.0:
|
||||||
|
available = minf(available, floorf(world.ledger.available(world.state, resource) / concrete[resource]))
|
||||||
|
var step: float = production_step(entity)
|
||||||
|
return floorf(available / step) * step if is_finite(step) else 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dbxnbd0g6hrab
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
extends "res://world/owniverse/simulation_module.gd"
|
||||||
|
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||||
|
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||||
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
|
|
||||||
|
func scale_batches(target: Dictionary, id: String, factor: float) -> void:
|
||||||
|
var batches: Array = target.batches.get(id, [])
|
||||||
|
var total: float = 0.0
|
||||||
|
var assigned: float = 0.0
|
||||||
|
var shares: Array = []
|
||||||
|
for batch: Dictionary in batches:
|
||||||
|
var expected: float = batch.amount * maxf(0.0, factor)
|
||||||
|
total += expected
|
||||||
|
batch.amount = floorf(expected)
|
||||||
|
assigned += batch.amount
|
||||||
|
shares.append({"batch": batch, "rest": expected - batch.amount})
|
||||||
|
shares.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
return a.rest > b.rest)
|
||||||
|
for share: Dictionary in shares:
|
||||||
|
if assigned < roundf(total):
|
||||||
|
share.batch.amount += 1.0
|
||||||
|
assigned += 1.0
|
||||||
|
share.batch.withdraw_remainder = minf(share.batch.get("withdraw_remainder", 0.0), share.batch.amount)
|
||||||
|
|
||||||
|
|
||||||
|
func append_batch(target: Dictionary, id: String, amount: float,
|
||||||
|
created_from: float, created_to: float, completed: Dictionary = {},
|
||||||
|
completed_before: float = -INF) -> float:
|
||||||
|
if amount <= 0.0 or not world.catalog.entities.has(id) or world.catalog.entities[id].lifespan <= 0.0:
|
||||||
|
return NAN
|
||||||
|
if created_to < created_from:
|
||||||
|
return NAN
|
||||||
|
if not target.batches.has(id):
|
||||||
|
target.batches[id] = []
|
||||||
|
if not target.has("batch_origins"):
|
||||||
|
target.batch_origins = {}
|
||||||
|
var batches: Array = target.batches[id]
|
||||||
|
var lifespan: float = CivilizationState.effective_span(world, target, id, Global.CivilizationRules)
|
||||||
|
if world.catalog.entities[id].group_as == "Star":
|
||||||
|
lifespan /= WorldParameters.effective(target, "Bur")
|
||||||
|
var batch_duration: float = lifespan / Global.batch_count if lifespan > 10.0 else lifespan
|
||||||
|
if batch_duration <= 0.0:
|
||||||
|
return NAN
|
||||||
|
if not target.batch_origins.has(id):
|
||||||
|
target.batch_origins[id] = batches[0].get("created_from", created_from) if not batches.is_empty() else created_from
|
||||||
|
var origin: float = target.batch_origins[id]
|
||||||
|
var cursor: float = created_from
|
||||||
|
var duration: float = created_to - created_from
|
||||||
|
var remaining_amount: float = amount
|
||||||
|
var completed_amount: float = 0.0
|
||||||
|
var completed_from: float = INF
|
||||||
|
var completed_to: float = -INF
|
||||||
|
# Subtract virtual windows in their original order to preserve floating-point
|
||||||
|
# rounding of lifetime production, rather than subtracting one large sum.
|
||||||
|
var remaining_object_years: float = amount * maxf(0.0, completed_before - (created_from + created_to) * 0.5) if is_finite(completed_before) else NAN
|
||||||
|
# Compute the index once. Recomputing floor from a rounded boundary can
|
||||||
|
# select the preceding window forever (e.g. fractional origins at large ages).
|
||||||
|
var window_index: float = floorf((cursor - origin) / batch_duration)
|
||||||
|
if origin + (window_index + 1.0) * batch_duration <= cursor:
|
||||||
|
window_index += 1.0
|
||||||
|
var batches_by_start: Dictionary = {}
|
||||||
|
for existing: Dictionary in batches:
|
||||||
|
var start: float = existing.get("created_from", -INF)
|
||||||
|
if not batches_by_start.has(start):
|
||||||
|
batches_by_start[start] = []
|
||||||
|
batches_by_start[start].append(existing)
|
||||||
|
while true:
|
||||||
|
var window_start: float = origin + window_index * batch_duration
|
||||||
|
var window_end: float = origin + (window_index + 1.0) * batch_duration
|
||||||
|
if window_end <= cursor:
|
||||||
|
push_error("Batch window exceeds floating-point time precision: " + id)
|
||||||
|
return remaining_object_years
|
||||||
|
var part_end: float = minf(created_to, window_end)
|
||||||
|
var part_amount: float = remaining_amount
|
||||||
|
if duration > 0.0 and part_end < created_to:
|
||||||
|
part_amount = floorf(amount * (part_end - created_from) / duration) - (amount - remaining_amount)
|
||||||
|
if window_end + lifespan <= completed_before:
|
||||||
|
# Retain the original integer allocation and lifetime integral, but
|
||||||
|
# do not allocate thousands of already-dead batch dictionaries.
|
||||||
|
completed_amount += part_amount
|
||||||
|
completed_from = minf(completed_from, window_start + lifespan)
|
||||||
|
completed_to = maxf(completed_to, window_end + lifespan)
|
||||||
|
remaining_object_years -= part_amount * expired_years(completed_before, window_start + lifespan, window_end + lifespan)
|
||||||
|
else:
|
||||||
|
var batch: Dictionary = {}
|
||||||
|
for existing: Dictionary in batches_by_start.get(window_start, []):
|
||||||
|
if existing.get("created_from", -INF) == window_start and existing.start_year == window_start + lifespan and existing.end_year == window_end + lifespan:
|
||||||
|
batch = existing
|
||||||
|
break
|
||||||
|
if batch.is_empty():
|
||||||
|
batch = {
|
||||||
|
"created_from": window_start,
|
||||||
|
"created_to": window_end,
|
||||||
|
"start_year": window_start + lifespan,
|
||||||
|
"end_year": window_end + lifespan,
|
||||||
|
"amount": 0.0,
|
||||||
|
}
|
||||||
|
batches.append(batch)
|
||||||
|
if not batches_by_start.has(window_start):
|
||||||
|
batches_by_start[window_start] = []
|
||||||
|
batches_by_start[window_start].append(batch)
|
||||||
|
batch.amount += part_amount
|
||||||
|
remaining_amount -= part_amount
|
||||||
|
if duration == 0.0 or part_end >= created_to:
|
||||||
|
break
|
||||||
|
cursor = window_end
|
||||||
|
window_index += 1.0
|
||||||
|
batches.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
return a.created_from < b.created_from)
|
||||||
|
if completed_amount > 0.0:
|
||||||
|
if not completed.has(id): completed[id] = []
|
||||||
|
completed[id].append({"amount": completed_amount, "from": completed_from, "to": completed_to})
|
||||||
|
return remaining_object_years
|
||||||
|
|
||||||
|
|
||||||
|
func withdraw_batches(target: Dictionary, id: String, to_year: float) -> Array:
|
||||||
|
var expired: Array = []
|
||||||
|
var remaining: Array = []
|
||||||
|
for batch: Dictionary in target.batches.get(id, []):
|
||||||
|
var start: float = maxf(batch.start_year, batch.get("processed_until", batch.start_year))
|
||||||
|
var end: float = batch.end_year
|
||||||
|
var amount: float = batch.amount
|
||||||
|
if amount <= 0.0:
|
||||||
|
continue
|
||||||
|
if to_year < start or (to_year == start and end > start):
|
||||||
|
remaining.append(batch)
|
||||||
|
continue
|
||||||
|
var consumed: float = amount
|
||||||
|
var until: float = minf(to_year, end)
|
||||||
|
if to_year < end:
|
||||||
|
var carry: float = batch.get("withdraw_remainder", 0.0)
|
||||||
|
var expected: float = carry + (amount - carry) * (to_year - start) / (end - start)
|
||||||
|
consumed = floorf(expected)
|
||||||
|
batch.withdraw_remainder = expected - consumed
|
||||||
|
batch.amount = maxf(0.0, amount - consumed)
|
||||||
|
batch.processed_until = to_year
|
||||||
|
remaining.append(batch)
|
||||||
|
if consumed > 0.0:
|
||||||
|
expired.append({"amount": consumed, "from": start, "to": until})
|
||||||
|
target.batches[id] = remaining
|
||||||
|
return expired
|
||||||
|
|
||||||
|
|
||||||
|
func object_years(source: Dictionary, id: String, quantity: float,
|
||||||
|
from_year: float, to_year: float) -> float:
|
||||||
|
var result: float = quantity * SimulationTime.elapsed_years(from_year, to_year)
|
||||||
|
for batch: Dictionary in source.batches.get(id, []):
|
||||||
|
var start: float = maxf(batch.start_year, batch.get("processed_until", batch.start_year))
|
||||||
|
var end: float = batch.end_year
|
||||||
|
result -= batch.amount * (expired_years(to_year, start, end) - expired_years(from_year, start, end))
|
||||||
|
return maxf(0.0, result)
|
||||||
|
|
||||||
|
|
||||||
|
func expired_years(year: float, start: float, end: float) -> float:
|
||||||
|
if year <= start:
|
||||||
|
return 0.0
|
||||||
|
if end <= start:
|
||||||
|
return year - start
|
||||||
|
if year >= end:
|
||||||
|
return year - end + (end - start) * 0.5
|
||||||
|
return (year - start) * (year - start) / (2.0 * (end - start))
|
||||||
|
|
||||||
|
|
||||||
|
func consume_from_batches(entity: OwniverseEntity, current_year: float) -> float:
|
||||||
|
var total: float = 0.0
|
||||||
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
|
for part: Dictionary in withdraw_batches(world.state, entity.id, current_year):
|
||||||
|
total += part.amount
|
||||||
|
var fraction: float = clampf(total / physical[entity.id], 0.0, 1.0) if physical.get(entity.id, 0.0) > 0.0 else 0.0
|
||||||
|
world.state.amount[entity.id] = world.state.amount.get(entity.id, 0.0) * (1.0 - fraction)
|
||||||
|
for owner_id in world.state.binds:
|
||||||
|
if world.state.binds[owner_id].has(entity.id):
|
||||||
|
world.state.binds[owner_id][entity.id] *= 1.0 - fraction
|
||||||
|
world.ledger.round_object_allocations(world.state)
|
||||||
|
world.ledger.refresh_groups(world.state)
|
||||||
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
func add_entity_to_batch(entity_id: String, amount: float, current_year: float) -> void:
|
||||||
|
append_batch(world.state, entity_id, amount, current_year, current_year)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://wyxpfe2amo5q
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
extends "res://world/owniverse/simulation_module.gd"
|
||||||
|
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||||
|
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
|
||||||
|
func physical_amounts(source: Dictionary) -> Dictionary:
|
||||||
|
var amounts: Dictionary = {}
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
if not world.catalog.is_group(id):
|
||||||
|
amounts[id] = maxf(0.0, source.amount.get(id, 0.0))
|
||||||
|
for bindings: Dictionary in source.binds.values():
|
||||||
|
for id in bindings:
|
||||||
|
if amounts.has(id):
|
||||||
|
amounts[id] += maxf(0.0, bindings[id])
|
||||||
|
return amounts
|
||||||
|
|
||||||
|
|
||||||
|
func available(source: Dictionary, id: String) -> float:
|
||||||
|
var amount: float = 0.0
|
||||||
|
for member in world.catalog.members(id):
|
||||||
|
amount += maxf(0.0, source.amount.get(member, 0.0))
|
||||||
|
return amount
|
||||||
|
|
||||||
|
|
||||||
|
func expand_requirements(source: Dictionary, requirements: Dictionary) -> Dictionary:
|
||||||
|
var expanded: Dictionary = {}
|
||||||
|
for id in requirements:
|
||||||
|
var weights: Dictionary = {}
|
||||||
|
for member in world.catalog.members(id):
|
||||||
|
weights[member] = source.amount.get(member, 0.0)
|
||||||
|
weights = QuantityMath.normalize_distribution(weights)
|
||||||
|
for member in weights:
|
||||||
|
expanded[member] = expanded.get(member, 0.0) + maxf(0.0, requirements[id]) * weights[member]
|
||||||
|
return expanded
|
||||||
|
|
||||||
|
|
||||||
|
func take_available(target: Dictionary, id: String, amount: float) -> Dictionary:
|
||||||
|
var quantities: Dictionary = {}
|
||||||
|
for member in world.catalog.members(id):
|
||||||
|
quantities[member] = target.amount.get(member, 0.0)
|
||||||
|
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
|
||||||
|
var taken: Dictionary = {}
|
||||||
|
var total: float = minf(amount, available(target, id))
|
||||||
|
var discrete: bool = not distribution.is_empty()
|
||||||
|
for member in distribution:
|
||||||
|
discrete = discrete and is_discrete(member)
|
||||||
|
if discrete:
|
||||||
|
total = floorf(total)
|
||||||
|
var shares: Array = []
|
||||||
|
var assigned: float = 0.0
|
||||||
|
for member in distribution:
|
||||||
|
var expected: float = total * distribution[member]
|
||||||
|
taken[member] = floorf(expected) if discrete else expected
|
||||||
|
assigned += taken[member]
|
||||||
|
shares.append({"id": member, "rest": expected - taken[member]})
|
||||||
|
if discrete:
|
||||||
|
shares.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
|
||||||
|
for share: Dictionary in shares:
|
||||||
|
if assigned >= total:
|
||||||
|
break
|
||||||
|
taken[share.id] += 1.0
|
||||||
|
assigned += 1.0
|
||||||
|
for member in taken:
|
||||||
|
target.amount[member] = maxf(0.0, target.amount.get(member, 0.0) - taken[member])
|
||||||
|
return taken
|
||||||
|
|
||||||
|
|
||||||
|
func is_discrete(_id: String) -> bool:
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func credit(target: Dictionary, id: String, amount: float) -> float:
|
||||||
|
if amount <= 0.0:
|
||||||
|
return 0.0
|
||||||
|
if is_discrete(id):
|
||||||
|
if not target.has("production_remainders"):
|
||||||
|
target.production_remainders = {}
|
||||||
|
var expected: float = amount + target.production_remainders.get(id, 0.0)
|
||||||
|
amount = floorf(expected)
|
||||||
|
target.production_remainders[id] = expected - amount
|
||||||
|
target.amount[id] = target.amount.get(id, 0.0) + amount
|
||||||
|
target.stats.total_amount[id] = target.stats.total_amount.get(id, 0.0) + amount
|
||||||
|
if world.catalog.entities.has(id) and world.catalog.entities[id].group_as != "" and world.catalog.entities[id].group_as != id:
|
||||||
|
var group: String = world.catalog.entities[id].group_as
|
||||||
|
target.stats.total_amount[group] = target.stats.total_amount.get(group, 0.0) + amount
|
||||||
|
return amount
|
||||||
|
|
||||||
|
|
||||||
|
func round_object_allocations(target: Dictionary) -> void:
|
||||||
|
var physical: Dictionary = physical_amounts(target)
|
||||||
|
for id in physical:
|
||||||
|
if not is_discrete(id):
|
||||||
|
continue
|
||||||
|
var slots: Array = [ {"owner": "", "value": target.amount.get(id, 0.0)}]
|
||||||
|
for owner_id in target.binds:
|
||||||
|
if target.binds[owner_id].has(id):
|
||||||
|
slots.append({"owner": owner_id, "value": target.binds[owner_id][id]})
|
||||||
|
var assigned: float = 0.0
|
||||||
|
for slot: Dictionary in slots:
|
||||||
|
slot.whole = floorf(slot.value)
|
||||||
|
slot.rest = slot.value - slot.whole
|
||||||
|
assigned += slot.whole
|
||||||
|
slots.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
return a.rest > b.rest if a.rest != b.rest else str(a.owner) < str(b.owner))
|
||||||
|
var left: float = roundf(physical[id]) - assigned
|
||||||
|
for slot: Dictionary in slots:
|
||||||
|
if left > 0.0:
|
||||||
|
slot.whole += 1.0
|
||||||
|
left -= 1.0
|
||||||
|
if slot.owner == "":
|
||||||
|
target.amount[id] = slot.whole
|
||||||
|
else:
|
||||||
|
target.binds[slot.owner][id] = slot.whole
|
||||||
|
|
||||||
|
|
||||||
|
func refresh_groups(target: Dictionary) -> void:
|
||||||
|
var groups: Dictionary = {}
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
var group: String = world.catalog.entities[id].group_as
|
||||||
|
if group != "":
|
||||||
|
groups[group] = groups.get(group, 0.0) + target.amount.get(id, 0.0)
|
||||||
|
for group in groups:
|
||||||
|
target.amount[group] = groups[group]
|
||||||
|
|
||||||
|
|
||||||
|
func clamp_auto_resources(target: Dictionary) -> void:
|
||||||
|
for id in target.get("auto_resources", {}):
|
||||||
|
target.auto_resources[id] = minf(target.auto_resources[id], available(target, id))
|
||||||
|
|
||||||
|
|
||||||
|
func bind_full(target: Dictionary, amounts: Dictionary) -> bool:
|
||||||
|
# Binding only mutates free amounts and owners' holdings.
|
||||||
|
var trial: Dictionary = {
|
||||||
|
"amount": target.amount.duplicate(),
|
||||||
|
"binds": target.binds.duplicate(true),
|
||||||
|
}
|
||||||
|
var requests: Array = []
|
||||||
|
for owner_id in amounts:
|
||||||
|
if not world.catalog.entities.has(owner_id):
|
||||||
|
return false
|
||||||
|
for id in world.catalog.entities[owner_id].bind:
|
||||||
|
var quantity: float = amounts[owner_id] * maxf(0.0, world.catalog.entities[owner_id].bind[id])
|
||||||
|
if not is_equal_approx(quantity, roundf(quantity)):
|
||||||
|
return false
|
||||||
|
if quantity > 0.0:
|
||||||
|
requests.append({"owner": owner_id, "id": id, "amount": roundf(quantity), "members": world.catalog.members(id).size()})
|
||||||
|
requests.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
if a.members != b.members:
|
||||||
|
return a.members < b.members
|
||||||
|
return str(a.id) < str(b.id) if a.id != b.id else str(a.owner) < str(b.owner))
|
||||||
|
for request: Dictionary in requests:
|
||||||
|
var taken: Dictionary = take_available(trial, request.id, request.amount)
|
||||||
|
var total: float = 0.0
|
||||||
|
for quantity in taken.values():
|
||||||
|
total += quantity
|
||||||
|
if total != request.amount:
|
||||||
|
return false
|
||||||
|
if not trial.binds.has(request.owner):
|
||||||
|
trial.binds[request.owner] = {}
|
||||||
|
for id in taken:
|
||||||
|
trial.binds[request.owner][id] = trial.binds[request.owner].get(id, 0.0) + taken[id]
|
||||||
|
target.amount = trial.amount
|
||||||
|
target.binds = trial.binds
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func bind_new_owners(target: Dictionary, amounts: Dictionary, require_full: bool = false) -> bool:
|
||||||
|
if require_full:
|
||||||
|
return bind_full(target, amounts)
|
||||||
|
var requests: Dictionary = {}
|
||||||
|
for owner_id in amounts:
|
||||||
|
if not world.catalog.entities.has(owner_id):
|
||||||
|
continue
|
||||||
|
for id in world.catalog.entities[owner_id].bind:
|
||||||
|
var quantity: float = amounts[owner_id] * maxf(0.0, world.catalog.entities[owner_id].bind[id])
|
||||||
|
var weights: Dictionary = {}
|
||||||
|
for member in world.catalog.members(id):
|
||||||
|
weights[member] = target.amount.get(member, 0.0)
|
||||||
|
weights = QuantityMath.normalize_distribution(weights)
|
||||||
|
for member in weights:
|
||||||
|
if not requests.has(member):
|
||||||
|
requests[member] = {}
|
||||||
|
requests[member][owner_id] = requests[member].get(owner_id, 0.0) + quantity * weights[member]
|
||||||
|
for id in requests:
|
||||||
|
var requested: float = 0.0
|
||||||
|
for quantity in requests[id].values():
|
||||||
|
requested += quantity
|
||||||
|
var available: float = available(target, id)
|
||||||
|
var factor: float = minf(1.0, available / requested) if requested > 0.0 else 0.0
|
||||||
|
for owner_id in requests[id]:
|
||||||
|
var quantity: float = requests[id][owner_id] * factor
|
||||||
|
if not target.binds.has(owner_id):
|
||||||
|
target.binds[owner_id] = {}
|
||||||
|
target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + quantity
|
||||||
|
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - quantity)
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func apply_costs(entity: OwniverseEntity, amount: float) -> void:
|
||||||
|
var costs: Dictionary = {}
|
||||||
|
for resource in entity.cost:
|
||||||
|
costs[resource] = roundf(maxf(0.0, entity.cost[resource]) * amount)
|
||||||
|
for resource in costs:
|
||||||
|
var quantities: Dictionary = physical_amounts(world.state)
|
||||||
|
var taken: Dictionary = take_available(world.state, resource, costs[resource])
|
||||||
|
for id in taken:
|
||||||
|
var factor: float = 1.0 - taken[id] / quantities[id] if quantities.get(id, 0.0) > 0.0 else 0.0
|
||||||
|
world.batches.scale_batches(world.state, id, factor)
|
||||||
|
refresh_groups(world.state)
|
||||||
|
clamp_auto_resources(world.state)
|
||||||
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
|
|
||||||
|
|
||||||
|
func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> bool:
|
||||||
|
if not bind_new_owners(world.state, {item_id: amount}, true):
|
||||||
|
return false
|
||||||
|
refresh_groups(world.state)
|
||||||
|
clamp_auto_resources(world.state)
|
||||||
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func process_entity(entity_id: String, amount: float) -> void:
|
||||||
|
if amount == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update state amount
|
||||||
|
world.state.amount[entity_id] = world.state.amount.get(entity_id, 0.0) + amount
|
||||||
|
world.state.stats.total_amount[entity_id] = world.state.stats.total_amount.get(entity_id, 0.0) + maxf(0.0, amount)
|
||||||
|
|
||||||
|
|
||||||
|
# Handle group updates if entity belongs to a group
|
||||||
|
var entity = world.catalog.entities[entity_id]
|
||||||
|
if entity.group_as != "":
|
||||||
|
world.state.amount[entity.group_as] = world.state.amount.get(entity.group_as, 0.0) + amount
|
||||||
|
world.state.stats.total_amount[entity.group_as] = world.state.stats.total_amount.get(entity.group_as, 0.0) + maxf(0.0, amount)
|
||||||
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
|
|
||||||
|
|
||||||
|
## Freeze holdings before simultaneous transitions and reserve successors.
|
||||||
|
func transfer_evolution(target: Dictionary, fractions: Dictionary, outputs: Dictionary) -> Dictionary:
|
||||||
|
# Use a frozen binding table so simultaneous transitions cannot cascade by type order.
|
||||||
|
var bindings: Dictionary = target.binds.duplicate(true)
|
||||||
|
var incoming: Dictionary = {}
|
||||||
|
for owner_id in bindings:
|
||||||
|
var owner_survival: float = 1.0 - fractions.get(owner_id, 0.0)
|
||||||
|
target.binds[owner_id] = {}
|
||||||
|
for id in bindings[owner_id]:
|
||||||
|
var held: float = bindings[owner_id][id]
|
||||||
|
var released: float = held * (1.0 - owner_survival)
|
||||||
|
target.amount[id] = target.amount.get(id, 0.0) + released
|
||||||
|
var heirs: Dictionary = world.stellar.stellar_successors(outputs.get(owner_id, {})) if world.catalog.entities.has(id) and world.catalog.entities[id].group_as == "Planet" else {}
|
||||||
|
var fraction: float = fractions.get(id, 0.0)
|
||||||
|
for heir in heirs:
|
||||||
|
if not incoming.has(heir):
|
||||||
|
incoming[heir] = {}
|
||||||
|
incoming[heir][id] = incoming[heir].get(id, 0.0) + released * (1.0 - fraction) * heirs[heir]
|
||||||
|
for child in outputs.get(id, {}):
|
||||||
|
if world.catalog.entities.has(child) and not CivilizationState.is_space_civilization(child, Global.CivilizationRules):
|
||||||
|
incoming[heir][child] = incoming[heir].get(child, 0.0) + released * fraction * heirs[heir] * outputs[id][child]
|
||||||
|
var remaining: float = held * owner_survival * (1.0 - fraction)
|
||||||
|
target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + remaining
|
||||||
|
for successor in outputs.get(id, {}):
|
||||||
|
if not world.catalog.entities.has(successor) or CivilizationState.is_space_civilization(successor, Global.CivilizationRules):
|
||||||
|
continue
|
||||||
|
var transferred: float = held * owner_survival * fraction * maxf(0.0, outputs[id][successor])
|
||||||
|
if not incoming.has(owner_id):
|
||||||
|
incoming[owner_id] = {}
|
||||||
|
incoming[owner_id][successor] = incoming[owner_id].get(successor, 0.0) + transferred
|
||||||
|
|
||||||
|
for id in fractions:
|
||||||
|
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) * (1.0 - fractions[id]))
|
||||||
|
|
||||||
|
return incoming
|
||||||
|
|
||||||
|
|
||||||
|
## Commit reservations after newly evolved objects have been credited.
|
||||||
|
func accept_transfers(target: Dictionary, incoming: Dictionary) -> void:
|
||||||
|
for owner_id in incoming:
|
||||||
|
if not target.binds.has(owner_id):
|
||||||
|
target.binds[owner_id] = {}
|
||||||
|
for id in incoming[owner_id]:
|
||||||
|
var transferred: float = minf(incoming[owner_id][id], target.amount.get(id, 0.0))
|
||||||
|
target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + transferred
|
||||||
|
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - transferred)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://buauxqyl73vjb
|
||||||
+12
-1412
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Modules borrow their world; a weak reference prevents ownership cycles.
|
||||||
|
var _world_ref: WeakRef
|
||||||
|
var world:
|
||||||
|
get: return _world_ref.get_ref()
|
||||||
|
|
||||||
|
func _init(owner_world) -> void:
|
||||||
|
_world_ref = weakref(owner_world)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://orcojklh0nne
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
extends "res://world/owniverse/simulation_module.gd"
|
||||||
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
|
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
|
||||||
|
## Testable optimization switch; both paths preserve the same event order.
|
||||||
|
var direct_completions: bool = true
|
||||||
|
|
||||||
|
func add_output_terms(outputs: Dictionary, terms: String, count: float) -> void:
|
||||||
|
if count <= 0.0:
|
||||||
|
return
|
||||||
|
for term in terms.split("&"):
|
||||||
|
var parts: PackedStringArray = term.strip_edges().split(":")
|
||||||
|
var id: String = parts[0].strip_edges()
|
||||||
|
var amount: float = float(parts[1]) if parts.size() == 2 else 1.0
|
||||||
|
if id != "" and amount > 0.0:
|
||||||
|
outputs[id] = outputs.get(id, 0.0) + count * amount
|
||||||
|
|
||||||
|
|
||||||
|
func evolution_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary:
|
||||||
|
if not entity.life_rule.is_empty():
|
||||||
|
return world.life.life_products(target, entity, count)
|
||||||
|
var outputs: Dictionary = {}
|
||||||
|
for id in entity.output_and:
|
||||||
|
outputs[id] = count * maxf(0.0, entity.output_and[id])
|
||||||
|
if not entity.output_or.is_empty():
|
||||||
|
# The parser stores the percentage after '%', e.g. 0.1 means 0.1 percent.
|
||||||
|
var chance: float = clampf(entity.output_or.get("chance", 0.0) / 100.0 * WorldParameters.effective(target, "Exp"), 0.0, 1.0)
|
||||||
|
if not target.has("evolution_remainders"):
|
||||||
|
target.evolution_remainders = {}
|
||||||
|
var expected: float = count * chance + target.evolution_remainders.get(entity.id, 0.0)
|
||||||
|
var positive: float = minf(count, floorf(snappedf(expected, 1.0e-12)))
|
||||||
|
target.evolution_remainders[entity.id] = maxf(0.0, expected - positive)
|
||||||
|
add_output_terms(outputs, str(entity.output_or.get("positive", "")), positive)
|
||||||
|
add_output_terms(outputs, str(entity.output_or.get("negative", "")), count - positive)
|
||||||
|
# Entity.blast is authoritative even if imported output still contains
|
||||||
|
# blast:N. Zero disables the event; never add both representations.
|
||||||
|
outputs.erase("blast")
|
||||||
|
if entity.blast > 0:
|
||||||
|
outputs["blast"] = count * entity.blast * WorldParameters.effective(target, "Exp")
|
||||||
|
return outputs
|
||||||
|
|
||||||
|
|
||||||
|
func create_planets(target: Dictionary, star_id: String, count: float,
|
||||||
|
from_year: float, to_year: float, metallicity: float) -> void:
|
||||||
|
if count <= 0.0 or world.catalog.entities[star_id].planets.is_empty():
|
||||||
|
return
|
||||||
|
if not target.binds.has(star_id):
|
||||||
|
target.binds[star_id] = {}
|
||||||
|
for id in world.catalog.entities[star_id].planets:
|
||||||
|
if not world.catalog.entities.has(id):
|
||||||
|
continue
|
||||||
|
var rule: Dictionary = world.catalog.entities[star_id].planets[id]
|
||||||
|
var amount: float = QuantityMath.accumulate_whole(target, "planet_remainders", star_id + ":" + id,
|
||||||
|
count * rule.amount * rule.chance * maxf(0.0, metallicity))
|
||||||
|
amount = world.ledger.credit(target, id, amount)
|
||||||
|
target.amount[id] -= amount
|
||||||
|
target.binds[star_id][id] = target.binds[star_id].get(id, 0.0) + amount
|
||||||
|
world.batches.append_batch(target, id, amount, from_year, to_year)
|
||||||
|
|
||||||
|
|
||||||
|
func stellar_successors(products: Dictionary) -> Dictionary:
|
||||||
|
var weights: Dictionary = {}
|
||||||
|
for id in products:
|
||||||
|
if world.catalog.entities.has(id) and (world.catalog.entities[id].group_as in ["Star", "BH"] or world.catalog.entities[id].event_group == "SNGRBB" or world.catalog.entities[id].count_as == "BD"):
|
||||||
|
weights[id] = products[id]
|
||||||
|
return QuantityMath.normalize_distribution(weights)
|
||||||
|
|
||||||
|
|
||||||
|
func destroy_planets(target: Dictionary, owner_id: String, ratio: float, level: int, cause: String = "local") -> void:
|
||||||
|
if ratio <= 0.0 or level not in [1, 2]:
|
||||||
|
return
|
||||||
|
var held: Dictionary = target.binds.get(owner_id, {})
|
||||||
|
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
|
var rocky: float = 0.0
|
||||||
|
for id in held.keys():
|
||||||
|
if not world.catalog.entities.has(id) or world.catalog.entities[id].group_as != "Planet":
|
||||||
|
continue
|
||||||
|
if level == 1 and not world.catalog.entities[id].blastability:
|
||||||
|
continue
|
||||||
|
var key: String = cause + ":" + str(level) + ":" + owner_id + ":" + id
|
||||||
|
var destroyed: float = minf(held[id], QuantityMath.accumulate_whole(target, "destruction_remainders", key, held[id] * minf(1.0, ratio)))
|
||||||
|
if destroyed == held[id]:
|
||||||
|
target.destruction_remainders[key] = 0.0
|
||||||
|
if destroyed <= 0.0:
|
||||||
|
continue
|
||||||
|
held[id] -= destroyed
|
||||||
|
world.batches.scale_batches(target, id, 1.0 - destroyed / physical[id])
|
||||||
|
if level == 1:
|
||||||
|
rocky += destroyed
|
||||||
|
if rocky > 0.0:
|
||||||
|
rocky = world.ledger.credit(target, "RP", rocky)
|
||||||
|
target.amount.RP -= rocky
|
||||||
|
held.RP = held.get("RP", 0.0) + rocky
|
||||||
|
|
||||||
|
|
||||||
|
func apply_blast(target: Dictionary, power: float, environment: Dictionary) -> void:
|
||||||
|
var hits: float = QuantityMath.accumulate_whole(target, "blast_remainders", "hits", power * environment.blast)
|
||||||
|
var quantities: Dictionary = world.ledger.physical_amounts(target)
|
||||||
|
var weights: Dictionary = {}
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
if not world.catalog.entities[id].planets.is_empty() and quantities.get(id, 0.0) > 0.0:
|
||||||
|
weights[id] = quantities[id]
|
||||||
|
var distribution: Dictionary = QuantityMath.normalize_distribution(weights)
|
||||||
|
# Fractional exposure is deterministic; destruction itself remains whole.
|
||||||
|
for id in distribution:
|
||||||
|
destroy_planets(target, id, hits * distribution[id] / quantities[id] * environment.density, 1, "blast")
|
||||||
|
|
||||||
|
|
||||||
|
func uses_short_lived_fast_path(id: String) -> bool:
|
||||||
|
if not direct_completions: return false
|
||||||
|
# Only terminal stellar events with linear outputs and no living successors.
|
||||||
|
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||||
|
if entity.event_group != "SNGRBB" or not entity.output_or.is_empty() or not entity.life_rule.is_empty():
|
||||||
|
return false
|
||||||
|
for successor in entity.output_and:
|
||||||
|
if world.catalog.entities.has(successor) and world.catalog.entities[successor].lifespan > 0.0:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cim2wuoas2ojv
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
extends "res://world/owniverse/simulation_module.gd"
|
||||||
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
|
|
||||||
|
func stellar_environment(source: Dictionary) -> Dictionary:
|
||||||
|
var quantities: Dictionary = world.ledger.physical_amounts(source)
|
||||||
|
# SMBH is a self-group in GI; its quantity stores mass, not object count.
|
||||||
|
var mass: float = source.amount.get("SMBH", 0.0)
|
||||||
|
var volume: float = quantities.get("S", 0.0)
|
||||||
|
for id in quantities:
|
||||||
|
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||||
|
if id in ["H", "He", "Met", "SMBH"]:
|
||||||
|
mass += quantities[id]
|
||||||
|
else:
|
||||||
|
mass += quantities[id] * maxf(0.0, entity.mass)
|
||||||
|
volume += quantities[id] * maxf(0.0, entity.space)
|
||||||
|
volume += quantities[id] * maxf(0.0, entity.voids) * 1.0e22
|
||||||
|
var metallicity: float = 0.0
|
||||||
|
if mass > 0.0:
|
||||||
|
metallicity = maxf(0.0, 0.4 * log(100.0 * source.amount.get("Met", 0.0) / mass + 0.11) + 0.88)
|
||||||
|
var base_mass: float = 65536.0 * (pow(mass, 0.1) if mass > 0.0 and mass < 1.0e10 else (10.0 if mass >= 1.0e10 else 1.0))
|
||||||
|
return {
|
||||||
|
"metallicity": metallicity,
|
||||||
|
"density": 1.23456e-7 * sqrt(base_mass + mass) / pow(maxf(1.0, volume), 1.0 / 6.0),
|
||||||
|
"blast": clampf(source.amount.get("S", 0.0) / volume, 0.0, 1.0) if volume > 0.0 else 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func modify_world_parameter(key: String, change: float) -> void:
|
||||||
|
if key not in ["Bur", "Exp", "Agg"] or not is_finite(change): return
|
||||||
|
var old_multiplier: float = WorldParameters.effective(world.state, key)
|
||||||
|
if not world.state.has("world_parameters"): world.state.world_parameters = WorldParameters.defaults()
|
||||||
|
world.state.world_parameters[key] = clampf(WorldParameters.value(world.state, key) + change, 1.0, 19.0)
|
||||||
|
if key == "Bur":
|
||||||
|
var ratio: float = old_multiplier / WorldParameters.effective(world.state, key)
|
||||||
|
var now: float = world.state.time.year
|
||||||
|
for id in world.state.batches:
|
||||||
|
if not world.catalog.entities.has(id) or world.catalog.entities[id].group_as != "Star": continue
|
||||||
|
for batch: Dictionary in world.state.batches[id]:
|
||||||
|
# Only future survival time changes. Birth dates and the fractional
|
||||||
|
# withdrawal carry remain intact; already elapsed time is not replayed.
|
||||||
|
for field in ["start_year", "end_year", "processed_until"]:
|
||||||
|
if batch.has(field): batch[field] = now + maxf(0.0, batch[field] - now) * ratio
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cimalpe85ofel
|
||||||
Reference in New Issue
Block a user