Skip to content

jengine-borough

A living town: villagers with needs, skills and moods who pick their own work by priority, buildings and work orders, hauling and trade done by the people, districts bought or conquered, and a storyteller that writes the town’s chronicle.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-borough
Version0.1.0
Entry pointjengine_borough::BoroughPackage
Script namespaceBorough
Listed byprojects/thornwick

List it in the top-level "packages" array of project.jengine:

{
"packages": [
{ "name": "jengine-borough", "source": { "type": "path", "path": "../../packages/jengine-borough" } }
]
}

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 Borough == nil then return end.

Scene keyRust type
borough_townTownTablepackage only: needs a v2 scene

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.

CallFlat name
Borough.found(e, seed, difficulty, name)borough_found
Borough.demo(e, seed)borough_demo
Borough.load(e, text)borough_load
Borough.save(e)borough_save
Borough.stop(e)borough_stop
Borough.speed(e, x)borough_speed
Borough.autopilot(e, on)borough_autopilot
Borough.pointer(e, x, y)borough_pointer
Borough.press(e, x, y)borough_press
Borough.drag(e, x, y)borough_drag
Borough.release(e, x, y)borough_release
Borough.order_at(e, x, y)borough_order_at
Borough.tool(e, kind)borough_tool
Borough.select(e, kind, id)borough_select
Borough.panel(e, name)borough_panel
Borough.demolish(e, building)borough_demolish
Borough.pause_building(e, building, on)borough_pause_building
Borough.limit(e, building, n)borough_limit
Borough.bill(e, building, recipe, mode, n)borough_bill
Borough.bill_remove(e, building, index)borough_bill_remove
Borough.bill_pause(e, building, index, on)borough_bill_pause
Borough.priority(e, person, work, level)borough_priority
Borough.assign(e, person, building)borough_assign
Borough.draft(e, person, on)borough_draft
Borough.draft_all(e, on)borough_draft_all
Borough.march(e, person, x, y)borough_march
Borough.acquire(e, district)borough_acquire
Borough.attack(e, district)borough_attack
Borough.answer(e, letter, choice)borough_answer
Borough.tax(e, level)borough_tax
Borough.rations(e, level)borough_rations
Borough.trade(e, good, sell, keep, buy_to)borough_trade
Borough.buy_food(e, on)borough_buy_food
Borough.festival(e)borough_festival
Borough.deal(e, faction, act)borough_deal
Borough.trigger(e, event)borough_trigger
  • projects/thornwick/assets/scenes/founding.scene
  • projects/thornwick/assets/scenes/howto.scene
  • projects/thornwick/assets/scenes/legends.scene
  • projects/thornwick/assets/scenes/title.scene
  • projects/thornwick/assets/scenes/town.scene

From packages/jengine-borough/README.md.

A living town. Villagers with needs, skills, traits and moods pick their own work by the priorities you give them; buildings are laid out, carried to and built by the people; workshops follow work orders; haulers and merchants move goods and trade them by your rules; the land is cut into districts you claim, buy or take by force; and a storyteller sends raids, caravans, plagues, wanderers and lords’ demands, and writes it all into the town’s chronicle. It is what Thornwick (projects/thornwick) is made of - RimWorld and Dwarf Fortress in a medieval frontier town, with a city-builder’s districts.

