Update speed limit, enable embedded PCK format, and enhance brown dwarf production logic

This commit is contained in:
Kirill
2026-09-21 20:42:40 +03:00
parent f246efd6ad
commit 1796829338
9 changed files with 192 additions and 97 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+12
View File
@@ -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`. При частичном списании:
+12 -64
View File
@@ -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:
+2
View File
@@ -1,5 +1,7 @@
# Проверки поэтапного переноса из Owniverse.U
`test_stellar_rules.gd` также проверяет автоматическое создание HBD: долю 0,1% нового автоматического водорода, появление по одному со случайным округлением, сохранение оплаченного остатка топлива, отсутствие повторного начисления на старый запас, продолжение случайной последовательности после сериализации и переход HBD → CBD с сохранением счётчика BD. Формулы описаны в `../simulation_rules.md`.
## Фоновое сохранение и частота обновления интерфейса
```sh
+27 -9
View File
@@ -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)
+59
View File
@@ -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
+33 -21
View File
@@ -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])
+45 -1
View File
@@ -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 = {}