Refactor Owniverse modules for quantity mathematics and entity catalog
- Introduced `quantity_math.gd` for arithmetic operations including allocation, normalization, and accumulation of quantities without scene tree dependencies. - Created `entity_catalog.gd` to manage entity definitions, groups, and event indexes, ensuring separation from simulation state and UI. - Implemented `item_presentation.gd` for read-only projections of simulation state, handling amounts and unlock conditions for UI elements. - Updated `civilization_evolution.gd` and `civilization_state.gd` to utilize new quantity math functions, improving code clarity and maintainability. - Enhanced tests in `test_quantity_catalog.gd` to validate new functionality and ensure correctness of mathematical operations. - Revised documentation to reflect changes in module structure and functionality.
This commit is contained in:
+3
-3
@@ -66,15 +66,15 @@ func format_number(value: float, digits: int = 0) -> String:
|
|||||||
var fixed: String = "%.*f" % [digits, value]
|
var fixed: String = "%.*f" % [digits, value]
|
||||||
var parts: PackedStringArray = fixed.split(".", true)
|
var parts: PackedStringArray = fixed.split(".", true)
|
||||||
var integer: String = parts[0]
|
var integer: String = parts[0]
|
||||||
var sign: String = ""
|
var sign_prefix: String = ""
|
||||||
if integer.begins_with("-") or integer.begins_with("+"):
|
if integer.begins_with("-") or integer.begins_with("+"):
|
||||||
sign = integer.left(1)
|
sign_prefix = integer.left(1)
|
||||||
integer = integer.substr(1)
|
integer = integer.substr(1)
|
||||||
var grouped: String = ""
|
var grouped: String = ""
|
||||||
while integer.length() > 3:
|
while integer.length() > 3:
|
||||||
grouped = " " + integer.right(3) + grouped
|
grouped = " " + integer.right(3) + grouped
|
||||||
integer = integer.left(integer.length() - 3)
|
integer = integer.left(integer.length() - 3)
|
||||||
grouped = sign + integer + grouped
|
grouped = sign_prefix + integer + grouped
|
||||||
return grouped + ("." + parts[1] if parts.size() > 1 else "")
|
return grouped + ("." + parts[1] if parts.size() > 1 else "")
|
||||||
|
|
||||||
var exp_value = int(floor(log(abs(value)) / log(10.0)))
|
var exp_value = int(floor(log(abs(value)) / log(10.0)))
|
||||||
|
|||||||
+9
-38
@@ -116,25 +116,11 @@ func set_owniverse_item_value() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _unlock_items():
|
func _unlock_items():
|
||||||
if locked_items.size() == 0: return
|
var result: Dictionary = owniverse.presentation.unlocks(owniverse.state, owniverse.catalog, locked_items)
|
||||||
var _new_locked_items = []
|
for item_id in result.visible:
|
||||||
var display_totals: Dictionary = owniverse.get_item_amounts(true)
|
if owniverse_items.has(item_id):
|
||||||
for _item_id in locked_items:
|
owniverse_items[item_id].set_visible(true)
|
||||||
#print(_item_id ' | ', _entity.unlock)
|
locked_items = result.remaining
|
||||||
var _flag_to_unlock = true
|
|
||||||
for _elem in owniverse.entities[_item_id].unlock:
|
|
||||||
if maxf(owniverse.state.stats.total_amount.get(_elem, 0.0), display_totals.get(_elem, 0.0)) < owniverse.entities[_item_id].unlock[_elem]:
|
|
||||||
_flag_to_unlock = false
|
|
||||||
break
|
|
||||||
|
|
||||||
if _flag_to_unlock:
|
|
||||||
for item_id in [_item_id, owniverse.entities[_item_id].count_as]:
|
|
||||||
if owniverse_items.has(item_id):
|
|
||||||
owniverse_items[item_id].set_visible(true)
|
|
||||||
else:
|
|
||||||
_new_locked_items.append(_item_id)
|
|
||||||
locked_items = _new_locked_items
|
|
||||||
|
|
||||||
|
|
||||||
func _on_indicator_pressed(indicator_name: String, page: int = 0):
|
func _on_indicator_pressed(indicator_name: String, page: int = 0):
|
||||||
_set_description(indicator_name, page)
|
_set_description(indicator_name, page)
|
||||||
@@ -228,7 +214,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 = 10 * _value if _value > 0 else 1
|
_value = 9 * _value if _value > 0 else 1
|
||||||
owniverse.produce_hs(item.item_id, _value)
|
owniverse.produce_hs(item.item_id, _value)
|
||||||
|
|
||||||
|
|
||||||
@@ -504,9 +490,9 @@ func init_enitities() -> Dictionary:
|
|||||||
var _result := {
|
var _result := {
|
||||||
'locked_items': [],
|
'locked_items': [],
|
||||||
}
|
}
|
||||||
for entity_id in Global.OwniverseEntities:
|
owniverse.catalog.load_definitions(Global.OwniverseEntities)
|
||||||
var entity = OwniverseEntity.new(Global.OwniverseEntities[entity_id])
|
for entity_id in owniverse.entities:
|
||||||
owniverse.entities[entity_id] = entity
|
var entity: OwniverseEntity = owniverse.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
|
||||||
@@ -519,23 +505,8 @@ func init_enitities() -> Dictionary:
|
|||||||
owniverse.state.stats.total_amount[entity.group_as] = 0
|
owniverse.state.stats.total_amount[entity.group_as] = 0
|
||||||
|
|
||||||
if entity.unlock != {}:
|
if entity.unlock != {}:
|
||||||
owniverse.locked_entities.append(entity)
|
|
||||||
_result.locked_items.append(entity.id)
|
_result.locked_items.append(entity.id)
|
||||||
|
|
||||||
if entity.product_per_year_type != "":
|
|
||||||
owniverse.auto_production.append(entity)
|
|
||||||
|
|
||||||
# groups
|
|
||||||
if entity.group_as != "":
|
|
||||||
if !owniverse.entity_groups.has(entity.group_as):
|
|
||||||
owniverse.entity_groups[entity.group_as] = []
|
|
||||||
owniverse.entity_groups[entity.group_as].append(entity_id)
|
|
||||||
|
|
||||||
if entity.event_group != "":
|
|
||||||
if !owniverse.entity_event_groups.has(entity.event_group):
|
|
||||||
owniverse.entity_event_groups[entity.event_group] = []
|
|
||||||
owniverse.entity_event_groups[entity.event_group].append(entity_id)
|
|
||||||
|
|
||||||
return _result
|
return _result
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
# Проверки поэтапного переноса из Owniverse.U
|
# Проверки поэтапного переноса из Owniverse.U
|
||||||
|
|
||||||
|
## Математика распределений и каталог
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_quantity_catalog.gd`
|
||||||
|
|
||||||
|
Проверяются целочисленность и границы распределения, сохранение суммы, разрешение равных остатков, накопление дробей и долгов округления, нормализация весов, размер производственной партии, загрузка каталога и его индексы. Архитектура описана в `../world/owniverse/README.md`. Сохранение игрока не используется.
|
||||||
|
|
||||||
## Иконки стадий жизни
|
## Иконки стадий жизни
|
||||||
|
|
||||||
|
Тест также проверяет модуль `item_presentation.gd`: отсутствие изменений входного состояния и списка закрытых иконок, решения по разблокировке, отложенную и принудительную публикацию количеств.
|
||||||
|
|
||||||
`godot --headless --path Owniverse.GI --script res://tests/test_life_icons.gd`
|
`godot --headless --path Owniverse.GI --script res://tests/test_life_icons.gd`
|
||||||
|
|
||||||
Проверяются одновременная разблокировка подробных и общих иконок EON/LP и C0.0/C0, количества свободных и связанных планет без двойного учёта, переключение подпанелей и очистка счётчика при исчезновении стадии. Сохранение игрока не используется.
|
Проверяются одновременная разблокировка подробных и общих иконок EON/LP и C0.0/C0, количества свободных и связанных планет без двойного учёта, переключение подпанелей и очистка счётчика при исчезновении стадии. Сохранение игрока не используется.
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ func run() -> void:
|
|||||||
manager.save_completed.connect(func(filename, success, error): completions.append([filename, success, error]))
|
manager.save_completed.connect(func(filename, success, error): completions.append([filename, success, error]))
|
||||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
var data: Dictionary = {"state": world.state, "ui": {"current_page": 1}}
|
var data: Dictionary = {"state": world.state, "ui": {"current_page": 1}}
|
||||||
|
var initial_energy: float = world.state.energy
|
||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
var old_plaintext: bool = global.save_plaintext_copy
|
var old_plaintext: bool = global.save_plaintext_copy
|
||||||
global.save_plaintext_copy = true
|
global.save_plaintext_copy = true
|
||||||
@@ -35,9 +36,9 @@ func run() -> void:
|
|||||||
await process_frame
|
await process_frame
|
||||||
check(not manager.is_saving() and frames > 0, "Frames continue while save is pending")
|
check(not manager.is_saving() and frames > 0, "Frames continue while save is pending")
|
||||||
var saved: Dictionary = manager.load_data("snapshot", "test-password")
|
var saved: Dictionary = manager.load_data("snapshot", "test-password")
|
||||||
check(saved.state.energy == 1e14 and saved.ui.current_page == 1, "Worker owns a deep snapshot")
|
check(saved.state.energy == initial_energy and saved.ui.current_page == 1, "Worker owns a deep snapshot")
|
||||||
var plain: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("snapshot.adventure.json")))
|
var plain: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("snapshot.adventure.json")))
|
||||||
check(plain.state.energy == 1e14, "Diagnostic JSON uses same snapshot")
|
check(plain.state.energy == initial_energy, "Diagnostic JSON uses same snapshot")
|
||||||
check(completions.size() == 1 and completions[0][1], "Completion reports successful write")
|
check(completions.size() == 1 and completions[0][1], "Completion reports successful write")
|
||||||
|
|
||||||
completions.clear()
|
completions.clear()
|
||||||
|
|||||||
@@ -33,6 +33,23 @@ func run() -> void:
|
|||||||
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.get_item_amounts(true).EON1 == 12.0 and world.get_item_amounts(true).LP == 16.0, "Historical counts retain individual and aggregate values")
|
||||||
check(world.state == before, "Presentation does not modify simulation state")
|
check(world.state == before, "Presentation does not modify simulation state")
|
||||||
|
var pending: Array = ["EON1", "EON2", "EON3", "C0.0"]
|
||||||
|
var pending_before: Array = pending.duplicate()
|
||||||
|
var decision: Dictionary = world.presentation.unlocks(world.state, world.catalog, pending)
|
||||||
|
check(decision.remaining == ["EON3"], "Only unmet requirements remain pending")
|
||||||
|
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
|
||||||
|
check(decision.visible.has(id), "Unlock projection includes detail and alias: " + id)
|
||||||
|
check(pending == pending_before and world.state == before, "Unlock evaluation leaves inputs untouched")
|
||||||
|
var snapshot: Dictionary = world.items_to_update.duplicate()
|
||||||
|
world.defer_item_updates = true
|
||||||
|
world.state.amount.EON1 += 1.0
|
||||||
|
world.refresh_item_amounts()
|
||||||
|
check(world.item_amounts_dirty and world.items_to_update == snapshot, "Deferred refresh retains pending snapshot")
|
||||||
|
world.refresh_item_amounts(true)
|
||||||
|
check(not world.item_amounts_dirty and world.items_to_update.EON1 == 13.0, "Forced refresh publishes latest amounts")
|
||||||
|
world.state.amount.EON1 -= 1.0
|
||||||
|
world.defer_item_updates = false
|
||||||
|
world.refresh_item_amounts()
|
||||||
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)
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const Math = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(condition: bool, message: String) -> void:
|
||||||
|
checks += 1
|
||||||
|
if not condition:
|
||||||
|
failures += 1
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var Catalog = load("res://world/owniverse/entity_catalog.gd")
|
||||||
|
check(Math.allocate_whole({}, 10.0) == {}, "Empty allocation")
|
||||||
|
check(Math.allocate_whole({"b": 1.0, "a": 1.0}, 1.0) == {"a": 1.0, "b": 0.0}, "Stable lexical tie")
|
||||||
|
for limit in range(30):
|
||||||
|
var requests: Dictionary = {"a": 3.9, "b": 9.1, "c": -2.0, "d": 7.0}
|
||||||
|
var result: Dictionary = Math.allocate_whole(requests, limit)
|
||||||
|
check(Math.sum(result) == minf(limit, 19.0), "Conserve bounded allocation")
|
||||||
|
for id in result:
|
||||||
|
check(result[id] == floorf(result[id]) and result[id] >= 0.0 and result[id] <= floorf(maxf(0.0, requests[id])), "Whole bounded branch")
|
||||||
|
check(Math.normalize_distribution({"a": -1.0, "b": 0.0}) == {}, "Nonpositive weights")
|
||||||
|
check(Math.normalize_distribution({"a": 2.0, "b": 6.0}) == {"a": 0.25, "b": 0.75}, "Normalize weights")
|
||||||
|
check(Math.production_step([0.25, 0.1, 2.0]) == 20.0, "Common whole production step")
|
||||||
|
check(Math.production_step([]) == 1.0, "Free production step")
|
||||||
|
var target: Dictionary = {}
|
||||||
|
var produced: float = 0.0
|
||||||
|
for i in 10:
|
||||||
|
produced += Math.accumulate_whole(target, "remainders", "a", 0.1)
|
||||||
|
check(produced == 1.0, "Fractional production carries")
|
||||||
|
var remainders: Dictionary = {}
|
||||||
|
var totals: Dictionary = {"a": 0.0, "b": 0.0, "c": 0.0}
|
||||||
|
for i in 30:
|
||||||
|
var result: Dictionary = Math.split(remainders, "split", 1.0, {"a": 1.0, "b": 1.0, "c": 1.0})
|
||||||
|
check(Math.sum(result) == 1.0, "Split conserves count")
|
||||||
|
for id in result: totals[id] += result[id]
|
||||||
|
check(totals == {"a": 10.0, "b": 10.0, "c": 10.0}, "Rounding debt distributes over time")
|
||||||
|
produced = 0.0
|
||||||
|
for i in 10: produced += Math.accumulate_rate(remainders, "rate", 1.0, 0.1)
|
||||||
|
check(produced == 1.0, "Rate carries")
|
||||||
|
var catalog = Catalog.new()
|
||||||
|
var rows: Dictionary = root.get_node("Global").OwniverseEntities
|
||||||
|
catalog.load_definitions(rows)
|
||||||
|
check(catalog.entities.size() == rows.size(), "Catalog loads all definitions")
|
||||||
|
for id in catalog.entities:
|
||||||
|
var entity = catalog.entities[id]
|
||||||
|
check(catalog.automatic_producers.has(entity) == (entity.product_per_year_type != ""), "Producer index")
|
||||||
|
check(catalog.unlockable_entities.has(entity) == (not entity.unlock.is_empty()), "Unlock index")
|
||||||
|
if entity.group_as != "":
|
||||||
|
check(catalog.groups[entity.group_as].has(id) and catalog.members(entity.group_as).has(id), "Group index")
|
||||||
|
if entity.event_group != "":
|
||||||
|
check(catalog.event_groups[entity.event_group].has(id), "Event index")
|
||||||
|
var members: Array = catalog.members("Star")
|
||||||
|
members.clear()
|
||||||
|
check(not catalog.members("Star").is_empty(), "Group results are independent")
|
||||||
|
check(Catalog.new().entities.is_empty(), "Catalog instances are independent")
|
||||||
|
for entity in catalog.entities.values(): entity.free()
|
||||||
|
print("Quantity and catalog: %d checks, %d failures" % [checks, failures])
|
||||||
|
quit(1 if failures else 0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bvri1enpsjisk
|
||||||
+2
-1
@@ -70,7 +70,8 @@ func run() -> void:
|
|||||||
world.evolute(0.005)
|
world.evolute(0.005)
|
||||||
check(world.state.time.year == 0.1, "Speed change preserves accumulated fraction")
|
check(world.state.time.year == 0.1, "Speed change preserves accumulated fraction")
|
||||||
world.speed = 100
|
world.speed = 100
|
||||||
check(world.speed == 5 and world.state.speed_in_year == 10000.0, "Upper speed limit unchanged")
|
var speed_limit: int = root.get_node("Global").SPEED_LIMIT
|
||||||
|
check(world.speed == speed_limit and world.state.speed_in_year == pow(10.0, speed_limit - 1), "Configured upper speed limit")
|
||||||
world.speed = -10
|
world.speed = -10
|
||||||
check(world.speed == 1 and world.state.speed_in_year == 1.0, "Lower speed limit unchanged")
|
check(world.speed == 1 and world.state.speed_in_year == 1.0, "Lower speed limit unchanged")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Модули Owniverse
|
||||||
|
|
||||||
|
Первый этап разделения Owniverse выделяет два модуля:
|
||||||
|
|
||||||
|
- `quantity_math.gd`: нормализация положительных весов, ограниченное целочисленное распределение методом наибольших остатков, распределение с накоплением ошибки округления, накопление дробного выпуска и минимальный целый размер производственной партии.
|
||||||
|
- `entity_catalog.gd`: загрузка определений, индексы групп и событий, списки автоматических производителей и разблокируемых сущностей.
|
||||||
|
|
||||||
|
Математика не обращается к дереву сцены, Global или Owniverse. Методы накопления изменяют переданный словарь остатков; остальные методы возвращают результат. Для распределения `split` количество должно быть целым, веса — неотрицательными. Равные остатки разрешаются по строковому ID. Сохранены прежние точности округления: 1e-12 для выпуска и 1e-10 для событий цивилизаций.
|
||||||
|
|
||||||
|
Каталог получает словарь исходных строк через `load_definitions(rows)`, вызываемый один раз для нового экземпляра. Конструктор OwniverseEntity по-прежнему использует настройки Global. Каталог не хранит количества, партии или состояние интерфейса. Определения пока остаются Node: освобождает их владелец мира, как до рефакторинга.
|
||||||
|
|
||||||
|
Метод `members(id)` возвращает копию списка. После изменения group_as нужно вызвать `rebuild_group_index()`; добавление или удаление определения автоматически обнаруживается по изменению размера словаря. Полная перезагрузка определений выполняется созданием нового каталога.
|
||||||
|
|
||||||
|
Owniverse сохраняет совместимые свойства entities/entity_groups/entity_event_groups и методы-обёртки. Они используют те же данные каталога, без дублирования. Stage загружает каталог, затем отдельно инициализирует количество, статистику и UI. Эволюция цивилизаций обращается непосредственно к QuantityMath.
|
||||||
|
|
||||||
|
Формулы, порядок обхода определений, формат сохранения и правила симуляции не изменены. Проверки модулей: `tests/test_quantity_catalog.gd`; интеграционные проверки перечислены в `tests/README.md`.
|
||||||
|
|
||||||
|
## Представление количеств и разблокировка иконок
|
||||||
|
|
||||||
|
`item_presentation.gd` получает состояние и каталог, не изменяет симуляцию и не обращается к UI или Global.
|
||||||
|
|
||||||
|
- `amounts(state, catalog, total_produced)` строит текущие или исторические количества. Текущие планеты включают свободные и связанные объекты. Подробные стадии сохраняют собственные значения одновременно с суммой count_as; готовые групповые счётчики повторно не суммируются.
|
||||||
|
- `unlocks(state, catalog, locked)` проверяет условия по историческим количествам и их отображаемым суммам. Возвращает remaining (ещё закрытые сущности) и visible (открываемые сущности и их count_as). Переданный список не изменяется. Уже открытые иконки повторно не блокируются.
|
||||||
|
- `refresh` управляет снимком pending_amounts и флагом dirty, учитывает отложенное обновление и принудительную публикацию. Сам модуль не запускает таймеры и не меняет частоту обновления.
|
||||||
|
|
||||||
|
Owniverse делегирует существующие get_item_amounts/refresh_item_amounts и свойства обновления экземпляру presentation. Stage применяет результат разблокировки к контролам и хранит список закрытых иконок. Снимки и флаги принадлежат отдельному миру и не включаются в сохранение.
|
||||||
@@ -1,57 +1,33 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
|
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
|
||||||
const State = preload("res://world/owniverse/civilization_state.gd")
|
const State = preload("res://world/owniverse/civilization_state.gd")
|
||||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
const ETHICS = State.ETHICS
|
const ETHICS = State.ETHICS
|
||||||
const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"}
|
const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"}
|
||||||
|
|
||||||
static func total(pop: Dictionary) -> float:
|
static func total(pop: Dictionary) -> float:
|
||||||
var result: float = 0.0
|
return QuantityMath.sum(pop)
|
||||||
for n in pop.values(): result += n
|
|
||||||
return result
|
|
||||||
|
|
||||||
static func add(to: Dictionary, from: Dictionary) -> void:
|
static func add(to: Dictionary, from: Dictionary) -> void:
|
||||||
for key in from: to[key] = to.get(key, 0.0) + from[key]
|
for key in from: to[key] = to.get(key, 0.0) + from[key]
|
||||||
|
|
||||||
static func take(world, pop: Dictionary, count: float) -> Dictionary:
|
static func take(world, pop: Dictionary, count: float) -> Dictionary:
|
||||||
var taken: Dictionary = world._allocate_whole(pop, count)
|
var taken: Dictionary = QuantityMath.allocate_whole(pop, count)
|
||||||
for key in taken: pop[key] -= taken[key]
|
for key in taken: pop[key] -= taken[key]
|
||||||
return taken
|
return taken
|
||||||
|
|
||||||
static func rate(target: Dictionary, key: String, count: float, probability: float) -> float:
|
static func rate(target: Dictionary, key: String, count: float, probability: float) -> float:
|
||||||
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
||||||
var expected: float = count * maxf(0.0, probability) + target.civilization_remainders.get(key, 0.0)
|
return QuantityMath.accumulate_rate(target.civilization_remainders, key, count, probability)
|
||||||
var actual: float = floorf(snappedf(expected, 1e-10))
|
|
||||||
target.civilization_remainders[key] = maxf(0.0, expected - actual)
|
|
||||||
return actual
|
|
||||||
|
|
||||||
# Fixed-size largest-remainder apportionment with saved signed rounding errors.
|
# Fixed-size largest-remainder apportionment with saved signed rounding errors.
|
||||||
# Runtime depends on the number of branches, never the number of civilizations.
|
# Runtime depends on the number of branches, never the number of civilizations.
|
||||||
static func split(target: Dictionary, key: String, count: float, weights: Dictionary) -> Dictionary:
|
static func split(target: Dictionary, key: String, count: float, weights: Dictionary) -> Dictionary:
|
||||||
var result: Dictionary = {}
|
if count <= 0.0 or QuantityMath.sum(weights) <= 0.0: return {}
|
||||||
var sum_weights: float = total(weights)
|
|
||||||
if count <= 0.0 or sum_weights <= 0.0: return result
|
|
||||||
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
||||||
var expected: Dictionary = {}
|
return QuantityMath.split(target.civilization_remainders, key, count, weights)
|
||||||
var positive_sum: float = 0.0
|
|
||||||
for id in weights:
|
|
||||||
expected[id] = count * weights[id] / sum_weights + target.civilization_remainders.get(key + ":" + id, 0.0)
|
|
||||||
positive_sum += maxf(0.0, expected[id])
|
|
||||||
var order: Array = []
|
|
||||||
var assigned: float = 0.0
|
|
||||||
for id in weights:
|
|
||||||
var normalized: float = maxf(0.0, expected[id]) * count / positive_sum if positive_sum > 0.0 else 0.0
|
|
||||||
result[id] = floorf(normalized)
|
|
||||||
assigned += result[id]
|
|
||||||
order.append({"id": id, "rest": normalized - result[id]})
|
|
||||||
order.sort_custom(func(a, b): return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
|
|
||||||
for entry in order:
|
|
||||||
if assigned >= count: break
|
|
||||||
result[entry.id] += 1.0
|
|
||||||
assigned += 1.0
|
|
||||||
for id in weights:
|
|
||||||
target.civilization_remainders[key + ":" + id] = clampf(expected[id] - result[id], -1.0, 1.0)
|
|
||||||
return result
|
|
||||||
|
|
||||||
static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictionary,
|
static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictionary,
|
||||||
diplomacy: bool = false, colony: bool = false) -> Dictionary:
|
diplomacy: bool = false, colony: bool = false) -> Dictionary:
|
||||||
@@ -132,7 +108,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
|||||||
var scale: float = maxf(1.0, p + d)
|
var scale: float = maxf(1.0, p + d)
|
||||||
var pro: float = rate(target, id + ":" + ethic + ":pro", triggered[ethic], p / scale)
|
var pro: float = rate(target, id + ":" + ethic + ":pro", triggered[ethic], p / scale)
|
||||||
var deg: float = rate(target, id + ":" + ethic + ":deg", triggered[ethic], d / scale)
|
var deg: float = rate(target, id + ":" + ethic + ":deg", triggered[ethic], d / scale)
|
||||||
var bounded: Dictionary = world._allocate_whole({"pro": pro, "deg": deg}, triggered[ethic])
|
var bounded: Dictionary = QuantityMath.allocate_whole({"pro": pro, "deg": deg}, triggered[ethic])
|
||||||
if rules[id].fights: warriors[ethic] = rate(target, id + ":" + ethic + ":war", triggered[ethic], minf(1.0, float(row.war) * modifiers.war))
|
if rules[id].fights: warriors[ethic] = rate(target, id + ":" + ethic + ":war", triggered[ethic], minf(1.0, float(row.war) * modifiers.war))
|
||||||
if rules[id].colonizes: settlers[ethic] = rate(target, id + ":" + ethic + ":col", triggered[ethic], minf(1.0, float(row.colonization) * modifiers.colonization))
|
if rules[id].colonizes: settlers[ethic] = rate(target, id + ":" + ethic + ":col", triggered[ethic], minf(1.0, float(row.colonization) * modifiers.colonization))
|
||||||
successes[ethic] = bounded.pro
|
successes[ethic] = bounded.pro
|
||||||
@@ -143,7 +119,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
|||||||
if rules[id].fights:
|
if rules[id].fights:
|
||||||
for ethic in warriors: warriors[ethic] = minf(warriors[ethic], triggered[ethic])
|
for ethic in warriors: warriors[ethic] = minf(warriors[ethic], triggered[ethic])
|
||||||
var wars: float = minf(total(warriors), floorf((total(triggered) + total(remaining[id])) / 2.0))
|
var wars: float = minf(total(warriors), floorf((total(triggered) + total(remaining[id])) / 2.0))
|
||||||
warriors = world._allocate_whole(warriors, wars)
|
warriors = QuantityMath.allocate_whole(warriors, wars)
|
||||||
for ethic in warriors: triggered[ethic] -= warriors[ethic]
|
for ethic in warriors: triggered[ethic] -= warriors[ethic]
|
||||||
var outcomes: Dictionary = split(target, id + ":wars", wars, {"deg": 1.0, "pro25": 2.0, "pro50": 3.0, "pro75": 3.0})
|
var outcomes: Dictionary = split(target, id + ":wars", wars, {"deg": 1.0, "pro25": 2.0, "pro50": 3.0, "pro75": 3.0})
|
||||||
var win_expected: float = outcomes.get("pro25", 0.0) * 0.25 + outcomes.get("pro50", 0.0) * 0.5 + outcomes.get("pro75", 0.0) * 0.75
|
var win_expected: float = outcomes.get("pro25", 0.0) * 0.25 + outcomes.get("pro50", 0.0) * 0.5 + outcomes.get("pro75", 0.0) * 0.75
|
||||||
@@ -153,7 +129,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
|||||||
add(triggered, progress(world, target, rules, budget, plan, winners))
|
add(triggered, progress(world, target, rules, budget, plan, winners))
|
||||||
fall(target, plan, rules, attackers_lost)
|
fall(target, plan, rules, attackers_lost)
|
||||||
var losses: float = minf(rate(target, id + ":loss", loss_expected / order, 1.0), total(triggered) + total(remaining[id]))
|
var losses: float = minf(rate(target, id + ":loss", loss_expected / order, 1.0), total(triggered) + total(remaining[id]))
|
||||||
var sides: Dictionary = world._allocate_whole({"due": total(triggered), "later": total(remaining[id])}, losses)
|
var sides: Dictionary = QuantityMath.allocate_whole({"due": total(triggered), "later": total(remaining[id])}, losses)
|
||||||
var victims: Dictionary = take(world, triggered, sides.due)
|
var victims: Dictionary = take(world, triggered, sides.due)
|
||||||
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
|
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
|
||||||
fall(target, plan, rules, victims)
|
fall(target, plan, rules, victims)
|
||||||
@@ -170,7 +146,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
|||||||
var plan: Dictionary = plans[id]
|
var plan: Dictionary = plans[id]
|
||||||
var retry: Dictionary = plan.outputs.get(id, {})
|
var retry: Dictionary = plan.outputs.get(id, {})
|
||||||
var amount: float = minf(collateral[id], total(retry) + total(remaining[id]))
|
var amount: float = minf(collateral[id], total(retry) + total(remaining[id]))
|
||||||
var sides: Dictionary = world._allocate_whole({"due": total(retry), "later": total(remaining[id])}, amount)
|
var sides: Dictionary = QuantityMath.allocate_whole({"due": total(retry), "later": total(remaining[id])}, amount)
|
||||||
var victims: Dictionary = take(world, retry, sides.due)
|
var victims: Dictionary = take(world, retry, sides.due)
|
||||||
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
|
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
|
||||||
fall(target, plan, rules, victims)
|
fall(target, plan, rules, victims)
|
||||||
@@ -223,19 +199,19 @@ static func colonize(world, target: Dictionary, colonies: Array, year: float, ru
|
|||||||
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 = {}
|
||||||
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
|
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
|
||||||
var granted: Dictionary = world._allocate_whole(requests, total(stock))
|
var granted: Dictionary = QuantityMath.allocate_whole(requests, total(stock))
|
||||||
for i in colonies.size():
|
for i in colonies.size():
|
||||||
var colony: Dictionary = colonies[i]
|
var colony: Dictionary = colonies[i]
|
||||||
var population: Dictionary = world._allocate_whole(colony.population, granted[str(i)])
|
var population: Dictionary = QuantityMath.allocate_whole(colony.population, granted[str(i)])
|
||||||
population = drift(target, colony.source + ":colonies", population, rules[colony.source].ethics, false, true)
|
population = drift(target, colony.source + ":colonies", population, rules[colony.source].ethics, false, true)
|
||||||
var count: float = total(population)
|
var count: float = total(population)
|
||||||
var targets: Dictionary = world._allocate_whole(stock, count)
|
var targets: Dictionary = QuantityMath.allocate_whole(stock, count)
|
||||||
for id in targets:
|
for id in targets:
|
||||||
var amount: float = targets[id]
|
var amount: float = targets[id]
|
||||||
if amount <= 0.0: continue
|
if amount <= 0.0: continue
|
||||||
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 = world._allocate_whole(slots, amount)
|
var taken: Dictionary = QuantityMath.allocate_whole(slots, amount)
|
||||||
world._scale_batches(target, id, 1.0 - amount / stock[id])
|
world._scale_batches(target, id, 1.0 - amount / stock[id])
|
||||||
stock[id] -= amount
|
stock[id] -= amount
|
||||||
for owner in taken:
|
for owner in taken:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
|
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
|
||||||
const ETHICS = ["m", "l", "w", "d", "c", "b"]
|
const ETHICS = ["m", "l", "w", "d", "c", "b"]
|
||||||
|
|
||||||
static func is_space_civilization(id: String, rules: Dictionary) -> bool:
|
static func is_space_civilization(id: String, rules: Dictionary) -> bool:
|
||||||
@@ -19,7 +21,7 @@ static func reconcile(world, target: Dictionary, rules: Dictionary) -> void:
|
|||||||
current[ethic] = maxf(0.0, current.get(ethic, 0.0))
|
current[ethic] = maxf(0.0, current.get(ethic, 0.0))
|
||||||
total += current[ethic]
|
total += current[ethic]
|
||||||
if total > count:
|
if total > count:
|
||||||
current = world._allocate_whole(current, count)
|
current = QuantityMath.allocate_whole(current, count)
|
||||||
elif total < count:
|
elif total < count:
|
||||||
var missing: float = count - total
|
var missing: float = count - total
|
||||||
var base: float = floorf(missing / ETHICS.size())
|
var base: float = floorf(missing / ETHICS.size())
|
||||||
@@ -131,7 +133,7 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
|||||||
target.amount[id] = supported
|
target.amount[id] = supported
|
||||||
world._credit(target, previous, excess)
|
world._credit(target, previous, excess)
|
||||||
world._append_batch(target, previous, excess, year, year)
|
world._append_batch(target, previous, excess, year, year)
|
||||||
var removed: Dictionary = world._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] = {}
|
||||||
for ethic in ETHICS:
|
for ethic in ETHICS:
|
||||||
@@ -143,7 +145,7 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
|||||||
for star in physical:
|
for star in physical:
|
||||||
if world.entities[star].event_group == "DwStar" and physical[star] > 0.0:
|
if world.entities[star].event_group == "DwStar" and physical[star] > 0.0:
|
||||||
stars[star] = physical[star]
|
stars[star] = physical[star]
|
||||||
var weights: Dictionary = world._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.
|
||||||
var assigned: float = 0.0
|
var assigned: float = 0.0
|
||||||
var owners: Array = weights.keys()
|
var owners: Array = weights.keys()
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Definitions and their indexes only: no population, batches or UI state.
|
||||||
|
## Definitions are Nodes owned by the caller, as before this extraction.
|
||||||
|
|
||||||
|
var entities: Dictionary[String, OwniverseEntity] = {}
|
||||||
|
var groups: Dictionary[String, Array] = {}
|
||||||
|
var event_groups: Dictionary[String, Array] = {}
|
||||||
|
var automatic_producers: Array = []
|
||||||
|
var unlockable_entities: Array = []
|
||||||
|
var _group_members: Dictionary = {}
|
||||||
|
var _indexed_entity_count: int = -1
|
||||||
|
|
||||||
|
## Populate an empty catalog; replacement uses a new catalog to preserve ownership.
|
||||||
|
func load_definitions(rows: Dictionary) -> void:
|
||||||
|
assert(entities.is_empty(), "Load definitions into an empty catalog")
|
||||||
|
for id in rows:
|
||||||
|
var entity := OwniverseEntity.new(rows[id])
|
||||||
|
entities[id] = entity
|
||||||
|
if not entity.unlock.is_empty(): unlockable_entities.append(entity)
|
||||||
|
if entity.product_per_year_type != "": automatic_producers.append(entity)
|
||||||
|
if entity.group_as != "":
|
||||||
|
if not groups.has(entity.group_as): groups[entity.group_as] = []
|
||||||
|
groups[entity.group_as].append(id)
|
||||||
|
if entity.event_group != "":
|
||||||
|
if not event_groups.has(entity.event_group): event_groups[entity.event_group] = []
|
||||||
|
event_groups[entity.event_group].append(id)
|
||||||
|
rebuild_group_index()
|
||||||
|
|
||||||
|
## Call explicitly after modifying group_as without changing the entity count.
|
||||||
|
func rebuild_group_index() -> void:
|
||||||
|
_group_members.clear()
|
||||||
|
for id in entities:
|
||||||
|
var group: String = entities[id].group_as
|
||||||
|
if not _group_members.has(group): _group_members[group] = []
|
||||||
|
_group_members[group].append(id)
|
||||||
|
_indexed_entity_count = entities.size()
|
||||||
|
|
||||||
|
func _ensure_group_index() -> void:
|
||||||
|
if _indexed_entity_count != entities.size(): rebuild_group_index()
|
||||||
|
|
||||||
|
func is_group(id: String) -> bool:
|
||||||
|
_ensure_group_index()
|
||||||
|
return groups.has(id) or _group_members.has(id)
|
||||||
|
|
||||||
|
func members(id: String) -> Array:
|
||||||
|
if not is_group(id): return [id]
|
||||||
|
return _group_members.get(id, []).duplicate()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://6g3m8wmm0ms7
|
||||||
@@ -21,7 +21,7 @@ static func matches(world, id: String) -> Array:
|
|||||||
for key in world.entities:
|
for key in world.entities:
|
||||||
var entity = world.entities[key]
|
var entity = world.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._is_group(key):
|
if not world.catalog.is_group(key):
|
||||||
result.append(key)
|
result.append(key)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Read-only projections of simulation state. No controls or simulation mutations.
|
||||||
|
|
||||||
|
var pending_amounts: Dictionary[String, float] = {}
|
||||||
|
var deferred_updates: bool = false
|
||||||
|
var dirty: bool = true
|
||||||
|
|
||||||
|
func refresh(state: Dictionary, catalog, force: bool = false) -> void:
|
||||||
|
dirty = true
|
||||||
|
if deferred_updates and not force:
|
||||||
|
return
|
||||||
|
pending_amounts = amounts(state, catalog)
|
||||||
|
dirty = false
|
||||||
|
|
||||||
|
## Aliases are presentation-only; group counters are not counted twice.
|
||||||
|
static func amounts(state: Dictionary, catalog, total_produced: bool = false) -> Dictionary[String, float]:
|
||||||
|
var source: Dictionary = state.stats.total_amount if total_produced else state.amount
|
||||||
|
if not total_produced:
|
||||||
|
source = source.duplicate()
|
||||||
|
var physical: Dictionary = {}
|
||||||
|
for id in catalog.entities:
|
||||||
|
if not catalog.is_group(id):
|
||||||
|
physical[id] = maxf(0.0, state.amount.get(id, 0.0))
|
||||||
|
for bindings: Dictionary in state.binds.values():
|
||||||
|
for id in bindings:
|
||||||
|
if physical.has(id):
|
||||||
|
physical[id] += maxf(0.0, bindings[id])
|
||||||
|
var planets: float = 0.0
|
||||||
|
for id in physical:
|
||||||
|
if catalog.entities[id].group_as == "Planet":
|
||||||
|
source[id] = physical[id]
|
||||||
|
planets += physical[id]
|
||||||
|
source["Planet"] = planets
|
||||||
|
var result: Dictionary[String, float] = {}
|
||||||
|
for id in source:
|
||||||
|
result[id] = 0.0
|
||||||
|
for id in source:
|
||||||
|
# group_as counters are already sums and must not be counted a second time.
|
||||||
|
if catalog.is_group(id):
|
||||||
|
continue
|
||||||
|
var item_id: String = id
|
||||||
|
if catalog.entities.has(id) and catalog.entities[id].count_as != "":
|
||||||
|
item_id = catalog.entities[id].count_as
|
||||||
|
# Detailed icons need their own count as well as the summary alias.
|
||||||
|
# Aggregate only from source, never from these presentation entries.
|
||||||
|
if item_id != id:
|
||||||
|
result[id] = result.get(id, 0.0) + float(source[id])
|
||||||
|
result[item_id] = result.get(item_id, 0.0) + float(source[id])
|
||||||
|
for id in source:
|
||||||
|
if catalog.is_group(id):
|
||||||
|
result[id] = float(source[id])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
## Evaluate pending icons against historical counts, returning UI decisions only.
|
||||||
|
static func unlocks(state: Dictionary, catalog, locked: Array) -> Dictionary:
|
||||||
|
var remaining: Array = []
|
||||||
|
var visible: Array = []
|
||||||
|
if locked.is_empty():
|
||||||
|
return {"remaining": remaining, "visible": visible}
|
||||||
|
var display_totals: Dictionary = amounts(state, catalog, true)
|
||||||
|
for id in locked:
|
||||||
|
var entity = catalog.entities[id]
|
||||||
|
var eligible: bool = true
|
||||||
|
for requirement in entity.unlock:
|
||||||
|
if maxf(state.stats.total_amount.get(requirement, 0.0), display_totals.get(requirement, 0.0)) < entity.unlock[requirement]:
|
||||||
|
eligible = false
|
||||||
|
break
|
||||||
|
if eligible:
|
||||||
|
for item_id in [id, entity.count_as]:
|
||||||
|
if item_id != "" and not visible.has(item_id):
|
||||||
|
visible.append(item_id)
|
||||||
|
else:
|
||||||
|
remaining.append(id)
|
||||||
|
return {"remaining": remaining, "visible": visible}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cdivwab70aqfb
|
||||||
+35
-120
@@ -4,22 +4,36 @@ const CivilizationEvolution = preload("res://world/owniverse/civilization_evolut
|
|||||||
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
|
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
const EntityCatalog = preload("res://world/owniverse/entity_catalog.gd")
|
||||||
|
const ItemPresentation = preload("res://world/owniverse/item_presentation.gd")
|
||||||
|
|
||||||
#var state: State = State.new()
|
#var state: State = State.new()
|
||||||
|
|
||||||
var entities: Dictionary[String, OwniverseEntity] = {}
|
var catalog = EntityCatalog.new()
|
||||||
var entity_groups: Dictionary[String, Array] = {}
|
# Compatibility accessors keep existing callers and fixtures on the same dictionaries.
|
||||||
var entity_event_groups: Dictionary[String, Array] = {}
|
var entities: Dictionary[String, OwniverseEntity]:
|
||||||
|
get: return catalog.entities
|
||||||
|
var entity_groups: Dictionary[String, Array]:
|
||||||
|
get: return catalog.groups
|
||||||
|
var entity_event_groups: Dictionary[String, Array]:
|
||||||
|
get: return catalog.event_groups
|
||||||
|
|
||||||
var _group_members: Dictionary = {}
|
var presentation = ItemPresentation.new()
|
||||||
var _indexed_entity_count: int = -1
|
var items_to_update: Dictionary[String, float]:
|
||||||
|
get: return presentation.pending_amounts
|
||||||
|
set(value): presentation.pending_amounts = value
|
||||||
|
var defer_item_updates: bool:
|
||||||
|
get: return presentation.deferred_updates
|
||||||
|
set(value): presentation.deferred_updates = value
|
||||||
|
var item_amounts_dirty: bool:
|
||||||
|
get: return presentation.dirty
|
||||||
|
set(value): presentation.dirty = value
|
||||||
|
|
||||||
var items_to_update: Dictionary[String, float] = {}
|
var auto_production: Array:
|
||||||
var defer_item_updates: bool = false
|
get: return catalog.automatic_producers
|
||||||
var item_amounts_dirty: bool = true
|
var locked_entities: Array:
|
||||||
|
get: return catalog.unlockable_entities
|
||||||
var auto_production: Array = []
|
|
||||||
var locked_entities: Array = []
|
|
||||||
|
|
||||||
# ------------------------------------
|
# ------------------------------------
|
||||||
|
|
||||||
@@ -123,45 +137,12 @@ func _evolute_to_datetime(datetime: float) -> void:
|
|||||||
refresh_item_amounts()
|
refresh_item_amounts()
|
||||||
|
|
||||||
|
|
||||||
## Aggregate only for presentation; never write aliases back into simulation state.
|
|
||||||
func get_item_amounts(total_produced: bool = false) -> Dictionary[String, float]:
|
func get_item_amounts(total_produced: bool = false) -> Dictionary[String, float]:
|
||||||
var source: Dictionary = state.stats.total_amount if total_produced else state.amount
|
return ItemPresentation.amounts(state, catalog, total_produced)
|
||||||
if not total_produced:
|
|
||||||
source = source.duplicate()
|
|
||||||
var physical: Dictionary = _physical_amounts(state)
|
|
||||||
var planets: float = 0.0
|
|
||||||
for id in physical:
|
|
||||||
if entities[id].group_as == "Planet":
|
|
||||||
source[id] = physical[id]
|
|
||||||
planets += physical[id]
|
|
||||||
source["Planet"] = planets
|
|
||||||
var result: Dictionary[String, float] = {}
|
|
||||||
for id in source:
|
|
||||||
result[id] = 0.0
|
|
||||||
for id in source:
|
|
||||||
# group_as counters are already sums and must not be counted a second time.
|
|
||||||
if _is_group(id):
|
|
||||||
continue
|
|
||||||
var item_id: String = id
|
|
||||||
if entities.has(id) and entities[id].count_as != "":
|
|
||||||
item_id = entities[id].count_as
|
|
||||||
# Detailed icons need their own count as well as the summary alias.
|
|
||||||
# Aggregate only from source, never from these presentation entries.
|
|
||||||
if item_id != id:
|
|
||||||
result[id] = result.get(id, 0.0) + float(source[id])
|
|
||||||
result[item_id] = result.get(item_id, 0.0) + float(source[id])
|
|
||||||
for id in source:
|
|
||||||
if _is_group(id):
|
|
||||||
result[id] = float(source[id])
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
func refresh_item_amounts(force: bool = false) -> void:
|
func refresh_item_amounts(force: bool = false) -> void:
|
||||||
item_amounts_dirty = true
|
presentation.refresh(state, catalog, force)
|
||||||
if defer_item_updates and not force:
|
|
||||||
return
|
|
||||||
items_to_update = get_item_amounts()
|
|
||||||
item_amounts_dirty = false
|
|
||||||
|
|
||||||
|
|
||||||
func _add_time_delta(delta: float) -> Dictionary:
|
func _add_time_delta(delta: float) -> Dictionary:
|
||||||
@@ -191,23 +172,11 @@ func modify_world_parameter(key: String, change: float) -> void:
|
|||||||
## Rebuild after changing group_as or replacing definitions without changing their count.
|
## Rebuild after changing group_as or replacing definitions without changing their count.
|
||||||
## Adding/removing definitions is detected automatically during initialization.
|
## Adding/removing definitions is detected automatically during initialization.
|
||||||
func rebuild_group_index() -> void:
|
func rebuild_group_index() -> void:
|
||||||
_group_members.clear()
|
catalog.rebuild_group_index()
|
||||||
for id in entities:
|
|
||||||
var group: String = entities[id].group_as
|
|
||||||
if not _group_members.has(group):
|
|
||||||
_group_members[group] = []
|
|
||||||
_group_members[group].append(id)
|
|
||||||
_indexed_entity_count = entities.size()
|
|
||||||
|
|
||||||
|
|
||||||
func _ensure_group_index() -> void:
|
|
||||||
if _indexed_entity_count != entities.size():
|
|
||||||
rebuild_group_index()
|
|
||||||
|
|
||||||
|
|
||||||
func _is_group(id: String) -> bool:
|
func _is_group(id: String) -> bool:
|
||||||
_ensure_group_index()
|
return catalog.is_group(id)
|
||||||
return entity_groups.has(id) or _group_members.has(id)
|
|
||||||
|
|
||||||
|
|
||||||
func _physical_amounts(source: Dictionary) -> Dictionary:
|
func _physical_amounts(source: Dictionary) -> Dictionary:
|
||||||
@@ -223,10 +192,7 @@ func _physical_amounts(source: Dictionary) -> Dictionary:
|
|||||||
|
|
||||||
|
|
||||||
func _members(id: String) -> Array:
|
func _members(id: String) -> Array:
|
||||||
if not _is_group(id):
|
return catalog.members(id)
|
||||||
return [id]
|
|
||||||
# Callers may modify the returned array without changing the index.
|
|
||||||
return _group_members.get(id, []).duplicate()
|
|
||||||
|
|
||||||
|
|
||||||
func _available(source: Dictionary, id: String) -> float:
|
func _available(source: Dictionary, id: String) -> float:
|
||||||
@@ -607,12 +573,7 @@ func stellar_environment(source: Dictionary) -> Dictionary:
|
|||||||
|
|
||||||
|
|
||||||
func _accumulate_whole(target: Dictionary, field: String, key: String, value: float) -> float:
|
func _accumulate_whole(target: Dictionary, field: String, key: String, value: float) -> float:
|
||||||
if not target.has(field):
|
return QuantityMath.accumulate_whole(target, field, key, value)
|
||||||
target[field] = {}
|
|
||||||
var expected: float = maxf(0.0, value) + target[field].get(key, 0.0)
|
|
||||||
var whole: float = floorf(snappedf(expected, 1.0e-12))
|
|
||||||
target[field][key] = maxf(0.0, expected - whole)
|
|
||||||
return whole
|
|
||||||
|
|
||||||
|
|
||||||
func _create_planets(target: Dictionary, star_id: String, count: float,
|
func _create_planets(target: Dictionary, star_id: String, count: float,
|
||||||
@@ -990,27 +951,7 @@ func _expired_years(year: float, start: float, end: float) -> float:
|
|||||||
|
|
||||||
# Largest-remainder allocation: whole float values, bounded by each request.
|
# Largest-remainder allocation: whole float values, bounded by each request.
|
||||||
func _allocate_whole(requests: Dictionary, limit: float) -> Dictionary:
|
func _allocate_whole(requests: Dictionary, limit: float) -> Dictionary:
|
||||||
var total: float = 0.0
|
return QuantityMath.allocate_whole(requests, limit)
|
||||||
for amount in requests.values():
|
|
||||||
total += floorf(maxf(0.0, amount))
|
|
||||||
var available: float = floorf(minf(maxf(0.0, limit), total))
|
|
||||||
var result: Dictionary = {}
|
|
||||||
var order: Array = []
|
|
||||||
var assigned: float = 0.0
|
|
||||||
for id in requests:
|
|
||||||
var expected: float = floorf(maxf(0.0, requests[id])) * available / total if total > 0.0 else 0.0
|
|
||||||
result[id] = floorf(expected)
|
|
||||||
assigned += result[id]
|
|
||||||
order.append({"id": id, "rest": expected - result[id]})
|
|
||||||
order.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 item: Dictionary in order:
|
|
||||||
if assigned >= available:
|
|
||||||
break
|
|
||||||
if result[item.id] < floorf(requests[item.id]):
|
|
||||||
result[item.id] += 1.0
|
|
||||||
assigned += 1.0
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# The entity currently exposes type/amount fields; life_product can contain
|
# The entity currently exposes type/amount fields; life_product can contain
|
||||||
@@ -1366,29 +1307,11 @@ func produce_hs(entity_id: String, value: float) -> float:
|
|||||||
# Find the smallest whole batch whose cost is whole for every resource.
|
# Find the smallest whole batch whose cost is whole for every resource.
|
||||||
# Decimal costs are represented as integer fractions; combine their denominators.
|
# Decimal costs are represented as integer fractions; combine their denominators.
|
||||||
func _production_step(entity: OwniverseEntity) -> float:
|
func _production_step(entity: OwniverseEntity) -> float:
|
||||||
var step: float = 1.0
|
return QuantityMath.production_step(entity.cost.values() + entity.bind.values())
|
||||||
for value in entity.cost.values() + entity.bind.values():
|
|
||||||
var cost: float = maxf(0.0, value)
|
|
||||||
if cost == 0.0:
|
|
||||||
continue
|
|
||||||
var decimal: String = String.num(cost, 12)
|
|
||||||
var point: int = decimal.find(".")
|
|
||||||
var digits: int = decimal.length() - point - 1 if point >= 0 else 0
|
|
||||||
var denominator: float = pow(10.0, digits)
|
|
||||||
var numerator: float = roundf(cost * denominator)
|
|
||||||
if numerator == 0.0:
|
|
||||||
return INF
|
|
||||||
denominator /= _whole_gcd(numerator, denominator)
|
|
||||||
step = step / _whole_gcd(step, denominator) * denominator
|
|
||||||
return step
|
|
||||||
|
|
||||||
|
|
||||||
func _whole_gcd(a: float, b: float) -> float:
|
func _whole_gcd(a: float, b: float) -> float:
|
||||||
while b > 0.0:
|
return QuantityMath.whole_gcd(a, b)
|
||||||
var remainder: float = fmod(a, b)
|
|
||||||
a = b
|
|
||||||
b = remainder
|
|
||||||
return a
|
|
||||||
|
|
||||||
|
|
||||||
func calculate_max_producible(entity: OwniverseEntity, desired_amount: float = 1.0E150) -> float:
|
func calculate_max_producible(entity: OwniverseEntity, desired_amount: float = 1.0E150) -> float:
|
||||||
@@ -1450,15 +1373,7 @@ func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> bo
|
|||||||
## @param values - dictionary containing numeric values
|
## @param values - dictionary containing numeric values
|
||||||
## @return dictionary with normalized values, each ranging from 0.0 to 1.0
|
## @return dictionary with normalized values, each ranging from 0.0 to 1.0
|
||||||
func _normalize_distribution(values: Dictionary) -> Dictionary:
|
func _normalize_distribution(values: Dictionary) -> Dictionary:
|
||||||
var normalized_distribution: Dictionary = {}
|
return QuantityMath.normalize_distribution(values)
|
||||||
var distribution_total: float = 0
|
|
||||||
for id in values:
|
|
||||||
if values[id] > 0:
|
|
||||||
distribution_total += values[id]
|
|
||||||
normalized_distribution[id] = values[id]
|
|
||||||
for id in normalized_distribution:
|
|
||||||
normalized_distribution[id] /= distribution_total
|
|
||||||
return normalized_distribution
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Quantity arithmetic without scene tree or global settings.
|
||||||
|
|
||||||
|
static func accumulate_whole(target: Dictionary, field: String, key: String, value: float) -> float:
|
||||||
|
if not target.has(field):
|
||||||
|
target[field] = {}
|
||||||
|
var expected: float = maxf(0.0, value) + target[field].get(key, 0.0)
|
||||||
|
var whole: float = floorf(snappedf(expected, 1.0e-12))
|
||||||
|
target[field][key] = maxf(0.0, expected - whole)
|
||||||
|
return whole
|
||||||
|
|
||||||
|
|
||||||
|
static func allocate_whole(requests: Dictionary, limit: float) -> Dictionary:
|
||||||
|
var total: float = 0.0
|
||||||
|
for amount in requests.values():
|
||||||
|
total += floorf(maxf(0.0, amount))
|
||||||
|
var available: float = floorf(minf(maxf(0.0, limit), total))
|
||||||
|
var result: Dictionary = {}
|
||||||
|
var order: Array = []
|
||||||
|
var assigned: float = 0.0
|
||||||
|
for id in requests:
|
||||||
|
var expected: float = floorf(maxf(0.0, requests[id])) * available / total if total > 0.0 else 0.0
|
||||||
|
result[id] = floorf(expected)
|
||||||
|
assigned += result[id]
|
||||||
|
order.append({"id": id, "rest": expected - result[id]})
|
||||||
|
order.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 item: Dictionary in order:
|
||||||
|
if assigned >= available:
|
||||||
|
break
|
||||||
|
if result[item.id] < floorf(requests[item.id]):
|
||||||
|
result[item.id] += 1.0
|
||||||
|
assigned += 1.0
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
static func production_step(costs: Array) -> float:
|
||||||
|
var step: float = 1.0
|
||||||
|
for value in costs:
|
||||||
|
var cost: float = maxf(0.0, value)
|
||||||
|
if cost == 0.0:
|
||||||
|
continue
|
||||||
|
var decimal: String = String.num(cost, 12)
|
||||||
|
var point: int = decimal.find(".")
|
||||||
|
var digits: int = decimal.length() - point - 1 if point >= 0 else 0
|
||||||
|
var denominator: float = pow(10.0, digits)
|
||||||
|
var numerator: float = roundf(cost * denominator)
|
||||||
|
if numerator == 0.0:
|
||||||
|
return INF
|
||||||
|
denominator /= whole_gcd(numerator, denominator)
|
||||||
|
step = step / whole_gcd(step, denominator) * denominator
|
||||||
|
return step
|
||||||
|
|
||||||
|
|
||||||
|
static func whole_gcd(a: float, b: float) -> float:
|
||||||
|
while b > 0.0:
|
||||||
|
var remainder: float = fmod(a, b)
|
||||||
|
a = b
|
||||||
|
b = remainder
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
static func normalize_distribution(values: Dictionary) -> Dictionary:
|
||||||
|
var normalized_distribution: Dictionary = {}
|
||||||
|
var distribution_total: float = 0
|
||||||
|
for id in values:
|
||||||
|
if values[id] > 0:
|
||||||
|
distribution_total += values[id]
|
||||||
|
normalized_distribution[id] = values[id]
|
||||||
|
for id in normalized_distribution:
|
||||||
|
normalized_distribution[id] /= distribution_total
|
||||||
|
return normalized_distribution
|
||||||
|
|
||||||
|
|
||||||
|
static func sum(pop: Dictionary) -> float:
|
||||||
|
var result: float = 0.0
|
||||||
|
for n in pop.values(): result += n
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
static func split(remainders: Dictionary, key: String, count: float, weights: Dictionary) -> Dictionary:
|
||||||
|
var result: Dictionary = {}
|
||||||
|
var sum_weights: float = sum(weights)
|
||||||
|
if count <= 0.0 or sum_weights <= 0.0: return result
|
||||||
|
var expected: Dictionary = {}
|
||||||
|
var positive_sum: float = 0.0
|
||||||
|
for id in weights:
|
||||||
|
expected[id] = count * weights[id] / sum_weights + remainders.get(key + ":" + id, 0.0)
|
||||||
|
positive_sum += maxf(0.0, expected[id])
|
||||||
|
var order: Array = []
|
||||||
|
var assigned: float = 0.0
|
||||||
|
for id in weights:
|
||||||
|
var normalized: float = maxf(0.0, expected[id]) * count / positive_sum if positive_sum > 0.0 else 0.0
|
||||||
|
result[id] = floorf(normalized)
|
||||||
|
assigned += result[id]
|
||||||
|
order.append({"id": id, "rest": normalized - result[id]})
|
||||||
|
order.sort_custom(func(a, b): return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
|
||||||
|
for entry in order:
|
||||||
|
if assigned >= count: break
|
||||||
|
result[entry.id] += 1.0
|
||||||
|
assigned += 1.0
|
||||||
|
for id in weights:
|
||||||
|
remainders[key + ":" + id] = clampf(expected[id] - result[id], -1.0, 1.0)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
static func accumulate_rate(remainders: Dictionary, key: String, count: float, probability: float) -> float:
|
||||||
|
var expected: float = count * maxf(0.0, probability) + remainders.get(key, 0.0)
|
||||||
|
var actual: float = floorf(snappedf(expected, 1e-10))
|
||||||
|
remainders[key] = maxf(0.0, expected - actual)
|
||||||
|
return actual
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dfl47j0lvkt2y
|
||||||
Reference in New Issue
Block a user