Add new music tracks to the sound directory
- Added cosmic_unicorns.ogg - Added funky_eldritch.ogg - Added meditative_beings.ogg - Added owniverse_main_theme.ogg These tracks enhance the audio experience of the project.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
- `quantity_math.gd`: нормализация положительных весов, ограниченное целочисленное распределение методом наибольших остатков, распределение с накоплением ошибки округления, накопление дробного выпуска и минимальный целый размер производственной партии.
|
||||
- `entity_catalog.gd`: загрузка определений, индексы групп и событий, списки автоматических производителей и разблокируемых сущностей.
|
||||
|
||||
Математика не обращается к дереву сцены, Global или Owniverse. Методы накопления изменяют переданный словарь остатков; остальные методы возвращают результат. Для распределения `split` количество должно быть целым, веса — неотрицательными. Равные остатки разрешаются по строковому ID. Старые поправки `snappedf` удалены: количества считаются целочисленно, дробные вычисления выполняет [модуль точной арифметики](math/README.md). Там же описаны границы точности и формат сохранения 3.
|
||||
Математика не обращается к дереву сцены, Global или Owniverse. Методы накопления изменяют переданный словарь остатков; остальные методы возвращают результат. Для распределения `split` количество должно быть целым, веса — неотрицательными. Равные остатки разрешаются по строковому ID. Количества считаются целочисленно в диапазоне E27, дробные остатки хранятся в `float`. [Модуль арифметики](math/README.md) описывает допуск дробных переносов, точные отношения и формат сохранения 4.
|
||||
|
||||
Каталог получает словарь исходных строк через `load_definitions(rows)`, вызываемый один раз для нового экземпляра. Конструктор OwniverseEntity по-прежнему использует настройки Global. Каталог не хранит количества, партии или состояние интерфейса. Определения пока остаются Node: освобождает их владелец мира, как до рефакторинга.
|
||||
|
||||
|
||||
@@ -18,9 +18,10 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
|
||||
var automatic: Dictionary = {}
|
||||
for id in quantities:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
var products: Dictionary = yearly_products(entity)
|
||||
if products.is_empty(): continue
|
||||
var object_years: Variant = world.batches.object_years(previous, id, quantities[id], from_year, to_year)
|
||||
var auto_share: Variant = E.clamp_value(entity.auto, 0, 1)
|
||||
var products: Dictionary = yearly_products(entity)
|
||||
for product in products:
|
||||
var amount: Variant = E.multiply(E.multiply(object_years, products[product]), WorldParameters.production(previous, entity))
|
||||
basic[product] = E.add(basic.get(product, 0), E.multiply(amount, (E.subtract(1, auto_share))))
|
||||
@@ -146,7 +147,7 @@ func produce_brown_dwarfs(target: Dictionary, new_hydrogen: Variant, from_year:
|
||||
var cost: Variant = entity.cost.get("H", 0)
|
||||
if E.le(cost, 0):
|
||||
return
|
||||
var quota: Variant = E.add(target.auto_star_deficits.get("HBD", 0), E.divide(E.multiply(E.maximum(0, new_hydrogen), E.of("0.001")), cost))
|
||||
var quota: Variant = E.add(target.auto_star_deficits.get("HBD", 0), E.mul_div(E.maximum(0, new_hydrogen), E.of("0.001"), cost))
|
||||
var fuel: Variant = E.maximum(0, target.auto_star_deficits.get("HBD_fuel", 0))
|
||||
var available: Variant = E.minimum(target.auto_resources.get("H", 0), world.ledger.available(target, "H"))
|
||||
quota = E.minimum(quota, E.divide((E.add(available, fuel)), cost))
|
||||
|
||||
@@ -231,4 +231,4 @@ static func effective_span(world, target: Dictionary, id: String, rules: Diction
|
||||
var weighted: Variant = population
|
||||
for home in target.civilization_homes.get(id, {}):
|
||||
weighted = E.add(weighted, E.multiply(target.civilization_homes[id][home], (E.subtract(E.maximum(0, world.catalog.entities[home].civ_bonus), 1))))
|
||||
return (E.divide(E.multiply(base, population), weighted) if E.gt(weighted, 0) else base)
|
||||
return (E.mul_div(base, population, weighted) if E.gt(weighted, 0) else base)
|
||||
|
||||
@@ -66,7 +66,7 @@ static func variables(world, global: Node, id: String, rate: Variant = 0.0) -> D
|
||||
var h: Variant = world.state.amount.get("H", 0)
|
||||
var mixture: String = "—"
|
||||
if E.gt(E.add(he, h), 0):
|
||||
mixture = E.add(E.add(E.add(E.add("He: ", number(E.divide(E.multiply(100, he), (E.add(he, h))), 2)), "% / H: "), number(E.divide(E.multiply(100, h), (E.add(he, h))), 2)), "%")
|
||||
mixture = E.add(E.add(E.add(E.add("He: ", number(E.mul_div(100, he, (E.add(he, h))), 2)), "% / H: "), number(E.mul_div(100, h, (E.add(he, h))), 2)), "%")
|
||||
var environment: Dictionary = world.environment.stellar_environment(world.state)
|
||||
return {"amount": amount, "colored costs": costs(world, global, id), "lifespan": (number(world.catalog.entities[id].lifespan) if world.catalog.entities.has(id) else "—"), "voids": number(voids), "he / h": E.add("\n", mixture), "metallicity": E.add(number(E.multiply(environment.metallicity, 100), 2), "%"), "энергия": number(world.state.energy), "плотность": number(environment.density, 6), "лет в секунду": number(world.state.speed_in_year), "amount1": "—", "amount2": "—", "amount3": "—", "amount4": "—"}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ func life_products(target: Dictionary, entity: OwniverseEntity, count: Variant)
|
||||
target.life_remainders = {}
|
||||
for channel in ["progress", "degradation"]:
|
||||
var key: String = E.add(E.add(entity.id, ":"), channel)
|
||||
var expected: Variant = E.add(E.divide(E.multiply(count, probabilities[channel]), probability_sum), target.life_remainders.get(key, 0))
|
||||
var expected: Variant = E.add(E.mul_div(count, probabilities[channel], probability_sum), target.life_remainders.get(key, 0))
|
||||
target.life_remainders[key] = expected
|
||||
pending[channel] = E.floor_value(expected)
|
||||
var assigned: Dictionary = QuantityMath.allocate_whole(pending, count)
|
||||
@@ -31,7 +31,7 @@ func life_products(target: Dictionary, entity: OwniverseEntity, count: Variant)
|
||||
var requests: Dictionary = {}
|
||||
for id in weights:
|
||||
var key: String = E.add(E.add(entity.id, ":fall:"), id)
|
||||
var expected: Variant = E.add(E.divide(E.multiply(assigned.degradation, weights[id]), weight_sum), target.life_remainders.get(key, 0))
|
||||
var expected: Variant = E.add(E.mul_div(assigned.degradation, weights[id], weight_sum), target.life_remainders.get(key, 0))
|
||||
target.life_remainders[key] = expected
|
||||
requests[id] = E.maximum(0, E.floor_value(expected))
|
||||
var falls: Dictionary = QuantityMath.allocate_whole(requests, assigned.degradation)
|
||||
|
||||
@@ -74,7 +74,7 @@ func plan_smbh(desired_amount: Variant) -> Dictionary:
|
||||
var order: Array = []
|
||||
var consumed_mass: Variant = 0
|
||||
for id in distribution:
|
||||
var expected: Variant = E.divide(E.multiply(requested, distribution[id]), world.catalog.entities[id].mass)
|
||||
var expected: Variant = E.mul_div(requested, distribution[id], world.catalog.entities[id].mass)
|
||||
taken[id] = E.minimum(E.floor_value(expected), E.floor_value(world.state.amount[id]))
|
||||
consumed_mass = E.add(consumed_mass, E.multiply(taken[id], world.catalog.entities[id].mass))
|
||||
order.append({"id": id, "remainder": E.subtract(expected, taken[id])})
|
||||
@@ -173,9 +173,9 @@ func production_step(entity: OwniverseEntity) -> Variant:
|
||||
return QuantityMath.production_step(E.add(entity.cost.values(), entity.bind.values()))
|
||||
|
||||
|
||||
func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant = 1.0E150) -> Variant:
|
||||
func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant = null) -> Variant:
|
||||
if E.eq(entity.id, "SMBH"):
|
||||
return plan_smbh(desired_amount).amount
|
||||
return plan_smbh(smbh_available() if desired_amount == null else desired_amount).amount
|
||||
var requirements: Dictionary = {}
|
||||
for resource in entity.cost:
|
||||
requirements[resource] = E.maximum(0, entity.cost[resource])
|
||||
@@ -189,7 +189,7 @@ func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant =
|
||||
# No recipe means unavailable for production, not unlimited free objects.
|
||||
if (not has_requirements):
|
||||
return 0
|
||||
var available: Variant = E.maximum(0, desired_amount)
|
||||
var available: Variant = INF if desired_amount == null else E.maximum(0, desired_amount)
|
||||
for resource in requirements:
|
||||
if E.gt(requirements[resource], 0):
|
||||
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), requirements[resource])))
|
||||
|
||||
@@ -1,82 +1,96 @@
|
||||
# Числа симуляции
|
||||
|
||||
Количество объектов, запасы, энергия, расходы, привязки и статистика считаются
|
||||
целочисленно, без перехода через `float`. Значения до `10^27` сохраняют каждую
|
||||
единицу; промежуточные произведения могут превышать этот диапазон.
|
||||
`exact.gd` сохраняет единый API `E` для симуляции и интерфейса. Целые числа,
|
||||
помещающиеся в `int64`, остаются обычными `int`. Остальные значения хранятся
|
||||
в `exact_value.gd`:
|
||||
|
||||
`big_integer.gd` хранит модуль числа массивом разрядов по основанию `10^9`,
|
||||
младший разряд первым. Переносы при умножении не переполняют `int64`.
|
||||
Деление использует нормализованную оценку цифры частного.
|
||||
```
|
||||
polarity × (high × 10^14 + low + fraction)
|
||||
```
|
||||
|
||||
`exact.gd` — единый API арифметики. Малые значения остаются обычными `int`,
|
||||
большие числа и дроби представлены `exact_value.gd`: знак, целый числитель,
|
||||
целый знаменатель. Дроби сокращаются. Операции создают новые значения;
|
||||
поля `ExactValue` и массивы его разрядов нельзя изменять после создания.
|
||||
`high` и `low` — целые блоки от 0 до 10^14−1, `fraction` — `float` от 0 до 1,
|
||||
`polarity` — знак. Значения неизменяемы после создания. Диапазон включает весь
|
||||
порядок E27: модуль целой части меньше 10^28. Каждая единица целой части
|
||||
сохраняется, в том числе при добавлении и вычитании единицы около 10^27.
|
||||
Выход за диапазон проверяется утверждениями; насыщения или обрезания запасов нет.
|
||||
|
||||
Числители и знаменатели в состоянии больше не хранятся. НОД при обычных
|
||||
арифметических операциях не вычисляется; обход состояния для ограничения дробей
|
||||
больше не нужен. `bounded` и `bound_state` оставлены как совместимые пустые операции.
|
||||
|
||||
## Операции
|
||||
|
||||
Сложение, вычитание и сравнение работают непосредственно с блоками.
|
||||
Для умножения и деления `big_integer.gd` используется как временный вычислитель
|
||||
с безопасными разрядами по основанию 10^9. Результат сразу возвращается в
|
||||
фиксированную структуру. Размер временных дробей определяется входными float,
|
||||
а не растёт от шага к шагу.
|
||||
|
||||
`E.mul_div(a, b, denominator)` вычисляет отношение за одну операцию. Это нужно
|
||||
для распределений: произведение двух количеств может превышать E27, хотя итог
|
||||
деления укладывается в диапазон. Промежуточное произведение не становится
|
||||
значением симуляции. Целое частное вычисляется до перевода остатка в float.
|
||||
|
||||
```gdscript
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
var stock = E.of("1e27") # Строка, не литерал float 1e27!
|
||||
stock = E.subtract(stock, 1) # 999999999999999999999999999
|
||||
var price = E.of("0.125") # Точно 1/8
|
||||
var cost = E.multiply(price, 8) # Точно 1
|
||||
if E.ge(stock, cost):
|
||||
stock = E.subtract(stock, cost)
|
||||
var stock = E.of("1e27") # Большое целое вводится строкой.
|
||||
stock = E.subtract(stock, 1)
|
||||
var share = E.mul_div(stock, 15, 100) # Точные 15% целого запаса.
|
||||
var whole_share = E.floor_value(share)
|
||||
```
|
||||
|
||||
Использовать `E.add/subtract/multiply/divide`, `E.eq/lt/le/gt/ge`,
|
||||
`E.floor_value/ceil_value/round_value`. Перегрузки операторов в GDScript нет:
|
||||
обычные `+`, `==`, `min`, `float(...)` для чисел симуляции не подходят.
|
||||
Сигнатуры принимают `Variant`, а не `float`. `E.integer` допускается только
|
||||
для значений в диапазоне `int64`, например индексов и состояния генератора.
|
||||
Для поиска чисел в массиве используется `E.contains`, поскольку сравнение
|
||||
объектов по ссылке не является сравнением числовых значений.
|
||||
`E.floor_value/ceil_value/round_value`. Обычные операторы над объектами
|
||||
`ExactValue` не подходят. `E.integer` допускается только для значений int64;
|
||||
`E.approx` — явный переход к приближённому float.
|
||||
|
||||
## Границы точности
|
||||
## Дробная точность
|
||||
|
||||
- Десятичные параметры CSV разбираются из строк точно. Стоимости, проверка
|
||||
доступности и размер целой производственной партии не округляются.
|
||||
- Суммы распределений и переносы целых объектов сохраняются точно.
|
||||
- Промежуточная рациональная арифметика точна. Перед публикацией следующего
|
||||
состояния знаменатели сохранённых дробей ограничиваются `10^36`.
|
||||
Если знаменатель больше, значение усекается к нулю с ошибкой меньше
|
||||
`10^-36`. Целые числа эта операция не меняет. Это предотвращает рост
|
||||
знаменателей при многократном накоплении меняющихся долей. Такая точность
|
||||
относится к каждой операции ограничения, а не является обещанием нулевой
|
||||
суммарной ошибки при бесконечном числе циклов.
|
||||
- Логарифмы, корни, нецелые степени и нормальная случайная выборка остаются
|
||||
приближёнными. Результаты трансцендентных функций переводятся обратно в
|
||||
рациональные коэффициенты с 12 знаками после точки. Статистическая выборка
|
||||
не обеспечивает равномерный выбор каждого отдельного целого около `10^27`;
|
||||
последующий учёт выбранного количества и остатка точен.
|
||||
- Возраст хранится в целых тиках (`10^9` тиков на год). Малый остаток кадрового
|
||||
времени остаётся `float` и переносится между кадрами; большой возраст
|
||||
никогда не складывается с малым приращением через `float`.
|
||||
- Интерфейс преобразует в `float` только координаты индикаторов. Текст чисел,
|
||||
доступное количество и значения кнопок формируются из точных чисел.
|
||||
Дробная часть приближённая. Для временного умножения/деления она преобразуется
|
||||
в десятичную дробь с 16 значащими цифрами; нормализация сохраняет значащие цифры
|
||||
малых коэффициентов. Десятичные стоимости вроде 0.125 и 0.0000000000001
|
||||
сохраняют корректный размер производственной партии. НОД применяется только
|
||||
при определении знаменателя стоимости в `production_step`.
|
||||
|
||||
`E.of(float)` сохраняет фактическое двоичное значение входного числа: он
|
||||
не может восстановить единицы, уже потерянные предыдущими вычислениями.
|
||||
При сложении двух ненулевых дробных частей перенос около целой границы
|
||||
стабилизируется с допуском 10^-12. Это предотвращает задержку редких событий
|
||||
из-за накопленной ошибки float. Допуск не применяется к целым блокам или
|
||||
округлению частного при делении. Суммы распределённых целых объектов проверяются
|
||||
строго, без допуска.
|
||||
|
||||
При умножении большого количества на приближённый коэффициент ошибка самого
|
||||
коэффициента усиливается: float-доля не обещает точности результата до единицы.
|
||||
Известные точные отношения нужно передавать через `mul_div(count, numerator,
|
||||
denominator)`, не вычисляя коэффициент заранее.
|
||||
|
||||
Возраст хранится как целые годы и дробный остаток; в наносекундные тики
|
||||
переводится только дробная часть года. Полный возраст больше не умножается на
|
||||
10^9. Остаток кадрового времени переносится отдельно. Геометрический объём
|
||||
пустот может превышать E27 из-за множителя 10^22: он вычисляется в float и
|
||||
используется только для приближённых коэффициентов плотности и бластов.
|
||||
|
||||
Форматирование не создаёт больших промежуточных значений симуляции: например,
|
||||
округлённая подпись `1.00E+28` допустима для количества чуть меньше 10^28.
|
||||
|
||||
## Сохранение
|
||||
|
||||
Формат сохранения — **3**, версия состояния — **0.2.0**. На главном потоке
|
||||
`E.encode` строит независимый снимок, затем существующий фоновый писатель
|
||||
сохраняет его. Большие целые и дроби кодируются как `{"$exact": "число"}`;
|
||||
для дробей используется строка `числитель/знаменатель`. Целые за пределами
|
||||
точного диапазона JSON/float тоже кодируются строкой. Зашифрованный файл и
|
||||
диагностический JSON используют один снимок. После проверки `E.decode`
|
||||
восстанавливает числа, без загрузки сериализованных объектов GDScript.
|
||||
Формат сохранения — **4**, версия состояния — **0.3.0**. Большие целые
|
||||
кодируются как `{"$exact": "целое число"}`. Блоки с дробью кодируются как
|
||||
`{"$fixed": ["high", "low", "float bytes in hex", sign]}`. Восемь байтов float
|
||||
сохраняются без потери точности JSON, включая очень малые остатки.
|
||||
Зашифрованный файл и диагностический JSON используют один снимок.
|
||||
|
||||
Миграции старой арифметики нет. Старый формат несовместим: существующий
|
||||
механизм загрузки сохраняет его в `.incompatible.bak` и начинает новую игру.
|
||||
Старое сохранение игрока не используется тестами и не изменялось при разработке.
|
||||
Миграции нет. Сохранение прежнего формата при обычной загрузке архивируется
|
||||
существующим механизмом `.incompatible.bak`; начинается новая игра.
|
||||
Тесты не загружают и не перезаписывают рабочее сохранение игрока.
|
||||
|
||||
## Проверки
|
||||
|
||||
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
|
||||
|
||||
Независимый эталон `tests/fixtures/exact_oracle.json` получен через Python
|
||||
`fractions.Fraction` (120 пар, seed 4321). Тест сравнивает строковые результаты
|
||||
суммы, разности, произведения, деления и округления вниз. Дополнительно
|
||||
проверяются границы `int64`, JSON, рецепты, распределения, жизнь при `10^27`,
|
||||
единичный расход энергии, тик времени и 100 циклов с меняющимися долями звёзд.
|
||||
`fractions.Fraction` (160 пар, seed 4321). Произведения находятся внутри E27;
|
||||
дробные результаты сравниваются с абсолютным допуском 10^-12, целые — точно.
|
||||
Дополнительно проверяются границы int64/E27, единичные изменения, точные
|
||||
проценты, объединённое умножение с делением, JSON, дробные рецепты,
|
||||
распределения, жизнь, энергия, время, форматирование и 100 циклов симуляции.
|
||||
|
||||
+152
-114
@@ -1,56 +1,76 @@
|
||||
extends RefCounted
|
||||
## Exact signed rational arithmetic. Native small integers are the fast path.
|
||||
## Fixed whole blocks and a float remainder. Native integers are the fast path.
|
||||
const Big = preload("res://world/owniverse/math/big_integer.gd")
|
||||
const Value = preload("res://world/owniverse/math/exact_value.gd")
|
||||
const SMALL: int = 1000000000
|
||||
const MAX_INT: int = 9223372036854775807
|
||||
const MIN_INT: int = -9223372036854775807 - 1
|
||||
const CARRY_SCALE: Array = [0, 0, 0, 0, 1] # 10^36
|
||||
const BLOCK: int = 100000000000000 # 10^14
|
||||
const LIMIT: Array = [0, 0, 0, 10] # 10^28, exclusive
|
||||
|
||||
static func make(n: Array, d: Array = [1], polarity: int = 1):
|
||||
assert(d != [0], "Zero denominator")
|
||||
if n == [0]: return 0
|
||||
if d != [1]:
|
||||
var common: Array = Big.gcd(n, d)
|
||||
n = Big.divmod(n, common)[0]
|
||||
d = Big.divmod(d, common)[0]
|
||||
return _make_reduced(n, d, 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))
|
||||
return _from_whole(qr[0], fraction, polarity)
|
||||
|
||||
## Internal: operands already reduced; do not repeat Euclid's algorithm.
|
||||
static func _make_reduced(n: Array, d: Array, polarity: int):
|
||||
if n == [0]: return 0
|
||||
if d == [1] and (n.size() == 1 or (n.size() == 2 and n[1] < SMALL)):
|
||||
return polarity * (n[0] + (n[1] * SMALL if n.size() == 2 else 0))
|
||||
if d == [1] and n.size() == 3:
|
||||
var limit: Array = [854775807 if polarity > 0 else 854775808, 223372036, 9]
|
||||
if Big.compare(n, limit) <= 0:
|
||||
return (("-" if polarity < 0 else "") + Big.decimal(n)).to_int()
|
||||
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))
|
||||
return _compose(high, low, fraction, polarity)
|
||||
|
||||
static func _compose(high: int, low: int, fraction: float, polarity: int):
|
||||
if fraction >= 1.0:
|
||||
low += int(floorf(fraction))
|
||||
fraction -= floorf(fraction)
|
||||
if fraction < 0.0:
|
||||
low -= 1
|
||||
fraction += 1.0
|
||||
if low >= BLOCK:
|
||||
high += low / BLOCK
|
||||
low %= BLOCK
|
||||
elif low < 0:
|
||||
high -= 1
|
||||
low += BLOCK
|
||||
assert(high >= 0 and high < BLOCK, "Number exceeds E27 range")
|
||||
if fraction == 0.0 and high <= 92233:
|
||||
var prefix: int = high * BLOCK
|
||||
if low <= MAX_INT - prefix: return polarity * (prefix + low)
|
||||
if polarity < 0 and low == MAX_INT - prefix + 1: return MIN_INT
|
||||
var result = Value.new()
|
||||
result.numerator = n
|
||||
result.denominator = d
|
||||
result.high = high
|
||||
result.low = low
|
||||
result.fraction = fraction
|
||||
result.polarity = polarity
|
||||
return result
|
||||
|
||||
static func _blocks(value) -> Array:
|
||||
value = of(value)
|
||||
if value is Value: return [value.high, value.low, value.fraction, value.polarity]
|
||||
# Signed division also handles MIN_INT without overflowing abs().
|
||||
return [absi(value / BLOCK), absi(value % BLOCK), 0.0, -1 if value < 0 else 1]
|
||||
|
||||
static func of(value):
|
||||
if value is Value or value is int: return value
|
||||
if value is float:
|
||||
if not is_finite(value): return value
|
||||
if value == floorf(value) and absf(value) < 1e18: return int(value)
|
||||
# Imports the actual binary float, not an invented decimal rounding.
|
||||
var data := PackedByteArray()
|
||||
data.resize(8)
|
||||
data.encode_double(0, value)
|
||||
var bits: int = data.decode_s64(0)
|
||||
var exponent: int = (bits >> 52) & 2047
|
||||
var mantissa: int = bits & 4503599627370495
|
||||
if exponent: mantissa += 4503599627370496
|
||||
var power: int = exponent - 1075 if exponent else -1074
|
||||
var n: Array = Big.parse(str(mantissa))
|
||||
var d: Array = [1]
|
||||
for i in absi(power):
|
||||
if power > 0: n = Big.multiply(n, [2])
|
||||
else: d = Big.multiply(d, [2])
|
||||
return make(n, d, -1 if value < 0 else 1)
|
||||
var magnitude: float = absf(value)
|
||||
return _from_whole(Big.parse("%.0f" % floorf(magnitude)), magnitude - floorf(magnitude), -1 if value < 0 else 1)
|
||||
var text: String = str(value).strip_edges().to_lower()
|
||||
if text == "": return 0
|
||||
if "/" in text:
|
||||
@@ -72,7 +92,17 @@ 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]
|
||||
return [value.numerator, value.denominator, value.polarity]
|
||||
var whole: Array = Big.parse(str(value.high) + "%014d" % 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))
|
||||
return [Big.add(Big.multiply(whole, d), n), d, value.polarity]
|
||||
|
||||
static func numeric(value) -> bool:
|
||||
return value is int or value is float or value is Value
|
||||
@@ -82,9 +112,7 @@ static func finite(value) -> bool:
|
||||
|
||||
static func approx(value) -> float:
|
||||
if not value is Value: return float(value)
|
||||
var a: String = Big.decimal(value.numerator)
|
||||
var b: String = Big.decimal(value.denominator)
|
||||
return value.polarity * float(a.left(16)) / float(b.left(16)) * pow(10.0, maxi(0, a.length() - 16) - maxi(0, b.length() - 16))
|
||||
return value.polarity * (float(value.high) * BLOCK + float(value.low) + value.fraction)
|
||||
|
||||
static func compare(a, b) -> int:
|
||||
if a is int and b is int: return 0 if a == b else (1 if a > b else -1)
|
||||
@@ -92,10 +120,12 @@ static func compare(a, b) -> int:
|
||||
var x: float = approx(a)
|
||||
var y: float = approx(b)
|
||||
return 0 if x == y else (1 if x > y else -1)
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
if x[2] != y[2]: return 1 if x[2] > y[2] else -1
|
||||
return x[2] * Big.compare(Big.multiply(x[0], y[1]), Big.multiply(y[0], x[1]))
|
||||
var x: Array = _blocks(a)
|
||||
var y: Array = _blocks(b)
|
||||
if x[3] != y[3]: return 1 if x[3] > y[3] else -1
|
||||
for i in 3:
|
||||
if x[i] != y[i]: return x[3] * (1 if x[i] > y[i] else -1)
|
||||
return 0
|
||||
|
||||
static func eq(a, b) -> bool:
|
||||
if a == null: return b == null
|
||||
@@ -134,69 +164,68 @@ static func add(a, b):
|
||||
if a is int and b is int:
|
||||
if (b >= 0 and a <= MAX_INT - b) or (b < 0 and a >= MIN_INT - b): return a + b
|
||||
if not finite(a) or not finite(b): return approx(a) + approx(b)
|
||||
if a is int and a == 0: return of(b)
|
||||
if b is int and b == 0: return of(a)
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
var common: Array = Big.gcd(x[1], y[1])
|
||||
var xd: Array = Big.divmod(x[1], common)[0]
|
||||
var yd: Array = Big.divmod(y[1], common)[0]
|
||||
var left: Array = Big.multiply(x[0], yd)
|
||||
var right: Array = Big.multiply(y[0], xd)
|
||||
var denominator: Array = Big.multiply(xd, y[1])
|
||||
var numerator: Array
|
||||
var polarity: int = x[2]
|
||||
if x[2] == y[2]:
|
||||
numerator = Big.add(left, right)
|
||||
else:
|
||||
var order: int = Big.compare(left, right)
|
||||
numerator = Big.subtract(left, right) if order >= 0 else Big.subtract(right, left)
|
||||
polarity = x[2] if order >= 0 else y[2]
|
||||
# Reduced inputs can only share a remaining factor from gcd(d1, d2).
|
||||
# Testing the entire new denominator repeats expensive long division.
|
||||
var reduction: Array = Big.gcd(numerator, common)
|
||||
return _make_reduced(Big.divmod(numerator, reduction)[0], Big.divmod(denominator, reduction)[0], polarity)
|
||||
var x: Array = _blocks(a)
|
||||
var y: Array = _blocks(b)
|
||||
if x[3] == y[3]:
|
||||
var fraction: float = x[2] + y[2]
|
||||
# Repeated fractional accumulation may land just below an integer.
|
||||
# This tolerance only applies to the fractional carry, never whole blocks.
|
||||
if x[2] > 0.0 and y[2] > 0.0 and fraction > 0.5 and absf(fraction - roundf(fraction)) <= 1e-12:
|
||||
fraction = roundf(fraction)
|
||||
return _compose(x[0] + y[0], x[1] + y[1], fraction, x[3])
|
||||
var order: int = 0
|
||||
for i in 3:
|
||||
if x[i] != y[i]:
|
||||
order = 1 if x[i] > y[i] else -1
|
||||
break
|
||||
if order == 0: return 0
|
||||
if order < 0:
|
||||
var tmp: Array = x
|
||||
x = y
|
||||
y = tmp
|
||||
return _compose(x[0] - y[0], x[1] - y[1], x[2] - y[2], x[3])
|
||||
|
||||
static func subtract(a, b):
|
||||
if not numeric(a) or not numeric(b): return a - b
|
||||
if a is int and b is int:
|
||||
if (b >= 0 and a >= MIN_INT + b) or (b < 0 and a <= MAX_INT + b): return a - b
|
||||
if not finite(a) or not finite(b): return approx(a) - approx(b)
|
||||
if b is int and b == 0: return of(a)
|
||||
var y: Array = parts(b)
|
||||
return add(a, _make_reduced(y[0], y[1], -y[2]))
|
||||
var y: Array = _blocks(b)
|
||||
return add(a, _compose(y[0], y[1], y[2], -y[3]))
|
||||
|
||||
static func multiply(a, b):
|
||||
if not numeric(a) or not numeric(b): return a * b
|
||||
if not finite(a) or not finite(b): return approx(a) * approx(b)
|
||||
if (a is int and a == 0) or (b is int and b == 0): return 0
|
||||
if a is int and a == 1: return of(b)
|
||||
if b is int and b == 1: return of(a)
|
||||
if eq(a, 0) or eq(b, 0): return 0
|
||||
if eq(a, 1): return of(b)
|
||||
if eq(b, 1): return of(a)
|
||||
if a is int and b is int and a != MIN_INT and b != MIN_INT:
|
||||
if absi(a) <= MAX_INT / absi(b): return a * b
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
var g1: Array = Big.gcd(x[0], y[1])
|
||||
var g2: Array = Big.gcd(y[0], x[1])
|
||||
return _make_reduced(Big.multiply(Big.divmod(x[0], g1)[0], Big.divmod(y[0], g2)[0]), Big.multiply(Big.divmod(x[1], g2)[0], Big.divmod(y[1], g1)[0]), x[2] * y[2])
|
||||
return make(Big.multiply(x[0], y[0]), Big.multiply(x[1], y[1]), x[2] * y[2])
|
||||
|
||||
## 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))
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
var z: Array = parts(divisor)
|
||||
assert(z[0] != [0], "Division by zero")
|
||||
return make(Big.multiply(Big.multiply(x[0], y[0]), z[1]), Big.multiply(Big.multiply(x[1], y[1]), z[0]), x[2] * y[2] * z[2])
|
||||
|
||||
static func divide(a, b):
|
||||
if not numeric(a) or not numeric(b): return a / b
|
||||
if not finite(a) or not finite(b): return approx(a) / approx(b)
|
||||
if b is int and b == 1: return of(a)
|
||||
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
|
||||
var y: Array = parts(b)
|
||||
assert(y[0] != [0], "Exact division by zero")
|
||||
return multiply(a, _make_reduced(y[1], y[0], y[2]))
|
||||
return mul_div(a, 1, b)
|
||||
|
||||
static func floor_value(a):
|
||||
if not finite(a): return a
|
||||
if a is int: return a
|
||||
var x: Array = parts(a)
|
||||
var qr: Array = Big.divmod(x[0], x[1])
|
||||
if x[2] < 0 and qr[1] != [0]: qr[0] = Big.add(qr[0], [1])
|
||||
return make(qr[0], [1], x[2])
|
||||
if not finite(a) or a is int: return a
|
||||
var x: Array = _blocks(a)
|
||||
return _compose(x[0], x[1] + (1 if x[3] < 0 and x[2] > 0 else 0), 0.0, x[3])
|
||||
|
||||
static func ceil_value(a): return neg(floor_value(neg(a)))
|
||||
static func round_value(a): return multiply(-1 if lt(a, 0) else 1, floor_value(add(absolute(a), of("0.5"))))
|
||||
@@ -207,24 +236,13 @@ static func integer(a) -> int:
|
||||
assert(result is int, "Value does not fit native int64")
|
||||
return result
|
||||
|
||||
## Bound persistent fractions, never integer quantities or recipe arithmetic.
|
||||
## Truncation towards zero changes a value by less than 10^-36.
|
||||
static func bounded(value):
|
||||
if not value is Value or Big.compare(value.denominator, CARRY_SCALE) <= 0:
|
||||
return value
|
||||
var units: Array = Big.divmod(Big.multiply(value.numerator, CARRY_SCALE), value.denominator)[0]
|
||||
return make(units, CARRY_SCALE, value.polarity)
|
||||
## Kept as no-op API: float remainder storage is already bounded.
|
||||
static func bounded(value): return value
|
||||
|
||||
## Only call on a newly owned state snapshot, never on a published old state.
|
||||
static func bound_state(value):
|
||||
if value is Value: return bounded(value)
|
||||
if value is Dictionary:
|
||||
for key in value: value[key] = bound_state(value[key])
|
||||
elif value is Array:
|
||||
for i in value.size(): value[i] = bound_state(value[i])
|
||||
return value
|
||||
static func bound_state(value): return value
|
||||
|
||||
static func power(a, b):
|
||||
if finite(b) and lt(b, 0): return of(pow(approx(a), approx(b)))
|
||||
if finite(b) and eq(b, floor_value(b)) and le(absolute(b), 10000):
|
||||
var exponent: int = integer(absolute(b))
|
||||
var result = 1
|
||||
@@ -232,7 +250,7 @@ static func power(a, b):
|
||||
if exponent % 2: result = multiply(result, a)
|
||||
exponent /= 2
|
||||
if exponent: a = multiply(a, a)
|
||||
return divide(1, result) if lt(b, 0) else result
|
||||
return result
|
||||
return coefficient(pow(approx(a), approx(b)))
|
||||
|
||||
static func coefficient(value: float):
|
||||
@@ -253,21 +271,27 @@ static func contains(container, value) -> bool:
|
||||
return value in container
|
||||
|
||||
static func fixed(value, digits: int = 0) -> String:
|
||||
var scale = power(10, digits)
|
||||
var rounded = round_value(multiply(value, scale))
|
||||
var text: String = str(absolute(rounded))
|
||||
if not finite(value): return str(value)
|
||||
var x: Array = parts(value)
|
||||
var scale: Array = Big.parse("1" + "0".repeat(digits))
|
||||
var qr: Array = Big.divmod(Big.multiply(x[0], scale), x[1])
|
||||
if Big.compare(Big.multiply(qr[1], [2]), x[1]) >= 0: qr[0] = Big.add(qr[0], [1])
|
||||
var text: String = Big.decimal(qr[0])
|
||||
if digits:
|
||||
text = text.lpad(digits + 1, "0")
|
||||
text = text.left(text.length() - digits) + "." + text.right(digits)
|
||||
return ("-" if lt(rounded, 0) else "") + text
|
||||
return ("-" if x[2] < 0 and qr[0] != [0] else "") + text
|
||||
|
||||
static func scientific(value, digits: int = 4) -> String:
|
||||
if eq(value, 0): return fixed(0, digits) + "E+0"
|
||||
var terms: Array = parts(absolute(value))
|
||||
var exponent: int = Big.decimal(terms[0]).length() - Big.decimal(terms[1]).length()
|
||||
if lt(absolute(value), power(10, exponent)): exponent -= 1
|
||||
var mantissa = divide(value, power(10, exponent))
|
||||
if ge(absolute(round_value(multiply(mantissa, power(10, digits)))), multiply(10, power(10, digits))):
|
||||
var scale = of("1e%d" % exponent)
|
||||
if lt(absolute(value), scale):
|
||||
exponent -= 1
|
||||
scale = of("1e%d" % exponent)
|
||||
var mantissa = divide(value, scale)
|
||||
if float(fixed(absolute(mantissa), digits)) >= 10.0:
|
||||
mantissa = divide(mantissa, 10)
|
||||
exponent += 1
|
||||
return fixed(mantissa, digits) + ("E+%d" if exponent >= 0 else "E%d") % exponent
|
||||
@@ -285,7 +309,11 @@ static func grouped(value, digits: int = 0) -> String:
|
||||
return ("-" if lt(value, 0) else "") + integer_text + result + ("." + parts_text[1] if parts_text.size() > 1 else "")
|
||||
|
||||
static func encode(value):
|
||||
if value is Value: return {"$exact": str(value)}
|
||||
if value is Value:
|
||||
var bytes := PackedByteArray()
|
||||
bytes.resize(8)
|
||||
bytes.encode_double(0, value.fraction)
|
||||
return {"$fixed": [str(value.high), str(value.low), bytes.hex_encode(), value.polarity]}
|
||||
if value is int and (value > 9007199254740991 or value < -9007199254740991):
|
||||
return {"$exact": str(value)}
|
||||
if value is Dictionary:
|
||||
@@ -300,6 +328,9 @@ static func encode(value):
|
||||
|
||||
static func decode(value):
|
||||
if value is Dictionary:
|
||||
if value.size() == 1 and value.has("$fixed"):
|
||||
var fields: Array = value["$fixed"]
|
||||
return _compose(int(fields[0]), int(fields[1]), fields[2].hex_decode().decode_double(0), int(fields[3]))
|
||||
if value.size() == 1 and value.has("$exact"): return of(value["$exact"])
|
||||
var result: Dictionary = {}
|
||||
for k in value: result[k] = decode(value[k])
|
||||
@@ -311,22 +342,29 @@ static func decode(value):
|
||||
return value
|
||||
|
||||
static func valid_encoded(value) -> bool:
|
||||
if value is float: return is_finite(value) and absf(value) < 1e28
|
||||
if value is Dictionary:
|
||||
if value.has("$fixed"):
|
||||
if value.size() != 1 or not value["$fixed"] is Array: return false
|
||||
var fields: Array = value["$fixed"]
|
||||
if fields.size() != 4: return false
|
||||
for i in 2:
|
||||
if not fields[i] is String or not fields[i].is_valid_int() or fields[i].length() > 14: return false
|
||||
if int(fields[i]) < 0 or int(fields[i]) >= BLOCK: return false
|
||||
if not fields[2] is String or fields[2].length() != 16 or not fields[2].is_valid_hex_number(): return false
|
||||
var fraction: float = fields[2].hex_decode().decode_double(0)
|
||||
return is_finite(fraction) and fraction >= 0.0 and fraction < 1.0 and fields[3] in [-1, 1]
|
||||
if value.has("$exact"):
|
||||
if value.size() != 1 or not value["$exact"] is String: return false
|
||||
var text: String = value["$exact"]
|
||||
if text.length() > 4096: return false
|
||||
if text.length() > 29: return false
|
||||
var terms: PackedStringArray = text.split("/")
|
||||
if terms.size() > 2: return false
|
||||
if terms.size() != 1: return false
|
||||
var n: String = terms[0].trim_prefix("-")
|
||||
if n.is_empty(): return false
|
||||
for ch in n:
|
||||
if ch < "0" or ch > "9": return false
|
||||
if terms.size() == 2:
|
||||
if terms[1].is_empty(): return false
|
||||
for ch in terms[1]:
|
||||
if ch < "0" or ch > "9": return false
|
||||
if Big.parse(terms[1]) == [0]: return false
|
||||
if Big.compare(Big.parse(n), LIMIT) >= 0: return false
|
||||
return true
|
||||
for k in value:
|
||||
if not valid_encoded(value[k]): return false
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
extends RefCounted
|
||||
## Immutable by contract; arithmetic creates new values. Signed reduced fraction.
|
||||
## Immutable magnitude high * 10^14 + low + fraction, with a separate sign.
|
||||
var polarity: int = 1
|
||||
var numerator: Array = [0]
|
||||
var denominator: Array = [1]
|
||||
var high: int = 0
|
||||
var low: int = 0
|
||||
var fraction: float = 0.0
|
||||
|
||||
func _to_string() -> String:
|
||||
const Big = preload("res://world/owniverse/math/big_integer.gd")
|
||||
return ("-" if polarity < 0 else "") + Big.decimal(numerator) + ("/" + Big.decimal(denominator) if denominator != [1] else "")
|
||||
var whole: String = str(high) + "%014d" % low if high else str(low)
|
||||
var tail: String = String.num(fraction, 16).trim_prefix("0").trim_suffix(".") if fraction else ""
|
||||
return ("-" if polarity < 0 else "") + whole + tail
|
||||
|
||||
@@ -66,7 +66,7 @@ func append_batch(target: Dictionary, id: String, amount: Variant, created_from:
|
||||
# next integer. Skip expired empty windows without changing the span
|
||||
# passed to the next generation or the lifetime production integral.
|
||||
if skip_empty_expired:
|
||||
var threshold = E.add(created_from, E.divide(E.multiply(duration, E.add(E.subtract(amount, remaining_amount), 1)), amount))
|
||||
var threshold = E.add(created_from, E.mul_div(duration, E.add(E.subtract(amount, remaining_amount), 1), amount))
|
||||
var next_nonempty = E.subtract(E.ceil_value(E.divide(E.subtract(threshold, origin), batch_duration)), 1)
|
||||
var expired_end = E.floor_value(E.divide(E.subtract(E.subtract(completed_before, lifespan), origin), batch_duration))
|
||||
var skip_to = E.minimum(next_nonempty, expired_end)
|
||||
@@ -83,7 +83,7 @@ func append_batch(target: Dictionary, id: String, amount: Variant, created_from:
|
||||
var part_end: Variant = E.minimum(created_to, window_end)
|
||||
var part_amount: Variant = remaining_amount
|
||||
if (E.gt(duration, 0) and E.lt(part_end, created_to)):
|
||||
part_amount = E.subtract(E.floor_value(E.divide(E.multiply(amount, (E.subtract(part_end, created_from))), duration)), (E.subtract(amount, remaining_amount)))
|
||||
part_amount = E.subtract(E.floor_value(E.mul_div(amount, (E.subtract(part_end, created_from)), duration)), (E.subtract(amount, remaining_amount)))
|
||||
if E.le(E.add(window_end, lifespan), completed_before):
|
||||
# Retain the original integer allocation and lifetime integral, but
|
||||
# do not allocate thousands of already-dead batch dictionaries.
|
||||
@@ -132,7 +132,7 @@ func withdraw_batches(target: Dictionary, id: String, to_year: Variant) -> Array
|
||||
var until: Variant = E.minimum(to_year, end)
|
||||
if E.lt(to_year, end):
|
||||
var carry: Variant = batch.get("withdraw_remainder", 0)
|
||||
var expected: Variant = E.add(carry, E.divide(E.multiply((E.subtract(amount, carry)), (E.subtract(to_year, start))), (E.subtract(end, start))))
|
||||
var expected: Variant = E.add(carry, E.mul_div((E.subtract(amount, carry)), (E.subtract(to_year, start)), (E.subtract(end, start))))
|
||||
consumed = E.floor_value(expected)
|
||||
batch.withdraw_remainder = E.subtract(expected, consumed)
|
||||
batch.amount = E.maximum(0, E.subtract(amount, consumed))
|
||||
@@ -160,7 +160,7 @@ func expired_years(year: Variant, start: Variant, end: Variant) -> Variant:
|
||||
return E.subtract(year, start)
|
||||
if E.ge(year, end):
|
||||
return E.add(E.subtract(year, end), E.multiply((E.subtract(end, start)), E.of("0.5")))
|
||||
return E.divide(E.multiply((E.subtract(year, start)), (E.subtract(year, start))), (E.multiply(2, (E.subtract(end, start)))))
|
||||
return E.mul_div((E.subtract(year, start)), (E.subtract(year, start)), (E.multiply(2, (E.subtract(end, start)))))
|
||||
|
||||
|
||||
func consume_from_batches(entity: OwniverseEntity, current_year: Variant) -> Variant:
|
||||
|
||||
@@ -52,7 +52,7 @@ var speed: int :
|
||||
state.speed_in_year = E.power(10, E.subtract(state.speed, 1))
|
||||
|
||||
func evolute(delta: Variant) -> void:
|
||||
if (E.le(delta, 0) or (not E.finite(delta))):
|
||||
if not E.finite(delta) or E.approx(delta) <= 0.0 or E.approx(delta) * E.approx(state.speed_in_year) >= 1e27:
|
||||
return
|
||||
var clock: Dictionary = SimulationTime.advance(state.time, E.approx(delta), speed)
|
||||
if not E.finite(clock.datetime): return
|
||||
|
||||
@@ -20,7 +20,7 @@ static func allocate_whole(requests: Dictionary, limit: Variant) -> Dictionary:
|
||||
var order: Array = []
|
||||
var assigned: Variant = 0
|
||||
for id in requests:
|
||||
var expected: Variant = (E.divide(E.multiply(E.floor_value(E.maximum(0, requests[id])), available), total) if E.gt(total, 0) else 0)
|
||||
var expected: Variant = (E.mul_div(E.floor_value(E.maximum(0, requests[id])), available, total) if E.gt(total, 0) else 0)
|
||||
result[id] = E.floor_value(expected)
|
||||
assigned = E.add(assigned, result[id])
|
||||
order.append({"id": id, "rest": E.subtract(expected, result[id])})
|
||||
@@ -38,7 +38,8 @@ static func production_step(costs: Array):
|
||||
var step = 1
|
||||
for cost in costs:
|
||||
if E.le(cost, 0): continue
|
||||
var denominator = E.make(E.parts(cost)[1])
|
||||
var terms: Array = E.parts(cost)
|
||||
var denominator = E.make(E.Big.divmod(terms[1], E.Big.gcd(terms[0], terms[1]))[0])
|
||||
step = E.multiply(E.divide(step, whole_gcd(step, denominator)), denominator)
|
||||
return step
|
||||
|
||||
@@ -76,12 +77,12 @@ static func split(remainders: Dictionary, key: String, count: Variant, weights:
|
||||
var expected: Dictionary = {}
|
||||
var positive_sum: Variant = 0
|
||||
for id in weights:
|
||||
expected[id] = E.add(E.divide(E.multiply(count, weights[id]), sum_weights), remainders.get(E.add(E.add(key, ":"), id), 0))
|
||||
expected[id] = E.add(E.mul_div(count, weights[id], sum_weights), remainders.get(E.add(E.add(key, ":"), id), 0))
|
||||
positive_sum = E.add(positive_sum, E.maximum(0, expected[id]))
|
||||
var order: Array = []
|
||||
var assigned: Variant = 0
|
||||
for id in weights:
|
||||
var normalized: Variant = (E.divide(E.multiply(E.maximum(0, expected[id]), count), positive_sum) if E.gt(positive_sum, 0) else 0)
|
||||
var normalized: Variant = (E.mul_div(E.maximum(0, expected[id]), count, positive_sum) if E.gt(positive_sum, 0) else 0)
|
||||
result[id] = E.floor_value(normalized)
|
||||
assigned = E.add(assigned, result[id])
|
||||
order.append({"id": id, "rest": E.subtract(normalized, result[id])})
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const STATE_VERSION: String = "0.2.0"
|
||||
const STATE_VERSION: String = "0.3.0"
|
||||
const TICKS_PER_YEAR: int = 1000000000
|
||||
|
||||
static func processed_year(datetime):
|
||||
return E.divide(E.floor_value(E.multiply(datetime, 10)), 10)
|
||||
var whole = E.floor_value(datetime)
|
||||
var ticks: int = roundi(E.approx(E.subtract(datetime, whole)) * TICKS_PER_YEAR)
|
||||
return E.add(whole, E.divide(ticks / 100000000, 10))
|
||||
|
||||
static func elapsed_years(from_year, to_year):
|
||||
return E.subtract(to_year, from_year)
|
||||
|
||||
## Frame fractions stay small; the universe age is an exact integer tick count.
|
||||
## Only the fractional year is converted to ticks; the whole age stays exact.
|
||||
static func advance(time: Dictionary, delta: float, speed: int) -> Dictionary:
|
||||
var ticks = E.round_value(E.multiply(time.datetime, TICKS_PER_YEAR))
|
||||
var increment: float = delta * pow(10.0, speed - 1) * TICKS_PER_YEAR + float(time.get("tick_remainder", 0.0))
|
||||
var whole_years = E.floor_value(time.datetime)
|
||||
var ticks: int = roundi(E.approx(E.subtract(time.datetime, whole_years)) * TICKS_PER_YEAR)
|
||||
var elapsed_years_float: float = delta * pow(10.0, speed - 1)
|
||||
var elapsed_whole = E.of(floorf(elapsed_years_float))
|
||||
var increment: float = (elapsed_years_float - floorf(elapsed_years_float)) * TICKS_PER_YEAR + float(time.get("tick_remainder", 0.0))
|
||||
var whole: float = roundf(increment) if absf(increment - roundf(increment)) < 1e-6 else floorf(increment)
|
||||
ticks = E.add(ticks, E.of(whole))
|
||||
var datetime = E.divide(ticks, TICKS_PER_YEAR)
|
||||
return {"datetime": datetime, "year": processed_year(datetime), "ticks": ticks, "tick_remainder": increment - whole}
|
||||
var elapsed = E.add(elapsed_whole, E.divide(E.of(whole), TICKS_PER_YEAR))
|
||||
var datetime = E.add(whole_years, E.add(E.divide(ticks, TICKS_PER_YEAR), elapsed))
|
||||
return {"datetime": datetime, "year": processed_year(datetime), "tick_remainder": increment - whole}
|
||||
|
||||
@@ -102,7 +102,7 @@ func apply_blast(target: Dictionary, power: Variant, environment: Dictionary) ->
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(weights)
|
||||
# Fractional exposure is deterministic; destruction itself remains whole.
|
||||
for id in distribution:
|
||||
destroy_planets(target, id, E.multiply(E.divide(E.multiply(hits, distribution[id]), quantities[id]), environment.density), 1, "blast")
|
||||
destroy_planets(target, id, E.multiply(E.mul_div(hits, distribution[id], quantities[id]), environment.density), 1, "blast")
|
||||
|
||||
|
||||
func uses_short_lived_fast_path(id: String) -> bool:
|
||||
|
||||
@@ -6,20 +6,22 @@ func stellar_environment(source: Dictionary) -> Dictionary:
|
||||
var quantities: Dictionary = world.ledger.physical_amounts(source)
|
||||
# SMBH is a self-group in GI; its quantity stores mass, not object count.
|
||||
var mass: Variant = source.amount.get("SMBH", 0)
|
||||
var volume: Variant = quantities.get("S", 0)
|
||||
# Geometric volume includes a 1e22 scale per void and can exceed E27.
|
||||
# It only feeds approximate density/blast coefficients, never stock counts.
|
||||
var volume: float = E.approx(quantities.get("S", 0))
|
||||
for id in quantities:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
if E.contains(["H", "He", "Met", "SMBH"], id):
|
||||
mass = E.add(mass, quantities[id])
|
||||
else:
|
||||
mass = E.add(mass, E.multiply(quantities[id], E.maximum(0, entity.mass)))
|
||||
volume = E.add(volume, E.multiply(quantities[id], E.maximum(0, entity.space)))
|
||||
volume = E.add(volume, E.multiply(E.multiply(quantities[id], E.maximum(0, entity.voids)), E.of("1.0e22")))
|
||||
volume += E.approx(quantities[id]) * maxf(0.0, E.approx(entity.space))
|
||||
volume += E.approx(quantities[id]) * maxf(0.0, E.approx(entity.voids)) * 1e22
|
||||
var metallicity: Variant = 0
|
||||
if E.gt(mass, 0):
|
||||
metallicity = E.maximum(0, E.add(E.multiply(E.of("0.4"), E.logarithm(E.add(E.divide(E.multiply(100, source.amount.get("Met", 0)), mass), E.of("0.11")))), E.of("0.88")))
|
||||
metallicity = E.maximum(0, E.add(E.multiply(E.of("0.4"), E.logarithm(E.add(E.mul_div(100, source.amount.get("Met", 0), mass), E.of("0.11")))), E.of("0.88")))
|
||||
var base_mass: Variant = E.multiply(65536, ((E.power(mass, E.of("0.1")) if (E.gt(mass, 0) and E.lt(mass, E.of("1.0e10"))) else ((10 if E.ge(mass, E.of("1.0e10")) else 1)))))
|
||||
return {"metallicity": metallicity, "density": E.divide(E.multiply(E.of("1.23456e-7"), E.square_root(E.add(base_mass, mass))), E.power(E.maximum(1, volume), E.divide(1, 6))), "blast": (E.clamp_value(E.divide(source.amount.get("S", 0), volume), 0, 1) if E.gt(volume, 0) else 1)}
|
||||
return {"metallicity": metallicity, "density": E.coefficient(1.23456e-7 * sqrt(E.approx(E.add(base_mass, mass))) / pow(maxf(1.0, volume), 1.0 / 6.0)), "blast": E.coefficient(clampf(E.approx(source.amount.get("S", 0)) / volume, 0.0, 1.0)) if volume > 0.0 else 1}
|
||||
|
||||
|
||||
func modify_world_parameter(key: String, change: Variant) -> void:
|
||||
|
||||
@@ -91,7 +91,7 @@ static func _integration(target: Dictionary, events: Variant) -> void:
|
||||
var chosen: int = rng.randi_range(0, 2)
|
||||
counts[chosen] = E.add(counts[chosen], 1)
|
||||
else:
|
||||
counts[0] = E.clamp_value(E.round_value(E.of(rng.randfn(E.approx(E.divide(events, 3)), E.approx(E.square_root(E.divide(E.multiply(events, 2), 9)))))), 0, events)
|
||||
counts[0] = E.clamp_value(E.round_value(E.of(rng.randfn(E.approx(E.divide(events, 3)), E.approx(E.square_root(E.mul_div(events, 2, 9)))))), 0, events)
|
||||
var rest: Variant = E.subtract(events, counts[0])
|
||||
counts[1] = E.clamp_value(E.round_value(E.of(rng.randfn(E.approx(E.divide(rest, 2)), E.approx(E.square_root(E.divide(rest, 4)))))), 0, rest)
|
||||
counts[2] = E.subtract(rest, counts[1])
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
[ext_resource type="Texture2D" uid="uid://so06vth6le3b" path="res://ui/main_menu/ru/russian_active.png" id="51_uvuuk"]
|
||||
[ext_resource type="Texture2D" uid="uid://dwqpcm1pryw44" path="res://ui/main_menu/en/leave_reality.png" id="52_uvuuk"]
|
||||
[ext_resource type="Texture2D" uid="uid://btghvfdh3guo5" path="res://ui/report/en/report.png" id="60_an6vj"]
|
||||
[ext_resource type="Texture2D" uid="uid://dvt8fw1m5mpki" path="res://ui/main_menu/sound/sound_runner.png" id="61_bemti"]
|
||||
[ext_resource type="Texture2D" uid="uid://bce2bpw5642er" path="res://ui/achievements/achievements.en.png" id="61_d36xh"]
|
||||
[ext_resource type="Script" uid="uid://vy0dj2cqfmk0" path="res://ui/credits/credits.gd" id="62_o2i0n"]
|
||||
[ext_resource type="Texture2D" uid="uid://c46h78ewwlafk" path="res://ui/credits/credits.en.png" id="62_vu83j"]
|
||||
@@ -986,11 +987,15 @@ theme_override_constants/separation = 64
|
||||
[node name="SoundVolume" type="HSlider" parent="Stage/MainMenu/Volume" unique_id=1297986446]
|
||||
custom_minimum_size = Vector2(444, 96)
|
||||
layout_mode = 2
|
||||
theme_override_icons/grabber = ExtResource("61_bemti")
|
||||
theme_override_icons/grabber_highlight = ExtResource("61_bemti")
|
||||
scrollable = false
|
||||
|
||||
[node name="MusicVolume" type="HSlider" parent="Stage/MainMenu/Volume" unique_id=729455266]
|
||||
custom_minimum_size = Vector2(444, 96)
|
||||
layout_mode = 2
|
||||
theme_override_icons/grabber = ExtResource("61_bemti")
|
||||
theme_override_icons/grabber_highlight = ExtResource("61_bemti")
|
||||
scrollable = false
|
||||
|
||||
[node name="Credits" type="TextureButton" parent="Stage/MainMenu" unique_id=44726060]
|
||||
|
||||
Reference in New Issue
Block a user