The rules have no ECS in them: town::Town is plain data that serializes whole (that is a save), and is stepped with Town::update(content, dt). draw puts a town on screen with ordinary components; bot runs one without a player; lib.rs is the seam.

  • Villagers (person, act) get hungry, tired and bored, fall sick and get hurt. Their mood is their needs plus the thoughts they carry (a wedding, a death, a raid survived, meagre rations, harsh taxes); a low one makes them sulk, binge or smash things, and a long one makes them leave.
  • Neighbours (social): every villager keeps opinions of the people they spend time with. Bonds grow out of chance talks - most of all of an evening at the tavern or the chapel - shaped by their traits (shared ones draw them together, opposite ones apart; hotheads and the gloomy are poor company), kinship and mood, and a grudge colours every word after it. So friends and rivals are grown, not rolled: friends cheer and grieve each other and are who marries, rivals sour each other’s days and trade insults, and a hothead who loathes someone starts a brawl - bruises, sometimes a villager knocked flat, never a death.
  • The dead (burial): a body lies where it fell until a hauler carries it to a graveyard (a zone, one grave a plot) and buries it. Passing a body shakes a villager, and a body left a day and a half weighs on the whole town; a burial comforts those who loved them, more with a chapel. Bodies nobody buries are taken by the crows after twelve days.
  • Homes (homes): every house is a cottage at first, and rises to a town house and then a merchant’s house when its household has what such a home wants - better food, a market, a tavern and a chapel in reach, a road to the door, cloth and ale, and the stone and planks to rebuild. Each level sleeps one more, cheers its household and pays more in taxes, so where the player puts a market decides which streets rise first. What a house still wants is in the inspector, reason by reason.
  • Work (jobs): each villager ranks eleven kinds of work 1-4 or never. An idle villager goes down their ranks and takes the nearest job of the first kind that has one, speaking for what it uses (a tree, a field tile, materials on their way to a site) so two never walk to the same tree. Skills make work faster and grow with doing it; no tools makes it slower.
  • Building: a laid-out building is a site; its materials are carried from the nearest heap that has them (a store, a workshop’s output, a pile) and builders raise it. Fields and pastures are zones of any size.
  • Production: fields (sown spring to autumn, lost to frost if unreaped), fishing huts (food in every season - thin under ice - from water that fills again over two days), pastures (wool, mutton, lambs), woodcutters (fell and replant), quarries (stone, and ore where there is a vein), workshops following work orders (building::Bill: make N, keep N in stock, or always).
  • Trade (trade): caravans come in from the edge and stand at the market (or the town hall); villagers set to Trade deal with them one bargain at a time - selling above the stock to keep, buying up to the stock wanted, and food when the town has under three days of it. Taxes bring gold daily.
  • The realm (realm, realm_rules): districts are claimed (wild, for gold), bought (from a lord who likes the town), or taken - an attack turns out the district’s garrison, the drafted militia beats it, and standing there takes it. A lord’s seat taken ends the house.
  • Diplomacy (diplomacy): the reeve moves a lord’s temper on purpose - a gift of gold (each soon after another counts for less, and one can buy a peace), a demand for tribute (paid by a lord the town’s armed might outmatches half again, who hates the town for it; refused by any other, maybe with war), or a marriage (a single villager weds into the house and leaves for good; once a year a house). An ally (60 and up) sells land at half price.
  • Violence (combat): raiders go for the stores, soldiers for the town hall, wolves for the sheep; foes plan through walls and hack at what is in the way. A raid breaks at half its number. Manned watchtowers shoot. The fallen are carried home by whoever can heal.
  • The storyteller (story): every half day it looks at the town. A threat comes after enough calm days (fewer when cruel), sized by wealth, people and age and capped by the town’s adults; between threats come the rest, weighted kinder to a miserable town. Every event is a row in a data table whose effect names its handler; some ask a question, answered with their last choice if left too long.
  • Life (life): births, weddings, ageing, the seasons, cloth for the winter, homes found, fires spreading - and once a day the numbers for the town’s graphs. A villager who slays three foes earns a name (the Bold, Wolfbane) that the chronicle remembers.
  • Milestones (goals): a ladder from the first house to bringing down a lord; the next one is in the HUD, each reached is chronicled.

