From 179682933878ad8126a9d661c2d89f8d8f41ef93 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 21 Sep 2026 20:42:40 +0300 Subject: [PATCH] Update speed limit, enable embedded PCK format, and enhance brown dwarf production logic --- autoload/global.gd | 2 +- export_presets.cfg | 2 +- simulation_rules.md | 12 ++++++ stage/stage.gd | 76 ++++++------------------------------ tests/README.md | 2 + tests/test_action_values.gd | 36 ++++++++++++----- tests/test_stellar_rules.gd | 59 ++++++++++++++++++++++++++++ ui/actions/actions.gd | 54 +++++++++++++++---------- world/owniverse/owniverse.gd | 46 +++++++++++++++++++++- 9 files changed, 192 insertions(+), 97 deletions(-) diff --git a/autoload/global.gd b/autoload/global.gd index 5746961..4122daf 100644 --- a/autoload/global.gd +++ b/autoload/global.gd @@ -1,6 +1,6 @@ extends Node -const SPEED_LIMIT: int = 5 +const SPEED_LIMIT: int = 6 # Write a readable diagnostic copy next to the encrypted save. Never used for loading. var save_plaintext_copy: bool = true diff --git a/export_presets.cfg b/export_presets.cfg index f72109d..c25ce50 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -30,7 +30,7 @@ script_export_mode=2 custom_template/debug="" custom_template/release="" debug/export_console_wrapper=1 -binary_format/embed_pck=false +binary_format/embed_pck=true texture_format/s3tc_bptc=true texture_format/etc2_astc=false shader_baker/enabled=false diff --git a/simulation_rules.md b/simulation_rules.md index 65bad89..7e83f41 100644 --- a/simulation_rules.md +++ b/simulation_rules.md @@ -13,6 +13,18 @@ 7. Применить суммарный бласт поколения; повторять обработку поколений, пока в интервале остаются переходы. 8. Пересчитать группы и опубликовать состояние. +## Автоматическое создание бурых карликов + +На горячие бурые карлики `HBD` выделяется 0,1% **нового фактически произведённого автоматического водорода** за расчётный интервал. По текущей таблице автоматическую долю имеют STN (`auto = 0.5`) и ASTN (`auto = 0.65`). Обычный водород, ручные покупки и неиспользованный запас прошлых интервалов повторно в эту долю не входят. + +При стоимости `c = 0.1 H` квота объектов увеличивается на `ΔQ = ΔH_auto × 0.001 / c`. Она хранится между циклами и в сохранении в `auto_star_deficits.HBD`. Доступность ограничивается автоматическим запасом и уже оплаченным топливом: `Q ≤ (auto_resources.H + HBD_fuel) / c`. + +Карлики появляются по одному. Для `q = max(0, Q)` создаётся `floor(q)` объектов и ещё один с вероятностью `q - floor(q)`, с ограничением по доступному топливу. Созданное количество вычитается из Q; отрицательный остаток компенсирует досрочное появление на следующих шагах. Повторного случайного округления без нового автоматического водорода нет. Это исключение из общего детерминированного округления GI. Состояние генератора `auto_star_deficits.HBD_random` сохраняется, поэтому загрузка продолжает ту же последовательность. + +Водород списывается целыми единицами авансом: `charge = ceil(max(0, count × c - HBD_fuel))`. Неиспользованный остаток `HBD_fuel += charge - count × c` сохраняется в `auto_star_deficits.HBD_fuel` и больше не входит в свободный запас или бюджет обычных звёзд. Например, 500 автоматических H позволяют создать 5 HBD, списав 1 H и оставив 0,5 H оплаченного топлива. Следующие 5 HBD используют этот остаток без повторного списания. Численность всегда целая, но не обязана быть кратной 10. При отсутствии обычных звёзд их доля остаётся в запасе, а производство HBD всё равно работает. + +Новые HBD получают обычные жизненные партии. По таблице их срок — 5 миллиардов лет, затем они постепенно превращаются в холодные `CBD`. На экране оба типа суммируются в `BD`. Ручного рецепта у общего счётчика BD нет. Миграция сохранений не требуется: квота использует существующий словарь, отсутствующее значение равно нулю. + ## Постепенные партии Окно рождения шириной `w = lifespan / 40` при `lifespan > 10`, иначе `w = lifespan`. Окно превращения сдвинуто на `lifespan`. При частичном списании: diff --git a/stage/stage.gd b/stage/stage.gd index 2e003de..b1386ca 100644 --- a/stage/stage.gd +++ b/stage/stage.gd @@ -9,10 +9,6 @@ var description_targets: Dictionary = {} var description_samples: Dictionary = {} var description_rates: Dictionary = {} var description_elapsed: float = 0.0 -const UI_UPDATE_INTERVAL: float = 0.1 -var ui_elapsed: float = 0.0 -var ui_pages: Vector2i = Vector2i(-1, -1) -var closing: bool = false const PAGE_COUNT = 5 const RESOURCE_PAGE = 1 @@ -93,9 +89,7 @@ func set_datetime(value: float): # Simulation time advances in tenths of a year; displaying five decimal # places only adds zeroes and suggests precision the simulation does not # have. - var text: String = Global.format_number(value, 1) if value != 0.0 else "0.0" - if date_time.text != text: - date_time.text = text + date_time.text = Global.format_number(value, 1) if value != 0.0 else "0.0" #region Item manipulations: update and visibility @@ -103,27 +97,8 @@ func set_datetime(value: float): func set_owniverse_item_value() -> void: for item_id in owniverse.items_to_update: if owniverse_items.has(item_id): - var item: OwniverseItem = owniverse_items[item_id] - if item.is_visible_in_tree() and item.get_global_rect().intersects(stage_page_scroller.get_global_rect()): - item.set_label(owniverse.items_to_update[item_id]) - - -func _refresh_ui(force: bool = false) -> void: - if force or owniverse.item_amounts_dirty: - owniverse.refresh_item_amounts(true) - set_owniverse_item_value() - set_datetime(owniverse.state.time.year) - _set_active_page_actions() - ui_elapsed = 0.0 - - -func _update_ui(delta: float) -> void: - ui_elapsed += delta - var position_in_pages: float = float(stage_page_scroller.scroll_horizontal) / PAGE_WIDTH - var pages := Vector2i(floori(position_in_pages), ceili(position_in_pages)) - if pages != ui_pages or ui_elapsed >= UI_UPDATE_INTERVAL: - ui_pages = pages - _refresh_ui() + owniverse_items[item_id].set_label(owniverse.items_to_update[item_id]) + owniverse.items_to_update.clear() #endregion @@ -244,9 +219,6 @@ func item_activated(item: OwniverseItem) -> void: if item.item_id == "S" or item.item_id == "H": var _value = owniverse.produce_hs(item.item_id, 100_000) - _unlock_items() - _refresh_ui(true) - _refresh_descriptions() func _check_counters(delta: float) -> Array: @@ -305,8 +277,6 @@ func _input(event: InputEvent) -> void: func _set_active_page_actions() -> void: for pa in page_actions: if !g_active_items.has(pa): continue - if not page_actions[pa].is_visible_in_tree() or not page_actions[pa].get_global_rect().intersects(stage_page_scroller.get_global_rect()): - continue var item_id: String = g_active_items[pa].item_id var entity: OwniverseEntity = owniverse.entities[item_id] @@ -332,17 +302,12 @@ func _page_action_pressed(page_id: int, action_amount: float) -> void: if action_amount > 0: owniverse.produce(item_id, action_amount) - _unlock_items() - _refresh_ui(true) - _refresh_descriptions() #endregion func _ready() -> void: - get_tree().auto_accept_quit = false - owniverse.defer_item_updates = true var inited_result = init_enitities() locked_items = inited_result.locked_items load_data() @@ -352,7 +317,6 @@ func _ready() -> void: set_datetime(owniverse.state.time.year) set_owniverse_item_value() _unlock_items() - _refresh_ui(true) g_counters.swipe.sec = 0.2 @@ -365,7 +329,8 @@ func _process(delta: float) -> void: owniverse.evolute(delta) _update_descriptions(delta) - _update_ui(delta) + set_owniverse_item_value() + set_datetime(owniverse.state.time.year) #owniverse.cycle(delta) @@ -374,6 +339,7 @@ func _process(delta: float) -> void: var actions = _check_counters(delta) _do_actions(actions) + _set_active_page_actions() func _change_speed(delta: int) -> void: @@ -440,8 +406,10 @@ func load_data() -> bool: SaveManager.blocked_saves["owniverse"] = true push_error("Save is missing the current entity: " + id) return false - owniverse.state = restored - owniverse.new_state = restored.duplicate(true) + var complete: Dictionary = owniverse.state.duplicate(true) + complete.merge(restored, true) + owniverse.state = complete + owniverse.new_state = complete owniverse.initialize_life_batches() owniverse.refresh_item_amounts() pending_page = clampi(int(game_data.get("ui", {}).get("current_page", 0)), 0, PAGE_COUNT - 1) @@ -455,41 +423,21 @@ func _restore_page() -> void: if pending_page >= 0: stage_page_scroller.scroll_horizontal = pending_page * PAGE_WIDTH pending_page = -1 - _refresh_ui(true) func save_data() -> void: var page: int = pending_page if page < 0: page = clampi(roundi(float(stage_page_scroller.scroll_horizontal) / PAGE_WIDTH), 0, PAGE_COUNT - 1) - SaveManager.request_save("owniverse", "qwerty", { + SaveManager.save_data("owniverse", "qwerty", { "state": owniverse.state, "ui": {"current_page": page}, }) func _notification(what: int) -> void: - if not is_node_ready() or closing: - return - if what == NOTIFICATION_WM_CLOSE_REQUEST: - _close_after_save() - elif what == NOTIFICATION_APPLICATION_PAUSED: + if is_node_ready() and (what == NOTIFICATION_WM_CLOSE_REQUEST or what == NOTIFICATION_APPLICATION_PAUSED): save_data() - # Mobile platforms can suspend all processing immediately after this event. - SaveManager.flush_saves() - - -func _close_after_save() -> void: - closing = true - set_process(false) - set_process_input(false) - mouse_filter = Control.MOUSE_FILTER_IGNORE - # Stop GUI actions too while the final snapshot is being written. - process_mode = Node.PROCESS_MODE_DISABLED - save_data() - if SaveManager.is_saving(): - await SaveManager.saves_drained - get_tree().quit() func init_enitities() -> Dictionary: diff --git a/tests/README.md b/tests/README.md index 50543c2..82605fb 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,5 +1,7 @@ # Проверки поэтапного переноса из Owniverse.U +`test_stellar_rules.gd` также проверяет автоматическое создание HBD: долю 0,1% нового автоматического водорода, появление по одному со случайным округлением, сохранение оплаченного остатка топлива, отсутствие повторного начисления на старый запас, продолжение случайной последовательности после сериализации и переход HBD → CBD с сохранением счётчика BD. Формулы описаны в `../simulation_rules.md`. + ## Фоновое сохранение и частота обновления интерфейса ```sh diff --git a/tests/test_action_values.gd b/tests/test_action_values.gd index c8ab69f..b2dc0d2 100644 --- a/tests/test_action_values.gd +++ b/tests/test_action_values.gd @@ -15,15 +15,33 @@ func check(value: bool, message: String) -> void: func run() -> void: var actions = load("res://ui/actions/actions.tscn").instantiate() root.add_child(actions) - var values: Array = [0.1, 0.01, 0.001, 1.0, 10.0] - var labels: Array = [] - for value in values: - labels.append(actions._format_action_value(value)) - check(labels == ["0.10", "0.010", "0.0010", "1", "10"], "Fractional action amounts retain precision") - check(labels[0] != labels[1] and labels[1] != labels[2], "Adjacent fractional amounts are distinguishable") - actions.set_actions("test", 123.0) - check(actions.action_values[0] == 1.0 and actions.action_values[1] == 10.0, "Distribution preserves distinct requests") - check(actions.action_labels[0].text != actions.action_labels[1].text, "Rendered adjacent buttons are distinct") + for maximum in [0.0, 0.1, 0.99, 1.0, 1.9, 10.0, 100.0, 123.0, 1000.0, 10000.0, 1e9, 1.1e9, 1e150]: + actions.set_actions("test", maximum) + var labels: Array = [] + var values: Array = [] + for i in 5: + var value: float = actions.action_values[i] + var text: String = actions.action_labels[i].text + check(value == floorf(value) and value <= floorf(maximum), "Whole affordable request") + if value > 0.0: + check(not values.has(value) and not labels.has(text), "Unique requests and displayed labels") + values.append(value) + labels.append(text) + else: + check(text == "" and actions.action_labels[i].get_parent().get_node("TextureButton").disabled, "Unused button blank and disabled") + if maximum >= 1.0: check(values.back() == floorf(maximum), "Maximum retained") + actions.set_actions("test", 100.0) + check(actions.action_values.values() == [1.0, 10.0, 100.0, 0.0, 0.0], "Exact power of ten not duplicated") + actions.set_actions("SMBH", 25.0, func(_request): return 25.0) + check(actions.action_values.values() == [25.0, 0.0, 0.0, 0.0, 0.0], "Identical SMBH previews collapse to one button") + actions.set_actions("step", 123.0, func(request): return floorf(request / 5.0) * 5.0) + check(actions.action_values.values() == [10.0, 100.0, 123.0, 0.0, 0.0], "Unproducible small requests omitted") + check(actions.action_labels[2].text == "120", "Recipe preview displayed while original request retained") + var emitted: Array = [] + actions.action_pressed.connect(func(_page, amount): emitted.append(amount)) + actions._on_action_button_pressed(2) + actions._on_action_button_pressed(4) + check(emitted == [123.0], "Only active button emits its matching request") actions.queue_free() print("Action values: %d checks, %d failures" % [checks, failures]) quit(0 if failures == 0 else 1) diff --git a/tests/test_stellar_rules.gd b/tests/test_stellar_rules.gd index 7b9f875..648ae05 100644 --- a/tests/test_stellar_rules.gd +++ b/tests/test_stellar_rules.gd @@ -168,6 +168,65 @@ func run() -> void: check(world.state.binds.RD.HP >= 1.0, "Automatic formation also creates planets") dispose(world) + world = fixture() + world.entities.N.auto = 1.0 + # 1000 newly produced automatic H reserve 1 H, enough for 10 HBD. + world.evolute(100.0) + check(world.state.amount.HBD == 10.0, "Brown dwarfs form without existing ordinary stars") + check(world.state.amount.H == 999.0 and world.state.auto_resources.H == 999.0, "Brown dwarfs consume exactly their reserved hydrogen") + check(world.get_item_amounts().BD == 10.0 and world.state.batches.HBD.size() > 0, "Brown dwarfs appear in counter and evolution batches") + world.entities.N.product_per_year_amount = 0.0 + world.evolute(100.0) + check(world.state.amount.HBD == 10.0, "Stored automatic budget is not taxed repeatedly") + dispose(world) + + world = fixture() + world.entities.N.auto = 1.0 + world.state.amount.RD = 1.0 + world.evolute(50.0) + check(world.state.amount.HBD == 5.0 and is_equal_approx(world.state.auto_star_deficits.HBD_fuel, 0.5), "Individual dwarfs retain prepaid fractional fuel") + check(world.state.amount.H == 0.0, "Prepaid fuel is not available to ordinary stars") + var resumed = fixture() + resumed.entities.N.auto = 1.0 + resumed.state = bytes_to_var(var_to_bytes(world.state)) + world.evolute(50.0) + resumed.evolute(50.0) + check(world.state.amount.HBD == 10.0, "Accumulated quota eventually creates a whole batch") + check(world.state == resumed.state, "Brown dwarf quota survives serialization deterministically") + check(world.state.amount.RD == 1000.0 and world.state.amount.H == 0.0, "Shared automatic hydrogen is spent once") + world.state.batches.HBD = [{"amount": 10.0, "start_year": 101.0, "end_year": 101.0}] + world._evolve_batches(world.state, 101.0) + check(world.state.amount.HBD == 0.0 and world.state.amount.CBD == 10.0 and world.get_item_amounts().BD == 10.0, "Cooling preserves aggregate brown dwarf count") + dispose(resumed) + dispose(world) + + world = fixture() + var saw_individual: bool = false + for i in range(250): + world.state.amount.H += 1.0 + world.state.auto_resources.H = world.state.auto_resources.get("H", 0.0) + 1.0 + var before: float = world.state.amount.HBD + world._produce_brown_dwarfs(world.state, 1.0, float(i), float(i + 1)) + var born: float = world.state.amount.HBD - before + check(born == 0.0 or born == 1.0, "Small fuel increments produce individual dwarfs") + saw_individual = saw_individual or born == 1.0 + check(saw_individual, "Random rounding does not wait for ten dwarfs") + resumed = fixture() + resumed.state = bytes_to_var(var_to_bytes(world.state)) + for i in range(250, 500): + for simulation in [world, resumed]: + simulation.state.amount.H += 1.0 + simulation.state.auto_resources.H += 1.0 + simulation._produce_brown_dwarfs(simulation.state, 1.0, float(i), float(i + 1)) + check(world.state == resumed.state, "Random sequence and prepaid fuel survive saving") + check(is_equal_approx(world.state.amount.H + world.state.amount.HBD * 0.1 + world.state.auto_star_deficits.HBD_fuel, 500.0), "Individual production conserves fuel including prepaid reserve") + check(is_equal_approx(world.state.amount.HBD + world.state.auto_star_deficits.HBD, 5.0), "Signed remainder preserves the 0.1 percent allocation") + var paused_count: float = world.state.amount.HBD + for i in 10: world._produce_brown_dwarfs(world.state, 0.0, 500.0, 501.0) + check(world.state.amount.HBD == paused_count, "No repeated random attempts without new fuel") + dispose(resumed) + dispose(world) + # Two simultaneous successors share surviving planets in the same 99:1 ratio. world = fixture() world.entities.HRG.destroy_level = 0 diff --git a/ui/actions/actions.gd b/ui/actions/actions.gd index 0d7360e..7ebe3d0 100644 --- a/ui/actions/actions.gd +++ b/ui/actions/actions.gd @@ -26,21 +26,37 @@ var action_values: Dictionary = { func set_actions(item_id: String, value: float, preview: Callable = Callable()) -> void: current_item_id = item_id var requests: Array = _get_distribution(value) - var displayed: Array = requests.duplicate() - if preview.is_valid(): - for i in range(displayed.size()): - displayed[i] = preview.call(requests[i]) if requests[i] > 0.0 else 0.0 - _set_action_values(requests, displayed) + var unique_requests: Array = [] + var displayed: Array = [] + var labels: Dictionary = {} + # Prefer the largest request (including MAX) when previews or rounded + # scientific labels coincide. Keep the request paired with its preview. + for i in range(requests.size() - 1, -1, -1): + if requests[i] <= 0.0: continue + var amount: float = preview.call(requests[i]) if preview.is_valid() else requests[i] + if not is_finite(amount) or amount < 1.0 or amount != floorf(amount): continue + var text: String = _format_action_value(amount) + if displayed.has(amount) or labels.has(text): continue + labels[text] = true + unique_requests.push_front(requests[i]) + displayed.push_front(amount) + while unique_requests.size() < 5: + unique_requests.append(0.0) + displayed.append(0.0) + _set_action_values(unique_requests, displayed) func _get_distribution(value: float) -> Array: - if value <= 0.0 or not is_finite(value): + if value < 1.0 or not is_finite(value): return [0.0, 0.0, 0.0, 0.0, 0.0] - var distribution: Array = [value, 0, 0, 0, 0] - - var exp_value = int(floor(log(abs(value)) / log(10.0))) - var floor_value = max(exp_value - 4, -1) - for ev in range(exp_value, floor_value, -1): - distribution.push_front(pow(10.0, ev)) + value = floorf(value) + var distribution: Array = [] + var power: float = 1.0 + while power < value: + distribution.append(power) + if distribution.size() > 4: distribution.pop_front() + power *= 10.0 + distribution.append(value) + while distribution.size() < 5: distribution.append(0.0) return distribution func _set_action_values(values: Array, displayed: Array) -> void: @@ -50,19 +66,15 @@ func _set_action_values(values: Array, displayed: Array) -> void: var text: String = _format_action_value(displayed[ind]) if action_labels[ind].text != text: action_labels[ind].text = text + action_labels[ind].get_parent().get_node("TextureButton").disabled = values[ind] <= 0.0 func _format_action_value(value: float) -> String: - if value <= 0.0 or not is_finite(value): + if value < 1.0 or not is_finite(value) or value != floorf(value): return "" - if value >= 1.0: - return Global.format_number(value) - # Global's default is intended for object counts. Action values may be - # fractional, so retain enough precision to keep adjacent buttons distinct. - var exponent: int = int(floor(log(value) / log(10.0))) - var digits: int = clampi(-exponent + 1, 2, 6) - return Global.format_number(value, digits) + return Global.format_number(value) func _on_action_button_pressed(button_id: int) -> void: - action_pressed.emit(page_id, action_values[button_id]) + if action_values.get(button_id, 0.0) > 0.0: + action_pressed.emit(page_id, action_values[button_id]) diff --git a/world/owniverse/owniverse.gd b/world/owniverse/owniverse.gd index c1e7c5b..1e73feb 100644 --- a/world/owniverse/owniverse.gd +++ b/world/owniverse/owniverse.gd @@ -997,6 +997,8 @@ func _produce_for_period(previous: Dictionary, target: Dictionary, quantities: D for id in budget: budget[id] = minf(maxf(0.0, budget[id]), _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 = {} @@ -1059,7 +1061,7 @@ func _produce_for_period(previous: Dictionary, target: Dictionary, quantities: D continue if not _try_production(target, entity, actual, true): continue - budget = target.auto_resources + budget = _ordinary_star_budget(target) created[id] = actual total_created += actual for id in candidates: @@ -1071,6 +1073,48 @@ func _produce_for_period(previous: Dictionary, target: Dictionary, quantities: D _create_planets(target, id, created[id], from_year, to_year, metallicity) +# HBD quota is in objects. HBD_fuel is prepaid hydrogen removed from free stock. +# Signed rounding debt and the PRNG state survive saving with this dictionary. +func _produce_brown_dwarfs(target: Dictionary, new_hydrogen: float, from_year: float, to_year: float) -> void: + if not entities.has("HBD"): + return + var entity: OwniverseEntity = 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), _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: + _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 + _credit(target, "HBD", amount) + _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 entities.has("HBD"): + var reserved: float = maxf(0.0, target.auto_star_deficits.get("HBD", 0.0) * 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 = {}