From f51eea6c0f0740ef2e9d21d5874609938e398577 Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 1 Oct 2026 20:30:53 +0300 Subject: [PATCH] Enhance simulation performance and accuracy by optimizing physical amount calculations and refining arithmetic operations --- stage/stage.gd | 15 +++- tests/README.md | 5 +- tests/test_exact_math.gd | 7 +- world/owniverse/README.md | 5 ++ world/owniverse/automatic_production.gd | 7 +- world/owniverse/civilization_evolution.gd | 4 +- world/owniverse/civilization_state.gd | 39 +++++----- world/owniverse/evolution_coordinator.gd | 5 +- world/owniverse/manual_production.gd | 6 +- world/owniverse/math/README.md | 5 ++ world/owniverse/math/exact.gd | 89 +++++++++++++++++------ world/owniverse/object_batches.gd | 11 +++ world/owniverse/object_ledger.gd | 7 ++ world/owniverse/stellar_rules.gd | 5 +- 14 files changed, 155 insertions(+), 55 deletions(-) diff --git a/stage/stage.gd b/stage/stage.gd index 0db4d39..c685788 100644 --- a/stage/stage.gd +++ b/stage/stage.gd @@ -91,6 +91,8 @@ var g_counters: Dictionary[String, Counter] = { var g_active_items: Dictionary = {} var pending_page: int = -1 +var active_actions_dirty: bool = true +var active_actions_elapsed: float = 0.0 var owniverse:Owniverse = Owniverse.new() var owniverse_items: Dictionary[String, OwniverseItem] = {} @@ -174,6 +176,7 @@ func _control_page(item: OwniverseItem) -> void: return g_active_items[item.item_page] = item + active_actions_dirty = true if current_item != null and current_item.item_page == item.item_page: current_item.set_selection(false) @@ -338,6 +341,8 @@ func create_new_universe() -> void: description_samples.clear() description_rates.clear() description_elapsed = 0.0 + active_actions_dirty = true + active_actions_elapsed = 0.0 for label in g_item_descriptions: label.text = "" for item in owniverse_items.values(): @@ -379,8 +384,14 @@ func _process(delta: float) -> void: var actions = _check_counters(delta) _do_actions(actions) - - _set_active_page_actions() + # Production limits are expensive (they walk the resource ledger). Refresh + # them when selection changes and at a modest cadence instead of every + # rendered frame. The simulation itself continues to run every frame. + active_actions_elapsed += delta + if active_actions_dirty or active_actions_elapsed >= 0.1: + active_actions_elapsed = 0.0 + active_actions_dirty = false + _set_active_page_actions() func _change_speed(delta: int) -> void: diff --git a/tests/README.md b/tests/README.md index 2d1899f..4fdd216 100644 --- a/tests/README.md +++ b/tests/README.md @@ -13,8 +13,9 @@ godot --headless --path Owniverse.GI --script res://tests/benchmark_saved_state. На диагностической копии с 120 партиями прежняя рациональная арифметика после оптимизации пустых окон давала 90–99 мс на шаг 1/60 секунды на скорости 6. -После перехода на целые блоки и float-остаток повторный замер дал 58–62 мс. -При шаге 0,1 секунды: 71–82 мс вместо 117–138 мс. Это замеры расчёта на данной +После перехода на целые блоки и устранения повторного построения физических +количеств повторный замер дал 53–58 мс. При шаге 0,1 секунды: 65–67 мс вместо +117–138 мс. Это замеры расчёта на данной копии, а не обещание FPS. Для сравнения старый JSON был преобразован только во временный тестовый файл; в загрузчик игры миграция не добавлена. Разреженные сценарии сравниваются с полным расчётом в `test_short_lived_events.gd`. diff --git a/tests/test_exact_math.gd b/tests/test_exact_math.gd index d2a6e0e..50c907b 100644 --- a/tests/test_exact_math.gd +++ b/tests/test_exact_math.gd @@ -62,11 +62,16 @@ func run(): check(E.bounded(count), str(count), "integer counts never quantized") check(E.mul_div(E.of("1e27"), 15, 100), "150000000000000000000000000", "exact percentage") check(E.mul_div(count, count, count), str(count), "fused product beyond storage range") + var limit = E.of("1e27") + check(E.floor_value(E.mul_div(E.subtract(limit, 1), E.subtract(limit, 3), E.subtract(limit, 2))), "999999999999999999999999997", "fused ratio immediately below whole boundary") + check(E.floor_value(E.divide(E.MIN_INT, -3)), "3074457345618258602", "MIN_INT division preserves quotient") + check(E.floor_value(E.divide(-10, 3)), "-4", "native negative fractional division") + check(E.mul_div(E.MAX_INT, 0, 7), "0", "native zero fused product") check(E.subtract(E.add(count, 1), count), "1", "single unit at E27") check(E.floor_value(E.divide(E.subtract(count, 1), count)), "0", "division must not round up a whole object") check(E.fixed(E.add(count, E.of("0.125")), 3), "1000000000000000000000000001.125", "formatting without oversized intermediate") check(E.scientific(E.of("9.999999e27"), 2), "1.00E+28", "display rounding at upper boundary") - check(E.eq(E.power(10, -100), E.of("1e-100")), "true", "negative power needs no huge intermediate") + check(absf(E.approx(E.power(10, -100)) / E.approx(E.of("1e-100")) - 1.0) < 1e-15, "true", "negative power retains float precision without huge intermediate") var largest = E.of("9999999999999999999999999999") check(E.eq(E.decode(E.encode(largest)), largest), "true", "upper range round trip") check(E.valid_encoded(E.encode(largest)), "true", "upper range encoding valid") diff --git a/world/owniverse/README.md b/world/owniverse/README.md index 11d3cf7..2d10fe6 100644 --- a/world/owniverse/README.md +++ b/world/owniverse/README.md @@ -41,6 +41,11 @@ Stage обращается непосредственно к `world.presentation В `owniverse.gd` остались создание состояния и модулей, скорость и проверка входного delta перед запуском эволюции. Дублирующее поле new_state и закомментированная старая реализация удалены. Единственный источник текущего состояния — `world.state`. +Внутри одной фазы снимок физических количеств переиспользуется для сверки +этик, звёздного бюджета и бюджета структур. После списания партий или переходов +создаётся новый снимок. Операции над одним типом объекта используют +`physical_amount`, не пересчитывая полную карту вселенной. + ### Порядок шага 1. Определить обработанную десятую года. При продвижении времени восстановить недостающие партии жизни и проверить вместимость. diff --git a/world/owniverse/automatic_production.gd b/world/owniverse/automatic_production.gd index cf33d8d..afcbefe 100644 --- a/world/owniverse/automatic_production.gd +++ b/world/owniverse/automatic_production.gd @@ -216,7 +216,12 @@ func produce_newborn_lifetime(target: Dictionary, id: String, count: Variant, bo object_years = E.subtract(object_years, E.multiply(E.maximum(0, added), world.batches.expired_years(to_year, batch.start_year, batch.end_year))) object_years = E.maximum(0, object_years) for resource in products: - var output: Variant = E.multiply(E.multiply(object_years, products[resource]), WorldParameters.production(target, world.catalog.entities[id])) + var coefficient: Variant = E.multiply(products[resource], WorldParameters.production(target, world.catalog.entities[id])) + # A giant cohort can accumulate more than E27 object-years. Clamp the + # multiplicand before multiplying so the exact-number backend never + # constructs an out-of-range intermediate value. + var safe_years: Variant = (E.minimum(object_years, E.divide(E.max_value(), coefficient)) if E.gt(coefficient, 0) else 0) + var output: Variant = E.multiply(safe_years, coefficient) if E.contains(["H", "S"], resource): output = E.minimum(output, target.energy) var actual: Variant = world.ledger.credit(target, resource, output) diff --git a/world/owniverse/civilization_evolution.gd b/world/owniverse/civilization_evolution.gd index 6fc9977..e4d4d1b 100644 --- a/world/owniverse/civilization_evolution.gd +++ b/world/owniverse/civilization_evolution.gd @@ -76,8 +76,8 @@ static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop plan.to = E.maximum(plan.to, year) return removed -static func prepare(world, target: Dictionary, expired: Dictionary, year: Variant, budget: Dictionary, rules: Dictionary) -> Dictionary: - State.reconcile(world, target, rules) +static func prepare(world, target: Dictionary, expired: Dictionary, year: Variant, budget: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary: + State.reconcile(world, target, rules, physical) var remaining: Dictionary = target.ethics.duplicate(true) var plans: Dictionary = {} var colonies: Array = [] diff --git a/world/owniverse/civilization_state.gd b/world/owniverse/civilization_state.gd index faa9c83..c08acb1 100644 --- a/world/owniverse/civilization_state.gd +++ b/world/owniverse/civilization_state.gd @@ -10,10 +10,11 @@ static func is_space_civilization(id: String, rules: Dictionary) -> bool: # Preserve any existing ethics. New populations start equally distributed; # reductions (including blasts) are proportional and never leave negative counts. -static func reconcile(world, target: Dictionary, rules: Dictionary) -> void: +static func reconcile(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> void: if (not target.has("ethics")): target.ethics = {} - var physical: Dictionary = world.ledger.physical_amounts(target) + if physical.is_empty(): + physical = world.ledger.physical_amounts(target) for id in rules: var count: Variant = physical.get(id, 0) var current: Dictionary = target.ethics.get(id, {}) @@ -40,8 +41,9 @@ static func detach_space_civilizations(target: Dictionary, rules: Dictionary) -> # A shared budget per stellar family, including stars bound into structures. # Reservations affect capacity, not the physical stock or its production. -static func stellar_budget(world, target: Dictionary, rules: Dictionary) -> Dictionary: - var physical: Dictionary = world.ledger.physical_amounts(target) +static func stellar_budget(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary: + if physical.is_empty(): + physical = world.ledger.physical_amounts(target) var capacity: Dictionary = {} for id in physical: var entity = world.catalog.entities[id] @@ -62,7 +64,7 @@ static func stellar_budget(world, target: Dictionary, rules: Dictionary) -> Dict var free: Dictionary = {} for home in capacity: free[home] = E.maximum(0, E.subtract(capacity[home], requested.get(home, 0))) - return {"capacity": capacity, "requested": requested, "free": free, "populations": populations, "habitats": habitat_budget(world, target, rules)} + return {"capacity": capacity, "requested": requested, "free": free, "populations": populations, "habitats": habitat_budget(world, target, rules, physical)} # Account for the source's existing reservation when changing stages. A failed # transition retains its population at the source, rather than losing objects. @@ -105,8 +107,9 @@ static func admit_transition(budget: Dictionary, rules: Dictionary, source: Stri # advanced populations priority and lets a demotion cascade in the same cycle. static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: Variant) -> void: detach_space_civilizations(target, rules) - reconcile(world, target, rules) - var budget: Dictionary = stellar_budget(world, target, rules) + var physical: Dictionary = world.ledger.physical_amounts(target) + reconcile(world, target, rules, physical) + var budget: Dictionary = stellar_budget(world, target, rules, physical) var remaining: Dictionary = budget.capacity.duplicate() var remaining_homes: Dictionary = budget.habitats.capacity.duplicate() var stages: Array = [] @@ -141,10 +144,10 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: target.ethics[previous][ethic] = E.add(target.ethics[previous].get(ethic, 0), removed[ethic]) if E.eq(previous, "C1.9"): var stars: Dictionary = {} - var physical: Dictionary = world.ledger.physical_amounts(target) - for star in physical: - if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(physical[star], 0)): - stars[star] = physical[star] + var current_physical: Dictionary = world.ledger.physical_amounts(target) + for star in current_physical: + if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(current_physical[star], 0)): + stars[star] = current_physical[star] var weights: Dictionary = QuantityMath.normalize_distribution(stars) # Worlds are aggregates: a star may host several inhabited planets. var assigned: Variant = 0 @@ -158,13 +161,15 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: target.binds[owner] = {} target.binds[owner][previous] = E.add(target.binds[owner].get(previous, 0), amount) target.amount[previous] = E.subtract(target.amount[previous], amount) - reconcile(world, target, rules) - assign_homes(world, target, rules) + physical = world.ledger.physical_amounts(target) + reconcile(world, target, rules, physical) + assign_homes(world, target, rules, physical) # Unity's structure chain: direct Claim resets the required structure count; # intervening stages require ten times more units of the preceding structure. -static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dictionary: - var physical: Dictionary = world.ledger.physical_amounts(target) +static func habitat_budget(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary: + if physical.is_empty(): + physical = world.ledger.physical_amounts(target) var structures: Array = [] var capacity: Dictionary = {} for id in world.catalog.entities: @@ -198,8 +203,8 @@ static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dict requested[req.id] = E.add(requested.get(req.id, 0), E.multiply(physical.get(id, 0), req.units)) return {"capacity": capacity, "requested": requested, "requirements": requirements, "structures": structures, "anchors": anchors} -static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void: - var budget: Dictionary = habitat_budget(world, target, rules) +static func assign_homes(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> void: + var budget: Dictionary = habitat_budget(world, target, rules, physical) var used: Dictionary = {} target.civilization_homes = {} var stages: Array = budget.anchors.keys() diff --git a/world/owniverse/evolution_coordinator.gd b/world/owniverse/evolution_coordinator.gd index 2b87724..0f2f074 100644 --- a/world/owniverse/evolution_coordinator.gd +++ b/world/owniverse/evolution_coordinator.gd @@ -92,8 +92,9 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di 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) + var current_quantities: Dictionary = world.ledger.physical_amounts(target) + var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules, current_quantities) + var civilization_transaction: Dictionary = CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules, current_quantities) for id in expired: var total: Variant = 0 for part : Dictionary in expired[id]: diff --git a/world/owniverse/manual_production.gd b/world/owniverse/manual_production.gd index bd685b3..7a61b93 100644 --- a/world/owniverse/manual_production.gd +++ b/world/owniverse/manual_production.gd @@ -11,12 +11,14 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant trial.binds = target.binds.duplicate(true) trial.auto_resources = target.auto_resources.duplicate() trial.batches = target.batches.duplicate() + # Physical object totals do not change while resources are withdrawn from + # this transaction. Keep one snapshot instead of scanning every resource. + var physical: Dictionary = world.ledger.physical_amounts(trial) var copied_batches: Dictionary = {} for resource in entity.cost: var cost: Variant = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount)) if (automatic and E.lt(trial.auto_resources.get(resource, 0), cost)): return false - var before: Dictionary = world.ledger.physical_amounts(trial) var taken: Dictionary = world.ledger.take_available(trial, resource, cost) var total: Variant = 0 for quantity in taken.values(): @@ -30,7 +32,7 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant 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: Variant = (E.subtract(1, E.divide(taken[id], before[id])) if E.gt(before.get(id, 0), 0) else 0) + var factor: Variant = (E.subtract(1, E.divide(taken[id], physical[id])) if E.gt(physical.get(id, 0), 0) else 0) world.batches.scale_batches(trial, id, factor) if (not world.ledger.bind_new_owners(trial, {entity.id: amount}, true)): return false diff --git a/world/owniverse/math/README.md b/world/owniverse/math/README.md index ee7a4b9..2a84155 100644 --- a/world/owniverse/math/README.md +++ b/world/owniverse/math/README.md @@ -21,6 +21,11 @@ polarity × (high × 10^14 + low + fraction) ## Операции Сложение, вычитание и сравнение работают непосредственно с блоками. +Деление int64 и объединённое умножение/деление при безопасном произведении +используют нативную арифметику. Перевод больших целых между блоками и временными +разрядами, извлечение коэффициента и сборка результата обходятся без строк. +Дробный коэффициент нормализуется численно до 16 значащих цифр; последние биты +float могут отличаться от результата прежнего строкового преобразования. Для умножения и деления `big_integer.gd` используется как временный вычислитель с безопасными разрядами по основанию 10^9. Результат сразу возвращается в фиксированную структуру. Размер временных дробей определяется входными float, diff --git a/world/owniverse/math/exact.gd b/world/owniverse/math/exact.gd index 5de7443..e1d8566 100644 --- a/world/owniverse/math/exact.gd +++ b/world/owniverse/math/exact.gd @@ -8,31 +8,55 @@ const MIN_INT: int = -9223372036854775807 - 1 const BLOCK: int = 100000000000000 # 10^14 const LIMIT: Array = [0, 0, 0, 10] # 10^28, exclusive +static func max_value(): + return make([999999999, 999999999, 9999]) + static func make(n: Array, d: Array = [1], polarity: int = 1): assert(d != [0], "Zero denominator") + if n.size() <= 2 and d.size() <= 2: + var numerator: int = n[0] + (n[1] * SMALL if n.size() == 2 else 0) + var denominator: int = d[0] + (d[1] * SMALL if d.size() == 2 else 0) + var whole: int = numerator / denominator + var fraction: float = minf(float(numerator % denominator) / float(denominator), 1.0 - pow(2.0, -53)) + return _compose(whole / BLOCK, whole % BLOCK, fraction, polarity) var qr: Array = Big.divmod(n, d) - var fraction: float = 0.0 - if qr[1] != [0]: - var numerator_text: String = Big.decimal(qr[1]) - var denominator_text: String = Big.decimal(d) - if denominator_text.length() < 300: - fraction = float(numerator_text) / float(denominator_text) - else: - var exponent: int = maxi(0, numerator_text.length() - 16) - maxi(0, denominator_text.length() - 16) - fraction = float(numerator_text.left(16)) / float(denominator_text.left(16)) - fraction *= pow(10.0, maxi(-308, exponent)) - fraction *= pow(10.0, mini(0, exponent + 308)) - # A remainder below one must not round up and create a whole object. - fraction = minf(fraction, 1.0 - pow(2.0, -53)) + var fraction: float = _fraction_ratio(qr[1], d) return _from_whole(qr[0], fraction, polarity) +static func _fraction_ratio(n: Array, d: Array) -> float: + if n == [0]: return 0.0 + var numerator: float = 0.0 + var denominator: float = 0.0 + var n_start: int = maxi(0, n.size() - 3) + var d_start: int = maxi(0, d.size() - 3) + for i in range(n.size() - 1, n_start - 1, -1): numerator = numerator * SMALL + n[i] + for i in range(d.size() - 1, d_start - 1, -1): denominator = denominator * SMALL + d[i] + var exponent: int = 9 * (n_start - d_start) + var fraction: float = numerator / denominator * pow(10.0, maxi(-308, exponent)) + fraction *= pow(10.0, mini(0, exponent + 308)) + return minf(fraction, 1.0 - pow(2.0, -53)) + static func _from_whole(n: Array, fraction: float, polarity: int): - var text: String = Big.decimal(n) assert(Big.compare(n, LIMIT) < 0, "Number exceeds E27 range") - var high: int = int(text.left(text.length() - 14)) if text.length() > 14 else 0 - var low: int = int(text.right(14)) + var middle: int = n[1] if n.size() > 1 else 0 + var low: int = n[0] + (middle % 100000) * SMALL + var high: int = middle / 100000 + if n.size() > 2: high += n[2] * 10000 + if n.size() > 3: high += n[3] * 10000000000000 return _compose(high, low, fraction, polarity) +static func _whole_limbs(high: int, low: int) -> Array: + return Big.trim([low % SMALL, low / SMALL + (high % 10000) * 100000, + (high / 10000) % SMALL, high / 10000000000000]) + +static func _int_limbs(value: int) -> Array: + var result: Array = [] + # Modulo/division before abs also supports MIN_INT. + while value != 0: + result.append(absi(value % SMALL)) + value /= SMALL + return result if not result.is_empty() else [0] + static func _compose(high: int, low: int, fraction: float, polarity: int): if fraction >= 1.0: low += int(floorf(fraction)) @@ -91,17 +115,23 @@ static func of(value): static func parts(value) -> Array: value = of(value) - if value is int: return [Big.parse(str(value).trim_prefix("-")), [1], -1 if value < 0 else 1] - var whole: Array = Big.parse(str(value.high) + "%014d" % value.low) + if value is int: return [_int_limbs(value), [1], -1 if value < 0 else 1] + var whole: Array = _whole_limbs(value.high, value.low) if value.fraction == 0.0: return [whole, [1], value.polarity] # Bound temporary decimal work to 16 significant digits of the float. # Normalize first so tiny rates retain their significant digits as well. var exponent: int = maxi(-323, int(floor(log(value.fraction) / log(10.0)))) - var text: String = String.num(value.fraction / pow(10.0, exponent), 15) - var point: int = text.find(".") - if point >= 0: exponent -= text.length() - point - 1 - var n: Array = Big.parse(text.replace(".", "")) - var d: Array = Big.parse("1" + "0".repeat(-exponent)) + var mantissa: int = roundi((value.fraction / pow(10.0, exponent)) * 1000000000000000.0) + exponent -= 15 + while mantissa % 10 == 0 and mantissa > 0: + mantissa /= 10 + exponent += 1 + var n: Array = _int_limbs(mantissa) + var d: Array = [] + d.resize((-exponent) / 9 + 1) + d.fill(0) + const POWERS: Array = [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000] + d[d.size() - 1] = POWERS[(-exponent) % 9] return [Big.add(Big.multiply(whole, d), n), d, value.polarity] static func numeric(value) -> bool: @@ -208,6 +238,8 @@ static func multiply(a, b): ## Fused ratio: the temporary product is never stored as a simulation value. static func mul_div(a, b, divisor): if not finite(a) or not finite(b) or not finite(divisor): return of(approx(a) * approx(b) / approx(divisor)) + if a is int and b is int and a != MIN_INT and b != MIN_INT: + if b == 0 or absi(a) <= MAX_INT / absi(b): return divide(a * b, divisor) var x: Array = parts(a) var y: Array = parts(b) var z: Array = parts(divisor) @@ -220,7 +252,16 @@ static func divide(a, b): if eq(b, 1): return of(a) if a is int and b is int and b != 0 and not (a == MIN_INT and b == -1): if a % b == 0: return a / b - return mul_div(a, 1, b) + if a != MIN_INT and b != MIN_INT: + var numerator: int = absi(a) + var denominator: int = absi(b) + var whole: int = numerator / denominator + var fraction: float = minf(float(numerator % denominator) / float(denominator), 1.0 - pow(2.0, -53)) + return _compose(whole / BLOCK, whole % BLOCK, fraction, -1 if (a < 0) != (b < 0) else 1) + var x: Array = parts(a) + var y: Array = parts(b) + assert(y[0] != [0], "Division by zero") + return make(Big.multiply(x[0], y[1]), Big.multiply(x[1], y[0]), x[2] * y[2]) static func floor_value(a): if not finite(a) or a is int: return a diff --git a/world/owniverse/object_batches.gd b/world/owniverse/object_batches.gd index 36199af..8430c83 100644 --- a/world/owniverse/object_batches.gd +++ b/world/owniverse/object_batches.gd @@ -6,6 +6,17 @@ const WorldParameters = preload("res://world/owniverse/world_parameters.gd") func scale_batches(target: Dictionary, id: String, factor: Variant) -> void: var batches: Array = target.batches.get(id, []) + if batches.is_empty(): + return + # The common case for short-lived stellar events is a single cohort. Avoid + # allocating the shares array and sorting it when no redistribution is + # needed. + if batches.size() == 1: + var only: Dictionary = batches[0] + var expected: Variant = E.multiply(only.amount, E.maximum(0, factor)) + only.amount = E.round_value(expected) + only.withdraw_remainder = E.minimum(only.get("withdraw_remainder", 0), only.amount) + return var total: Variant = 0 var assigned: Variant = 0 var shares: Array = [] diff --git a/world/owniverse/object_ledger.gd b/world/owniverse/object_ledger.gd index c80e51e..91b09c0 100644 --- a/world/owniverse/object_ledger.gd +++ b/world/owniverse/object_ledger.gd @@ -15,6 +15,13 @@ func physical_amounts(source: Dictionary) -> Dictionary: return amounts +func physical_amount(source: Dictionary, id: String) -> Variant: + var amount: Variant = E.maximum(0, source.amount.get(id, 0)) + for bindings: Dictionary in source.binds.values(): + amount = E.add(amount, E.maximum(0, bindings.get(id, 0))) + return amount + + func available(source: Dictionary, id: String) -> Variant: var amount: Variant = 0 for member in world.catalog.members(id): diff --git a/world/owniverse/stellar_rules.gd b/world/owniverse/stellar_rules.gd index 3d397ba..d611ba5 100644 --- a/world/owniverse/stellar_rules.gd +++ b/world/owniverse/stellar_rules.gd @@ -69,7 +69,6 @@ func destroy_planets(target: Dictionary, owner_id: String, ratio: Variant, level if (E.le(ratio, 0) or not E.contains([1, 2], level)): return var held: Dictionary = target.binds.get(owner_id, {}) - var physical: Dictionary = world.ledger.physical_amounts(target) var rocky: Variant = 0 for id in held.keys(): if ((not world.catalog.entities.has(id)) or E.ne(world.catalog.entities[id].group_as, "Planet")): @@ -82,8 +81,10 @@ func destroy_planets(target: Dictionary, owner_id: String, ratio: Variant, level target.destruction_remainders[key] = 0 if E.le(destroyed, 0): continue + var physical: Variant = world.ledger.physical_amount(target, id) held[id] = E.subtract(held[id], destroyed) - world.batches.scale_batches(target, id, E.subtract(1, E.divide(destroyed, physical[id]))) + if E.gt(physical, 0): + world.batches.scale_batches(target, id, E.subtract(1, E.divide(destroyed, physical))) if E.eq(level, 1): rocky = E.add(rocky, destroyed) if E.gt(rocky, 0):