jengine-idle
An idle game: generators and upgrades as data, a prestige that trades the run for a bonus, and offline progress from the wall clock.
| Kind | Project package: linked into the builds of the projects that list it |
| Source | packages/jengine-idle |
| Version | 0.1.0 |
| Entry point | jengine_idle::IdlePackage |
| Script namespace | Idle |
| Listed by | projects/arcade-classics |
Using it
Section titled “Using it”List it in the top-level "packages" array of project.jengine:
{ "packages": [ { "name": "jengine-idle", "source": { "type": "path", "path": "../../packages/jengine-idle" } } ]}Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the
editor’s Rebuild viewport banner). Native and web builds link it on their own.
A scene that uses its components must be a v2 scene ("version": 2): a v1 scene has no field
for a package component and keeps it as inert custom data.
A script can check that the package is linked with if Idle == nil then return end.
Components
Section titled “Components”| Scene key | Rust type | |
|---|---|---|
idle_game | IdleGame | package only: needs a v2 scene |
Script commands
Section titled “Script commands”Commands run in every runtime and return nothing; a package publishes what scripts read onto components (see the notes below). The flat name also works.
| Call | Flat name |
|---|---|
Idle.buy(e, generator, count) | idle_buy |
Idle.upgrade(e, upgrade) | idle_upgrade |
Idle.prestige(e) | idle_prestige |
Idle.click(e, amount) | idle_click |
Idle.load(e, json, now) | idle_load |
Idle.reset(e) | idle_reset |
Scenes that use it
Section titled “Scenes that use it”projects/arcade-classics/assets/scenes/cellar.scene
From packages/jengine-idle/README.md.
An idle game: generators that make a thing, upgrades that multiply it, a prestige that throws the run away for a permanent bonus, and progress while the game is closed.
Used by projects/arcade-classics (Cinder Cellar).
| Module | What it is |
|---|---|
numbers | Big numbers and how to say them, the exponential cost curve, and the closed form for “how many can I afford”. |
content | Generators, upgrades and the prestige curve as rows in the project’s data tables. |
game | The run: buying, producing, prestige, saving, and coming back. |
Offline progress is why the engine grew a wall clock
Section titled “Offline progress is why the engine grew a wall clock”The engine had now_ms — monotonic, arbitrary origin, explicitly not a
date. It measures a session and nothing else, so two readings either side of
a restart mean nothing, and “how long was I away?” was unanswerable. That
question is the entire premise of this genre (and half the premise of a
daily reward), so the engine now has Time.get_unix() /
jengine_core::profiling::unix_time() and the save carries a stamp.
Coming back is then arithmetic, with a clamp at each end:
let away = (now - self.save.stamp).max(0.0); // clocks go backwardslet paid_for = away.min(OFFLINE_CAP); // eight hourslet gained = self.rate * paid_for * OFFLINE_RATE; // half of being thereBoth clamps are load-bearing. A system clock moves backwards for ordinary reasons — a timezone change, an NTP correction, a player setting the date forward to cheat and then back — and an unclamped subtraction hands out negative money for it. Without the cap, a save from last year buys the whole game on the way back in, which is not a reward, it is the end of the game. And offline production pays half, because if it paid full there would be no reason to be there.
The other half of the same idea is that the stamp has to stay current while the game is running, or a session that never gets saved measures its own length as absence next time. The system restamps on its autosave tick.
The closed form, and why a loop will not do
Section titled “The closed form, and why a loop will not do”The price of the next one is base * growth^owned, and the total of the
next n is a geometric series. The “buy max” button every one of these
games has needs that series inverted: how many can this bank afford?
let inner = budget * (growth - 1.0) / unit + 1.0;let n = inner.log(growth).floor().max(0.0);Counting them one at a time would be a loop whose length the player
chooses — a shallow curve and a large bank is eighteen thousand iterations
of something that runs every frame, and a late-game player has worse. Two
logarithms answer it instead. tests/numbers.rs checks the closed form
against the loop it replaces, and checks that one more would not have been
affordable, which is the off-by-one this shape invites.
f64 is the right storage — 15 significant digits is more precision than an
idle game’s arithmetic deserves, and it reaches 1e308 — so the real work is
the writing down: say() gives three significant figures and a short-scale
suffix (1.23K, 999M, 4.70Sx), and falls back to exponent notation past
the last name rather than running off the end of the list.
Prestige, and the gating before it
Section titled “Prestige, and the gating before it”(earned / required) ^ curve, floored. A curve below 1 flattens it, so a run
ten times as long is not ten times the reward — that is what stops the
second run from being the last one. The reset takes the currency, the
generators and the upgrades; the prestige currency, the reset count and the
lifetime totals survive it.
Generators and upgrades carry unlock_at / show_at thresholds on lifetime
earnings, because twelve buttons on the first screen is how these games lose
people. The content loader refuses a table where nothing is available at the
start, an upgrade that boosts a generator that does not exist (it would
appear, cost money and silently do nothing), and a cost curve that never
rises.
The component
Section titled “The component”idle_game on one entity, in a "version": 2 scene. The package draws
nothing and saves nothing — it publishes the whole run as a JSON string and
the game hands that to the engine’s save system.
"idle_game": { "tables": { "generators": "data/generators", "upgrades": "data/upgrades", "prestige": "data/prestige" }, "save_slot": "arcade", "save_key": "cellar", "autosave": 10.0}Script commands
Section titled “Script commands”| Call | Does |
|---|---|
Idle.buy(e, generator, count) | buy that many; 0 buys as many as the bank allows |
Idle.upgrade(e, upgrade) | buy an upgrade |
Idle.prestige(e) | trade the run for the bonus |
Idle.click(e, amount) | the button every one of these games opens with |
Idle.load(e, json, now) | restore a save and pay for the time away |
Idle.reset(e) | wipe everything, prestige included |
What scripts read
Section titled “What scripts read”IdleStateevery frame:currency,earned,rate,prestige,prestige_gain,prestige_required,resets,lifetime,played,offline_paid,offline_seconds, and thegenerators/upgradeslists — each row carrying its price, its “max” order, what it produces, and whether it is unlocked and affordable, so the game draws a button without recomputing the curve in Lua.saveis the whole run as a string.IdleEvents.events:buy,upgrade,prestige,offline,autosave.