The whole rulebook - goods, buildings, recipes, events, traits - is data: content/*.json, compiled in, and overridden by a project’s borough_* data tables (see projects/thornwick/tools/gen_data.py).

borough_town on one entity:

"borough_town": { "land": "Land", "demo": false, "seed": 0, "difficulty": "steady", "speed": 1.0 }

land names an entity with a tilemap whose layers are ground, shade, marks and features (y-sorted) and whose tilesets follow draw::tiles (Thornwick’s tools/gen_tiles.py builds them). With demo the package founds a town itself and lets the bot run it (a title screen). An ambient_light anywhere in the scene is turned with the time of day. Every texture the package draws must be preloaded (Thornwick’s scenes give the director a sprite_library of them).

Namespace Borough; e is the borough_town entity. Lua counts from 1.

CallDoes
found(e, seed, difficulty, name)a new town (gentle / steady / cruel; seed 0 from the clock)
demo(e, seed)a town the bot runs
load(e, text) / save(e)a town from a save / the town as JSON in save next frame
stop(e), speed(e, x), autopilot(e, on)end it; clock speed (0 pauses, up to 8); the bot runs it
pointer(e, x, y), press, drag, releasethe pointer in world units: tools and selection
order_at(e, x, y)the other button: march the drafted there, or put the tool down
tool(e, kind)a building id, chop / mine / road / clear / wall / gate / demolish, or ""
select(e, kind, id)person / building / foe / district / none
panel(e, name)publish people, goods, realm, letters or chronicle
demolish(e, b), pause_building(e, b, on)tear down or cancel; pause
limit(e, b, n)a woodcutter’s or quarry’s stopping point: rest when the town holds n (0: never)
bill(e, b, recipe, mode, n), bill_remove(e, b, i), bill_pause(e, b, i, on)work orders (count / until / forever)
priority(e, person, work, level), assign(e, person, b)work ranks; put someone to work at a building (0 frees them)
draft(e, person, on), draft_all(e, on), march(e, person, x, y)the militia (person 0: all drafted)
acquire(e, district), attack(e, district)claim or buy; march on
deal(e, faction, act)deal with a lord: gift (gold for goodwill), demand (tribute - paid only by a lord the town’s arms outmatch) or marry (a single villager weds into the house and leaves)
answer(e, letter, choice)answer a letter
trigger(e, event)make an event (a row of borough_events) happen now - for scenarios and testing
tax(e, 0-3), rations(e, 0-2), trade(e, good, sell, keep, buy_to), buy_food(e, on), festival(e)decrees

The borough custom-component bag on the entity: hud (date, time, speed, gold, people, mood, food in days, every good’s count, foes, letters waiting, map size), selected (the inspected villager, building, district or foe in full), tool (what is held and why it cannot go there), hover (the tile under the pointer), catalog (the build menu), letters, panel (the open panel’s data), summary and home (where to start the camera), events (this frame’s happenings: built, death, letter_threat, caravan, chronicle, …), message (an order’s answer), save and errors.

ModuleWhat it is
townthe town, its step, placement, stores, claims, walkable regions
persona villager: needs, mood, skills, traits, priorities
jobs / actchoosing a job / doing it (walking, hauling, building, farming, crafting, healing)
buildinga building, its stock and supplies, work orders
map, path, worldgenthe land; A* that plans foes through walls; a new land, checked
realm, realm_rulesdistricts and factions; claiming, buying, sieges
combat, trade, story, lifefoes and towers; caravans; the storyteller; growth, moods, days
goalsthe milestones
socialbonds, friends and rivals, insults and brawls
burial, homesthe dead and the graveyard; houses that rise a level
diplomacygifts, demands for tribute and marriages with the lords
ordersevery player command, applied in one place
botthe mayor who is not a person
contentthe rulebook, as data
view, drawwhat scripts read; the town on screen
  • cargo test -p jengine-borough - the rules module by module; every event fired and answered; town_lives (the bot keeps three towns fed and growing for a year, a save goes on exactly as the town would have, a cruel storyteller is crueller); friendship and rivalry grown from traits, a brawl that never kills; a house that rises only when its wants are met; a door its own building would wall in, refused; gifts that count for less and buy peace, tribute only from the outmatched, a marriage that sends a villager away; thornwick_scenes (the shipped scenes played through their own Lua: founding, building by clicking, panels, a gift to a lord, the militia, a letter, saving and continuing).
  • --release --test chronicle -- --ignored --nocapture prints a bot town day by day (SEED, DAYS); survey sums up twelve - deaths by cause, house levels, graves, friendships, rivalries, brawls and weddings.