jengine-city
A city builder - zones that grow by demand, power and water networks, traffic, land value, a budget and disasters.
| Kind | Project package: linked into the builds of the projects that list it |
| Source | packages/jengine-city |
| Version | 0.1.0 |
| Entry point | jengine_city::CityPackage |
| Script namespace | City |
| Listed by | projects/strategy-classics |
Using it
Section titled “Using it”List it in the top-level "packages" array of project.jengine:
{ "packages": [ { "name": "jengine-city", "source": { "type": "path", "path": "../../packages/jengine-city" } } ]}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 City == nil then return end.
Components
Section titled “Components”| Scene key | Rust type | |
|---|---|---|
city_game | CityGame | 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 |
|---|---|
City.start(e, scenario, difficulty, seed, form) | city_start |
City.demo(e, seed) | city_demo |
City.load(e, text) | city_load |
City.save(e) | city_save |
City.stop(e) | city_stop |
City.speed(e, speed) | city_speed |
City.tool(e, tool) | city_tool |
City.pointer(e, x, y) | city_pointer |
City.press(e, x, y) | city_press |
City.release(e, x, y) | city_release |
City.cancel(e) | city_cancel |
City.overlay(e, name) | city_overlay |
City.tax(e, zone, rate) | city_tax |
City.fund(e, dept, percent) | city_fund |
City.bond(e, take) | city_bond |
City.disasters(e, on) | city_disasters |
City.trigger(e, kind) | city_trigger |
City.autopilot(e, on) | city_autopilot |
City.assist(e, on) | city_assist |
City.daylight(e, on) | city_daylight |
City.clock(e, hour) | city_clock |
City.rename(e, name) | city_rename |
Scenes that use it
Section titled “Scenes that use it”projects/strategy-classics/assets/scenes/city.scene
From packages/jengine-city/README.md.
A SimCity-style city builder: residential, commercial and industrial zones
that grow and decay by demand, power and water networks, traffic over the
road graph, land value, pollution and crime, a budget, disasters, milestones
and scenarios. It was written for Brickhaven (projects/strategy-classics,
game id city); any game that wants a zoned, simulated city under it can use
it, and anything that wants only part of it (a traffic model over a grid, a
flood-filled utility network) can call its modules directly.
The simulation is city::City - plain data with no ECS in it, stepped by
City::update(dt) and changed by City::apply(tool, from, to), the same
call every tool goes through. It serializes whole, so a save is
serde_json::to_string(&city) and a load is from_str + after_load().
The package around it draws a city with ordinary engine components and
takes the player’s commands from scripts.
Using it
Section titled “Using it”List it in the project’s project.jengine:
{ "name": "jengine-city", "source": { "type": "path", "path": "../../packages/jengine-city" } }A scene using its component must be a v2 scene ("version": 2). The scene
authors the tilemap the land is drawn into (named by city_game.land, with
layers terrain, ground, road, rail, wires, overlay and pipes,
and an autotile on terrain), an ambient_light (the day and night turn
it), the atlas sprites/city/buildings.atlas in atlas_refs, and any number
of particle emitters named CitySmoke1, CitySmoke2, … which the package
moves onto fires and the smokiest chimneys in view. Brickhaven’s generator,
projects/strategy-classics/tools/gen_city.py, writes all of it, and the
tile and region layouts it uses are mirrored in draw.rs (a test checks the
atlas against them).
The rules
Section titled “The rules”- Zones and growth (
growth). Each zoned tile is a lot that grows from empty to density 4. Every five days a lot weighs its zone’s demand against what it sees: power, a road or rail within 3 tiles, a commute that works, water, land value, pollution, crime and nearby services. Without water a lot stops at density 1; tall homes and offices need valuable land. A lot cut off from power or its road for about four months is abandoned a level at a time. - Demand (the RCI bars): people come where there are jobs, shops follow people and industry, industry follows workers and the outside market. Taxes over 7% push demand down and under 7% up - at 20% nothing grows. Past 25,000 residents homes need a stadium, past 6,000 jobs commerce needs an airport and industry a seaport.
- Power and water (
networks) are flood fills from every plant and pump, so plants on one grid share their capacity; a grid that asks for more browns out at its far end. Power passes along lines and through every zone and building; water passes along pipes (an underground layer), which serve lots within 2 tiles. Pumps need power and pump more beside water. - Traffic (
traffic): a shortest-path fill over roads and rails from every job (and every home), costing a congested road more. Commuters walk down it from home to work and load every road tile they pass; trips over a road’s capacity are next month’s congestion. A lot whose commute is longer than 56 tiles has nowhere to go. Rail carries trips at half the cost and puts no car on a road. - Fields (
fields): service coverage over a radius scaled by funding; pollution from industry, plants, ports and busy roads, blurred and soaked up by trees and parks; crime from density, cut by police and land value; land value from water, trees, parks, schools, hospitals and the city’s middle, less pollution, crime and traffic. - Money (
budget): taxes per zone, funding per department (police, fire, health and schools, transport - starved roads crumble), upkeep, up to five bonds at rising interest, paid a twelfth each month. A year with an empty treasury loses the game. - Disasters (
disaster): fire that spreads between buildings and forest and is put out by fire coverage, floods rising off the shore, tornadoes, and a nuclear meltdown that poisons the ground. With disasters on they come by themselves; any can be set off on purpose. - Milestones and scenarios (
scenario): Village, Town (2,000), City (10,000), Capital (50,000), Metropolis (100,000), each unlocking buildings. A free sandbox (Metropolis wins), and four goals with deadlines: Founding (3,000 in three years), Boomtown (50,000 in twenty), Gridlock (a jammed city whose congestion must be halved) and After the Storm (a city to rebuild). - The bot (
bot): a mayor who lays a street grid in blocks and does what a player would through the same tools - claims blocks by demand, keeps a plant ahead of the load, pumps water, places services where coverage is thin, clears rubble, re-lays torn streets, keeps the treasury afloat. It builds the grown scenarios’ cities, plays Brickhaven’s demo, and can just say its next action, which is how a test plays a scenario by clicks.
Components
Section titled “Components”| Component | Is |
|---|---|
city_game | Where a city is played. land names the tilemap entity; size the map’s width and height in tiles (112); demo founds a sandbox city at once and lets the bot build it; seed its land; daylight runs a cosmetic day and night (four minutes a day: the ambient light dims and windows, headlights and stadium lights come on). The city itself lives beside it, never saved with the scene. |
Script commands
Section titled “Script commands”Every command takes the entity carrying city_game first. Tile coordinates
are tile space: x right, y down, a tile one unit (pass a pointer’s exact
position; it is floored).
| Call | Does |
|---|---|
City.start(e, scenario, difficulty, seed, form) | A new city: sandbox, founding, boomtown, gridlock or storm; easy, normal or hard; seed 0 for a random one; land river, coast, lake, island or "" for any. |
City.demo(e, seed) | A sandbox city the bot builds at full speed (a title screen’s backdrop). |
City.load(e, text) / City.save(e) | Load a save, or ask for one: it appears in city.save the next frame. |
City.stop(e) | Put the city away. |
City.speed(e, n) | 0 paused, 1 slow, 2 normal, 3 fast, 4 ultra (20 days a second). |
City.tool(e, key) | Pick a tool: query, bulldoze, road, rail, power, pipe, res, com, ind, coal, gas, nuclear, wind, pump, tower, police, fire, school, hospital, park, stadium, seaport, airport. |
City.pointer(e, x, y) | Where the pointer is, for the ghost, the cost and the hover readout. |
City.press(e, x, y) / City.release(e, x, y) | A drag: the tool is used from the press to the release (an L for networks, a rectangle for zones and the bulldozer, the footprint centred on the release for a building). |
City.cancel(e) | Drop a drag. |
City.overlay(e, name) | Show a data overlay: power, water, traffic, pollution, land_value, crime, police, fire, health, school, zones, pipes (the underground view), "" for none. |
City.tax(e, zone, rate) | Tax rate in percent for zone 0 (R), 1 (C), 2 (I), 0 to 20. |
City.fund(e, dept, percent) | Funding for 0 police, 1 fire, 2 health and schools, 3 transport. |
City.bond(e, take) | Issue a $10,000 bond (true) or repay one (false). |
City.disasters(e, on) / City.trigger(e, kind) | Disasters on or off; set one off: fire, flood, tornado, meltdown. |
City.autopilot(e, on) | Let the bot run the city. |
City.assist(e, on) | Let the bot only plan: its next action appears in city.plan. |
City.daylight(e, on) / City.clock(e, hour) / City.rename(e, name) | The day and night, and the hour it shows (0..24); the city’s name. |
What scripts read back
Section titled “What scripts read back”A command returns nothing, so everything is published in the custom bag
city on the same entity (Entity.get_custom(e, "city", key)), each frame:
| Key | Holds |
|---|---|
active | Whether a city is running. |
hud | name, title, date, month, funds, net (last month), pop, speed, demand (R, C, I in -1..1), power and water (use, capacity), congestion, pollution, crime, land_value, the scenario’s goal, deadline and progress, burning, tornado, and result ({ won, text } once the game is decided). |
map | w, h, tile (world units) and x, y: the map’s top-left corner in the world. |
home | The tile to point the camera at first. |
tool | The tool’s key and name, dragging, and what the drag under the pointer would cost - or why it cannot be done. |
hover | The tile under the pointer: what, land_value, pollution, crime, traffic, powered, watered, police, fire, commute, zone, level. |
budget | Every ten frames: tax, funding (percent), bonds, and ledgers for the year at these rates, last_year and month. |
tools | Every tool’s key, name, cost, about, unlocked and the milestone that unlocks it. |
stats, history | Totals for tests and graphs; population and funds by month. |
events | This frame’s news: { kind, text, x, y } (fires, milestones, budgets, advice). Read it every frame. |
message | The last command’s answer, { text, ok }. |
plan | With assist on, the bot’s next action: { kind = "tool", tool, x0, y0, x1, y1 }, { kind = "tax", zone, rate } or { kind = "bond", take }. |
focus, night, overlay, save | Where the bot last built; how dark it is (0..1); the overlay shown; a save asked for with City.save. |
The minimap is a texture the package writes, __city/minimap (two pixels a
tile, the current overlay’s colours, the view outlined): show it with a
.gui Image whose texture_id is set to that name once the city runs.
cargo test -p jengine-city holds the rules: tools and their prices, saving
a city whole, power through lines and zones and a grid browning out, water
from a powered pump reaching two tiles from a pipe, commutes too long to
work, commuters jamming a road and rail taking them off it, pollution
lowering land value, police (and their funding) cutting crime, a powered
town growing, no power meaning no growth, high taxes stalling it, no
water keeping lots low, fire spreading and a fire station stopping it,
tornado, flood and meltdown, both lose conditions, the grown scenarios
starting jammed and ruined, a session played from requests - and
the_bot_builds_a_town, the bot passing 5,000 people within five years on
three different maps without going broke.
cargo test -p jengine-core --test strategy_classics city plays the real
scene through Brickhaven’s own Lua: a road and zones laid with the mouse,
the budget window used, pause holding the calendar, a saved city written to
the save slot, a Founding left alone running out of time, and the Founding
won end to end - the bot planning, the script turning each plan into a
click, and the test making the click.