Adventure Simulator
Adventure Simulator1 is an open source browser game using novel technologies to revive the golden age of pseudo-MMOs.
A web-first pseudo-MMO
The mid-2000s yielded a number of highly successful "pseudo-MMO" browser games, like Neopets and Club Penguin,2 whose markets have since been captured by mobile apps and native desktop games. However, new technologies like Wasm, WebGPU, and Datastar allow us to make a new kind of browser game, one with near-feature and performance parity with native applications: a kind of game that has been impossible to build until very recently.
Bulletin-board world
A traditional MMO uses a central server to maintain the live state of the game world, run simulation logic in real time, and push state updates to clients dozens of times per second. Designing and implementing this server presents a host of complex networking challenges; building a backend that can handle massive concurrency, synchronize thousands of players in real time, ensure consistency so everyone sees the same world, and maintain low latency isn't a lot of fun, which is why most people don't make traditional MMOs.
We aren't making a traditional MMO either. Our plan is to sidestep these challenges altogether by representing our world, not as a continuous simulation on a server, but as a bulletin board.3 A database contains information about players, places, and quests, and players interact with this world-database by taking discrete actions through an asynchronous, hypertext (web-style) interface. Unlike an MMO's world-server, a bulletin board database has no active connections to maintain; as soon as it responds to your request, it forgets you exist. When players do need real-time action, e.g. when they engage enemies in combat, we create a private virtual server just for their party, though as any real-time networking can quickly become dangerously complex, we intend to keep as much state as possible on the server, using server-sent events to push updates directly to the client as events happen.4
Player characters spend their downtime in settlements, which are persistent, bulletin board-like social hubs where they can purchase equipment, join parties, and embark on quests. When their party sets out on a quest, players load into a real-time, WebGPU-rendered combat simulation when they arrive their destination or are randomly attacked along the way; when the real-time simulation is no longer required, players transition back into the discrete hypertext format.
This is all to say that we aren't building a "normal" web game that uses Wasm and WebGPU to run in the browser. We are building a hypertext bulletin-board game which can act like a normal game when needed, like in combat, and where most of the game logic isn't even running in the browser but rather streamed, via Datastar, from the server.
Gameplay
Strategic balance and core-loop regression testing are supported by the NPC simulator documented in the strategic simulation reference. Its live mode drives the same party, quest, travel, autoresolve, loot, trade, and equipment reducers as players, against an explicitly disposable local database. The nearest games for inspiration are Mount and Blade, Battle Brothers, Jagged Alliance, Starsector, and to some extent Kenshi.
Like the former three, the world of Adventure Simulator is separated between the "tactical" layer (a real-time simulation) and the "strategic" layer (which advances in discrete chunks of time, generally after fast travel or resting). We have the same basic gameplay formula where the player recruits a party to adventure with, defeats enemies in randomly generated missions, and uses their hard-earned rewards to buy equipment for future missions.
Like in Kenshi, Battle Brothers, and Jagged Alliance, players can control multiple characters, though in Adventure Simulator, characters can be either mortal or immortal. Mortal characters offer a more roguelike/"extraction" experience, with fast progression and frequent deaths; when one of your mortal characters dies, any wealth not on their person will be inherited by your other characters. Immortal characters offer a more conventional RPG/MMO experience, which emulates the cost of mortal characters with costly respawns and slow healing.5
If there's any design choice in particular that makes our approach unique, it is specifically that we relinquish the vision of a continuous, immersive world. Kenshi clings to that vision, despite all the systems of the game going against it,6 and most MMOs try to reach that ideal before networking gets in the way. We take our inspiration from singleplayer games like Starsector, Jagged Alliance, and Mount and Blade which all chose to have a strategic layer, not because they had to for networking, but because their gameplay loop would be really boring without one. You can actually walk around cities in Warband and Bannerlord, but zero players actually do this outside of sieges because walking around is boring. Thus, we take those games' basic design and combine it with the one infamous problem it incidentally solves: MMO networking.
The prototype's first-character flow begins with a life-stage choice. Young characters are age 16 and professionless; adults are age 22 and newly journeyman-equivalent in one of ten profession families; old characters are age 40 and master-equivalent. Professional rosters contain one candidate per family, with the specific eligible organization selected deterministically from the tab's private seed when a family has multiple options. Witch hunters, knights, and foresters each have one denomination-neutral organization and therefore always receive that family's organization. Candidate state remains untrusted browser coordinates until confirmation authoritatively regenerates and atomically persists the selected character. Confirmed characters accumulate in a browser-scoped roster and can be switched from the strategic header; Character select returns to the life-stage flow to create another.
Setting
The world of Adventure Simulator is a historical fantasy version of Earth. Players of Warhammer Fantasy or readers of pre-Tolkien fantasy will be familiar with the concept: the setting is a real-world historical period with generic fantasy elements inexplicably sprinkled throughout.
Science fiction historian Brian Stableford has defined "historical fantasy" as "a term applied to fantasies in which the actual history of the primary world is conscientiously reproduced, save for limited infusions of working magic located within a 'secret history.'"
The heuristic for the fantasy elements is to put them in places that don't fundamentally alter historical conditions. Elves generally keep to forests or fictitious islands; Dwarves dwell within mountains; and creatures like Orcs, Goblins, Beastmen, and the Undead either roam as hordes or infest caves, crypts, and abandoned Dwarven settlements. To the extent that the kingdoms of Men interact with these fantastical elements, it is generally in hiring heroes to deal with the nuisances caused by hostile fantasy creatures. Elves and Dwarves are uninterested in Human political squabbles over borders and wars of succession, and fantastical enemies don't really pose a strategic threat to Human kingdoms, so the historical and fantastical elements of the setting can generally avoid stepping on each others' toes.
As for the historical elements, the year is approximately 1544 AD. Being both the height of Charles V's transatlantic empire and a year after the first Europeans reached Japan, it's just about the earliest feasible date in which all major cultures of the world can be at least indirectly aware of each other.7 For the MVP, the playable section of Earth will be limited to northern Germany, around the Baltic Sea;8 in the long term, we will gradually expand to all of Europe and beyond.
Philosophy
Below are some guiding principles for Adventure Simulator development.
Open source software
We tentatively intend to keep everything AGPLv3, but we're willing to hear out the case for other licenses.
The AGPL applies to Adventure Simulator software unless a file or artifact says otherwise. Generated strategic map tiles and terrain-routing packs are data artifacts with a separate licence boundary: project-owned contributions are offered under CC BY-SA 4.0 and underlying datasets retain their own terms. See MAP_DATA_LICENSE.md before distributing or hosting those artifacts.
It's clear to us that Adventure Simulator is very much the kind of project which will benefit from collaboration and indefinite iteration, which makes open source the obvious choice by a country mile. For instance, though our MVP for Adventure Simulator is (deliberately)9 generic historical fantasy, we don't intend or hope for it to stay that way. The project's open source nature will allow modders to come in and take it in all sorts of unexpected directions in the future; they may create total conversions to other fantasy settings, sci-fi settings, or... something else entirely.
Procedural assets
Third-party asset licenses and attribution are recorded in THIRD_PARTY_NOTICES.md. It should be easy for players to create content for the game, so to that end, we will use low-fidelity procedural assets to greatly reduce the barrier to entry. This doesn't mean that we don't care about fidelity at all; it means fidelity must necessarily come from procedural iteration rather than a trained CG artist's skill. The system Nintendo uses for Miis, for example, is a better example of how we might approach a character creator than, say, Baldur's Gate III. But that doesn't mean that we're going for an especially cartoony art style, either; there's nothing to prevent us applying a system like to more realistically proportioned characters (as Nintendo did, more or less, with Breath of the Wild and its sequel).
The same principle for graphics applies to audio. A good introduction to procedural audio may be found in Designing Sound by Andy Farnell.
Physically based gameplay
We would like the underlying gameplay systems to be realistic, as the real world can generally offer an unambiguous answer to any design question. It's not always easy to discover that answer, nor is it always easy to implement it without resorting to simplified abstractions,10 but all the imperfect solutions at least point in the same direction. Call this philosophy physically based gameplay, parallel to "physically based rendering" for graphics.11
The real world is not always as fun as a game ought to be. Fortunately, there are two ways to get around this:
Abstraction-based approach
We can abstract away the parts of the real world that are not particularly fun.
Holding W to walk 50 km between settlements is not particularly fun, nor is resting for several months to heal a serious injury, but if we put these activities in the "strategic layer" of the game (separate from the real-time "tactical layer"), a player can skip them by fast-forwarding in time. Likewise, micromanaging inventory is not particularly fun, but as the game becomes complex enough to necessitate it, we can also add tools to automate it, such as setting a desired weight limit and value/weight ratio for loot.
Content-based approach
We can design the non-real parts of the world to be more fun.
Being that this is a fantasy world, the fantastical elements are free variables for us to balance the game with. Suppose real-life combat is too fast for it to be viable to reliably dodge most attacks; we can simply give common fantasy enemies like Goblins, Orcs, and Skeletons such poor melee skills that an agile player character can reliably dodge them. Or suppose stealth is too frustrating with realistic detection ranges; we can just ensure that these fantasy creatures tend to have very poor eyesight.
Funding and legal
Adventure Simulator Group LLC is a for-profit company owned by Bruno Segovia (CEO) and Adler Halbe (Director). The founders are willing to invest serious portions of their incomes to see at least a prototype of this through. As they will be maintaining full-time employment throughout the development process, their contributions will largely be in the form of cash, but they will be available most days to provide guidance and strategic direction, primarily during evenings and weekends (PST).
Once the game works well enough to start hosting (and is sufficiently fun to be worth anyone's time), the founders will try and transition to a more sustainable funding model: one where players may have a single character per account for free but pay a subscription fee for multi-character accounts. This funding will be used to hire more developers, pay server costs, and hopefully obtain some profit.
Due to being open source, if at any time Adventure Simulator Group starts "enshittifying" the service, the community can simply fork it and host their own instance of the server. This hopefully will never happen, however, as the threat of it ought to suffice to keep everyone's incentives aligned. At face value, this is a terrible business decision (to willingly give up one's monopoly power), but the success of Patreon and Substack is evidence that relying on the goodwill of the community can be a genuinely viable business model, especially for an inherently creative product like a game. Will it actually work? We don't know. Let's find out!
Are you accepting investors?
Probably not. We want to be very selective about adding board members. However, if you think that you can make a good case, send an email to our CEO, Bruno Segovia.
Open (paid) positions
All positions are remote-only and with no Zoom meetings (unless you actually want them). Contact halbe@adventuresim.org to apply.
Having hired our first round of developers, we are not currently seeking applicants for any positions. We will likely initiate another developer hiring round in March. In the meantime, if you think you can contribute in some other way like writing or testing, send an email to halbe@adventuresim.org.
-
Working title. ↩
-
And actual MMOs, such as Runescape and AdventureQuest. ↩
-
You can think of a bulletin board as halfway between a Discord server and an Internet forum. Think of an imageboard: the threads are more live than reddit or forums, less live than chatrooms. The benefit of the format is that it works both synchronously and asynchronously; you can have a nearly live chat with a guy on /tg/, but the format also works even if you're the only live user on a given thread at that time. This isn't an unheard-of inspiration for a fantasy RPG; Dragon's Dogma's internal project name was "BBS-RPG" due to the "custom mercenary character" system. We would be taking the idea much further than DD did, of course. ↩
-
We end up rendering a sort of network-driven "immediate mode" view of the world. ↩
-
Mortal characters will probably be randomly generated by default. The idea is that players who prefer custom characters will naturally gravitate to the immortal option. ↩
-
Even at 4x speed, which most computers can barely handle simulating, you're still spending most of your time watching your characters travel or rest. ↩
-
Any later and your combat has too much "shot", not enough "pike." We briefly considered 1650 AD as it's a very dynamic and rich setting (the EIC under Cromwell's England and VOC of the Dutch Republic scramble to take East Asian colonies from Portugal and Spain as Russian explorers reach the Pacific and Japanese pirates roam the seas), but by that time, swords and pikes just aren't seeing enough use in combat for our purposes. Someone else should totally make that game, though. ↩
-
Thanks to the Hansa keeping very detailed maps of its trade routes, we have Viabundus: an extremely high-quality CC-BY-SA data source for northern Europe's roads, terrain, and settlements. Presently, only the Hanseatic trade zone is in scope for Viabundus, but the project is gradually expanding into greater Europe. We thank the University of Göttingen for maintaining Viabundus. ↩
-
Think of this as a high-effort tech demo in the spirit of Valve (cf. Half-Life). We really enjoy "weird fiction" like Morrowind and Dune, but at least for Adventure Simulator's first iteration, the goal is to innovate in tech, not aesthetic. For now, our aesthetic is what has been proven to work. ↩
-
Quantum physics is not in-scope for the MVP, to say the least. ↩
-
A game like Team Fortress 2, deliberately cartoony and unrealistic-looking, still employs "physically based rendering" in that its visuals are based on real-world lighting and material values, just tweaked and exaggerated to produce an unreal effect. The base values come from somewhere other than pure arbitrary imagination. Also known as "you need to know the rules in order to break them." ↩
Jeff arrives home after a long day at work and logs into Adventure Simulator. He selects his character, Geoffrey, and loads into the settlement that he last logged out from, Newport. From the settlement menu, he opens up the "quests" menu to pick out an adventure for the evening.
Planning
By default, it filters for quests whose party leaders are looking for members compatible with Geoffrey's build. In this case, a moderately armored, shielded, hammer-wielding man-at-arms. Party leaders can be as open or restrictive as they like with their filters, so some parties are specifically looking for hammerers, but others merely look for armored characters and aren't too picky about weapons.
He can see both the enemies that will likely be encountered in the quest as well as the rewards. One catches his eye, a quest to hunt down a pack of orcs that have been hunting in Puzzlewood, a forest under the protection of the elves. Jeff, being the kind of guy to name his character Geoffrey, has heard that elven characters are immortal and therefore better suited his playstyle. He doesn't want to have lots of mortal characters dying all the time and rolling new ones, and if he gets enough favor with the elves he'll be able to make his own. So he joins this quest.
Dylan sees that the last party member requested has joined, a hammer-wielding man-at-arms. This fills out an important gap in the party composition, as there is a chance that some of the orcs have crude heavy armor. Dylan's character, Derthert, is a huntsman whose shortbow is not well-suited to piercing armor. Likewise, the other party member, Jack, is more of an assassin. Perfectly capable of bypassing armor when catching an enemy unaware or staggered, but his dagger will have a difficult time staggering enemies on its own which leaves him vulnerable if he fails to get the drop on an armored orc.
Dylan had not quite finished planning out the journey by the time that Jeff joined. There is still the question of which path they want to take. They have three options. All begin by taking a barge up the river severn:
- Lydney - They can get off at Lydney and trek over an open field into the forest, but a griffin has been terrorizing travelers in the area. Griffins have exceptionally good eyesight and speed, making it difficult for even a fast and stealthy party to reliably avoid it, so this area will not be safe for weaker parties until a stronger party deals with it.
- River Wye - They can switch to a rowboat at Bulwark and row up the River Wye, but the treacherous with shallow water and steep cliffs. The entire party will have to be very mobile with light equipment and good upper body strength to navigate safely.
- Long way - They can journey on foot from Bulwark, entering the forest immediately and staying safe from the griffin. But this will make the journey take much longer, exposing them to the risk of being attacked by random packs of goblins or critters. It will also require either camping in the forest or exhausting themselves before they return to Bulwark. The party is poorly equipped to defend itself from a griffin, which would normally be fought with polearms, heavy crossbows, or firearms. Likewise, they would rather not take the long way, as such a small party with no watchdog will have an extended rest duration due to the need to keep watch. Geoffrey's armor and weapon/shield will make the climb more hazardous than it would be otherwise, but he has enough strength to make up for it. After agreeing on option 2, the party sets out.
Travel
The barge to Bulwark is uneventful, and once there, they stay the night and rent a rowboat in the morning. They then begin up the river.
Adventure Simulator, in its MVP form, does not have any sort of actual simulated boat mechanics. The boat exists entirely in the travel menu, simply checking their upper body strength versus the difficulty of the terrain as they travel. If they were to randomly encounter any enemies during the journey, they would be automatically beached at the nearest spot of land for the battle. Luckily, the party is not ambushed and makes it all the way to the center of the forest without incident.
Navigating Difficult Terrain
From here, they have to ascend some difficult terrain to exit the riverbank and enter the forest. The game loads from the travel screen to a simulation, as this is not a trivial encounter (and unlike the rapids, we can simulate it in the MVP). Geoffrey keeps his gauntlets and pauldrons in his backpack, as they would otherwise restrict his movement, and straps his shield to his backpack. Derthert, likewise, slings his bow around his shoulder. Jack, being both lightweight and strong, quickly scampers up the cliff with a rope and lowers it. Using the rope, Derthert and Geoffrey have no trouble climbing up, the penalty of their equipment offset by the ease of using the rope. The party leaves the rope behind, as they will use it to safely descend back to their boat on the journey back.
The Camp
Continuing their journey in the map screen, they eventually arrive at their destination: the reported location of the orc camp. They load back out of the map screen into the world. But when they arrive, the orcs are not there. The party must make a decision:
- Wait in ambush for the orcs when they return
- Attempt to track them, using Derthert's keen senses to spot recent tracks
The party decides to go with option 1, as the quest was more on the risky side than might be ideal for a party of their strength. The reported number of orcs is 3-5, and orcs are nearly on par with trained humans, so an ambush will put the odds in their favor.
They pick locations for the three of them, placing Geoffrey furthest from the camp due to his lack of stealth and Jack the closest, as his job will be to go in first and pick off as many as he can before being detected. Once in position, the party waits for their targets to arrive. This is done by choosing how long you would like to wait, at most, but will be interrupted if any enemies arrive before then.
Stealth
After a couple hours, the orcs show up with a freshly killed elk in tow. Five of them, to be exact, one of which is in armor. They fail to detect any of the party members due to their exceptionally poor hearing and vision, plus the fact that the party is stationary and in hiding places. The orcs begin to rest, leaving one awake on night watch.
A quiet kill requires a significant surplus of accuracy, as the target must die instantaneously and preferably in melee, allowing the assassin to cover its mouth and lay the body down gently. Derthert is certainly capable of instantly killing the orc on watch with a headshot, but there is a good chance that the others will wake. Thus, it falls upon Jack to take the initiative.
The orc on watch does not have any kind of complex patrol, so sneaking up on him requires only to keep the sound of one's footsteps light. Jack's equipment has no articulated metallic bits, therefore no noise penalties, and his legs have high agility, so he is able to approach silently, though he slows down as he nears his target.
There is a short window of reaction time that an enemy has to being surprised before they can make any sound, so it is not strictly necessary for Jack to kill the orc before he is perceived. Even an orc, with dull hearing, will hear the footsteps of a mere human (as opposed to an elf) immediately behind him. But by then, it will be too late, and Jack's dagger will be lodged in his heart. Sure enough, Jack attacks, and performs a near-silent takedown. Not completely silent, but not loud enough to wake anyone. He moves on to the next.
Orcs do not bother to take their armor off to sleep. Jack would like to eliminate the armored one, but it might not be a good idea. He would likely succeed in killing it, but its high amount of armor coverage translates to a lower surplus accuracy score and therefore a louder kill. Thus, the others will likely wake. He decides to save the armored orc for last and pick off the rest, or at least however many that he can.
Assassinating a sleeping orc is easier than an awake one, all else being equal, but there is a degree of unpredictability to any skill check. In the case of an attack its how accurate he places the cursor to the center of his target's hitbox, but for avoiding noise when moving around, its just random. Unfortunately for Jack, its the randomness that gets him. We don't actually simulate this level of detail, but ostensibly Jack kicked a rock while moving that awoke the nearest orc. Jack does not manage to close the distance and kill it before it begins to lose its flat-footed penalty, and by the time he does attack, he does not have enough surplus attack to perform a silent kill.
The scuffle wakes up the rest of the camp, and Derthert whistles as he nocks his first arrow, prompting Geoffrey to make his way there.
Combat
Derthert fires on the nearest unarmored orc to Jack. The orc, though aware that the camp is under attack, does not actually see Derthert and thus cannot dodge the attack, leaving it at the mercy of Derthert's accuracy. Against a mostly still, unarmored target from close range, there is more than enough surplus accuracy to instantly incapacitate it--the arrow landing right in its neck, leaving only two orcs remaining.
Outnumbered in melee, including against an armored enemy, Jack hoofs it in Geoffrey's direction, followed by the armored orc. The other unarmored orc grabs his shield and faces Derthert.
With a shield, the only way to reliably shoot an enemy is to get lucky with a leg shot or flank them, the latter of which is not currently available to Derthert. He makes a couple attempts, but eventually has to either draw his shortsword or flee. Not being an especially trained melee combatant, but having great endurance, Derthert opts for the latter, and will attempt to juke his way toward Jack and Geoffrey.
Meanwhile, Geoffrey and Jack catch up with one another, and the two prepare to meet the armored orc in melee. Jack is little threat to the orc on his own, but if Geoffrey can manage to stagger it or knock it down with his hammer, Jack will be able to finish him off. The two surround the orc, who at first charges at Jack, managing to dodge the orc's fairly inaccurate attacks with his superior agility.
Geoffrey in turn attacks the orc, and lands a decent glancing blow on its armor. This doesn't do any damage, but does cause some amount of unbalance which will both penalize its next attack and dodge. Ignoring Geoffrey, the orc continues on Jack, being that he is unarmored and unshielded.
Dodging, blocking, and armor are not roughly interchangeable in Adventure Simulator. Dodging is a much worse way to avoid most damage, its advantage is only that it allows you to travel light and is ideal against attacks that are so devastating that armor or block would do little against them anyway, such as the club of a giant ogre. Thus, despite Jack's superior agility, even a glancing blow from the orc's axe is enough to put him out of commission. And sure enough, he presses the dodge button a tenth of a second too late, the orc carving a small chunk of flesh out of his upper right arm.
Without bandaging, the wound will continue to bleed out, and even as it is right now his right arm is practically useless. But, on account of the orc once again ignoring Geoffrey, he is free to land another hit. This time square on the back, causing enough unbalance to stagger the orc. Jack follows up with a kick to the shins, enough to completely incapacitate the orc, toppling him to the ground. A prone character has a very, very low capacity to dodge attacks, but the orc is still fully armored and Jack's dagger-arm is out of commission. Even still, Geoffrey's surplus attack is basically maxed out. His weapon isn't precise, so it can't actually bypass full armor, but he is easily able to land a direct hit on the back of the orc's head.
Orcs are thick-skulled, in both the literal and figurative sense, so this isn't quite enough to knock it out. But he is now completely stunned, which allows Geoffrey the opportunity to restrain him while Jack picks up his dagger with his left hand and slides it through the orc's visor, finishing him off.
It is at this point that Derthert arrives, shielded orc in tow. The orc, seeing its dead captain beside Jack and Geoffrey, suffers a huge morale penalty and begins to flee. Derthert turns, drawing and releasing an arrow. Though not heavily armored like his captain, this orc does still have a chain shirt and helmet, meaning that Derthert will still need some decent surplus accuracy to hit a fleshy bit. Unfortunately he whiffs it and and the arrow hits the back, its damage entirely absorbed by the chainmail and though its force causes some unbalance, the orc is too far away for Geoffrey to exploit this.
After-Battle
Orcs, having very high strength, can easily outsprint most humans. But a hunt is often a function of endurance, not speed, especially when you're already wounded. The party can take their time tracking the orc, giving them the opportunity to bandage Jack and gather their packs before setting out to finish off their quarter.
Geoffrey has a little Anatomy training, enough to apply a bandage and tourniquet. He staunches Jack's bleeding and splashes some alcohol on the wound for good measure.
As the party leaves the area and enters the travel screen, they see a summary of all the loot. The process of actually managing your inventory is largely automated, you define only a minimum weight/value ratio and how much total weight you're willing to carry. These parameters are used for your characters to automatically decide how much loot to take, prioritizing the lightest and most valuable loot first. But you can override this, in this case Geoffrey decides to bring the orc's latest kill, the elk, even though it will significantly encumber him. He's in no rush anyway, as Derthert still needs to track down the final orc.
Tracking
From the map, the party can see the lone orc's tracks quite vividly as it flees. Every party leaves behind tracks which are visible for a duration depending on the party's size and stealth versus the tracking party's track skill and eyesight. At this point, the party splits, with Jack and Geoffrey heading back to the cliffs by their boat and Derthert following the orc.
The orc doesn't get far before becoming exhausted. He is wounded, wearing chainmail, and does not have especially high endurance to begin with. Derthert engages several times, the first couple times he is detected and unable to ambush, retreating each time. But eventually he catches his prey unaware and loads back from the map screen into the simulated world. He lets off an opening arrow on the orc's shield arm, forcing it to drop its shield, then its leg, crippling its movement, and finally square in the neck. He collects his arrows, then returns to the map screen to regroup with Geoffrey and Jack at the cliffs.
When one party is in a simulation and the others are not, they cannot advance time on the map screen due to the potential desynchronization that would occur. Essentially, both sub-parties need to declare their intentions, then when they're both ready, time jumps forward until their next encounter. This is a fairly complicated edge case, though, and we can omit the ability to split the party for the MVP. They can also disband the party, but this would make them unable to regroup without first returning to a settlement. Jack and Geoffrey chit-chat during Derthert's encounter, then continue once he returns to the map.
Descent
The party rendezvous at the cliffs and must now figure out a way to get Jack down safely, as his injury will impose a significant penalty to climbing, even with the rope and the fact that he'll be descending. They settle on having him grab onto Geoffrey with his left arm, who is still able to make the climb check despite the significantly higher weight because he is now descending and has left most of his equipment at the top of the cliff. Geoffrey then climbs back up to pick up his equipment as well as Derthert's pack, and descends once more. Derthert then unties the rope and scampers down the cliff to join them.
The three adventurers return to their rowboat, one man short for the rowing, but will now be headed downstream which is much easier. They return to Bulwark safely, then take the next barge to Newport and collect their reward by turning in five right orc ears.
Aftermath
Jack, having been wounded, is entitled to a disproportionate share of the bounty and proceeds from the loot to compensate for his injury. They otherwise typically divvy it up according to how powerful each character is according to the favor required to create them. In this case, the characters are approximately equally powerful, so Derthert and Geoffrey take equal shares. But Geoffrey specifically wants favor with the elves, while Derthert and Jack are indifferent, so he agrees to receive all of the favor and little of the money. This gives him enough favor to create a custom elf character.
Jack convalesces for a couple weeks before his arm returns to normal, and can either play as another of his characters in the meantime or skip right through it, as time between player characters is not synchronized when they aren't in a party.
Derthert, having gained an appreciation of how difficult it can be to deal with even moderately armored targets with a shortbow, uses his money to buy an elven bow, which is something like a fantastical version of a compound bow that allows him to reliably penetrate chainmail with direct hits (though not plate).
This page outlines the heuristics by which this game should be designed and implemented. The details specified in all other pages can be freely changed as long as you believe that it better fits the considerations outlined here.
Concept
The goal of this game is to make you feel like an adventurer in a believable world.
- Make/pick a character
- Take a quest
- Gather a party
- Prepare for your journey
- Venture forth
- Encounter unexpected challenges
- Finish the job
- Return for your reward
- Repeat step 2 until you die, in which case repeat step 1. We may want some kind of progression between characters later on as well as the ability to create multiple characters and switch between them, leaving each other their estate upon death or something, but this is the basic idea. In the long term, we would love to have this become some large-scale world simulator in the vein of Dorf adventure mode or Mountain Blade, as well as large-scale networking features, but for now the scope will remain limited.
Right now, we aren't too concerned about making the content especially interesting. Only the features. So what content there is can be lazily procedurally generated. But when we establish a fun formula we will eventually either focus on making the procedural generation more interesting or creating tools for players to create their own content. We want there to be a blurry line between developers and players and harness the latent potential of the modding community by offering players/modders an open-source, consistently designed platform.
Design
Realistic With Caveats
The underlying game mechanics should always be implemented in a "realistic" manner, but when we encounter a realistic mechanic that isn't fun, we should either account for this in the content or abstract over it.
Content-based approach
Suppose we are implementing a stealth system and we find realistic eyesight to be unfun (like getting spotted 200ft away under moonlight). Instead of making humans worse at detecting, we simply contrive enemies with worse eyesight. Therefore orcs, goblins, skeletons, and the like have really bad eyesight for some reason. Conversely, we can also simply give the player supernatural stealth methods like magical invisibility or a chameleon cloak.
The goal here is that since the underlying game mechanics are physically based, we have a point of reference to balance the game off of (the real world). This allows different types of gameplay to co-exist. If you want to have a really realistic, punishing time, play as a normal human fighting normal humans. If you want something else, fight enemies that are more fantastical with a character that is more fantastical. This should also help avoid the scenario where developers realize that a gameplay system is poorly balanced, change it, then retroactively break all the old content designed with that poor balance in mind.
Abstraction-based approach
Suppose we are implementing travel in the overworld and we find realistic travel times / world scales to be unfun (holding W for 10 hours on a road to get to a rest stop). We should instead use the interface to skip over these segments, therefore a fast travel system. It would still actually simulate the travel in a realistic manner though, so you will need to have food and rest and all that.
What if micromanaging food is tedious?
Same solution, abstract it in the interface. I don't really care whether my character eats his grains or his jerky or his rice. I just want to know how many days worth of food he has, he can automatically eat it as he goes. When I go back to town, I just want to give him a food budget and he chooses whatever level of nutrition is suitable for it (like 10 days on a cheap budget means grains, 10 days on a luxurious budget means jerky).
The abstractions however should be fairly scalable. If you want to walk for 10 hours to your destination, you can. If you want to manually pull food out of your inventory and eat it, you can. The lowest level of abstraction should be very precise, down to the level of which hand you are holding something in or which pocket you put it in.
The current strategic inventory rows represent contents packed in a character's backpack (or the party chest), rather than items immediately retrievable from the character's person. Immediate-access slots and precise hand/pocket placement remain the intended lower-level model; the backpack interface is its current high-level abstraction, not evidence that every carried item is equally accessible in a tactical scene.
World and Lore
"Kirkland Signature"
When you go to Costco and buy Kirkland Signature peanuts, you do not get xtreme flavor blasted japapeno nacho-cheese chocolate-covered peanuts. You can get salted peanuts or unsalted. Maybe you will like the flavor-blasted peanuts more, but you know that the Kirkland Signature ones will at least be solid. Their brand represents that which is generic, but high-quality. These aren't Great Value (Walmart) peanuts, they're Kirkland Signature. That is to say, we will be making our world very generic, but it should still be high-quality. Think Tolkien, or a more toned-down version of Warhammer Fantasy.
Historical Fantasy
The world is Earth and the year is 1544, but there are also inexplicable fantasy elements added everywhere. There's elves and goblins in the forests, dwarves and orcs in the mountains, and vampires and undead in the crypts. But the human kingdoms and empires are the same, only now King Henry VIII has dragons flying around his kingdom causing mayhem and he needs YOU, noble knight, to stop them!
In general though, the fantastical elements and historical elements should try to avoid stepping on each other's toes too much. The human kingdoms regard things like griffins and orcs as nuisances, best dealt with by hiring brave adventurers from ahistorical fantasy factions, not as existential threats of relevance to the historical record. Likewise, elves and wizards don't much care which duke is sqabbling over which patch of dirt, they have evil liches to slay and cults to root out.
Therefore, most of our "lore" is just actual history. For the fantasy stuff, there will certainly need to be actual lore written, but that can come later as the game becomes more fleshed out. Its not really necessary to understand the origin of the curse of vampirism in order to be an adventurer who goes on a procedurally generated quest to slay a vampire.
This sounds too generic...
In being a platform for players to create content, eventually we imagine people might use it to make interesting, bizarre worlds. The core gameplay systems that enable this generic fantasy game should also basically work the same in a modern world of firearms and computers or a sci-fi world of spaceships and energy weapons. An AR15 is a very accurate arquebus that fires and reloads extremely quickly. A plasma rifle is an AR15 whose projectiles explode and melt through armor. A car is a faster horse, kind-of. A spaceship is a car that moves in three dimensions. There will of course need to be a lot of work done to support these systems, but in being open-source we hope that someone who would otherwise make their own sci-fi game from scratch might find it easier to extend ours to support it. But all of this is way, way down the line. For now we are making a generic fantasy adventure simulator.
Minimum Viable Product
These features demonstrate everything needed for the basic gameplay loop. It won't necessarily be a very fun game at this point, but gives an idea of the potential once each of these barebones systems are fleshed out more.
- A dude can fight another dude in standing melee combat
- Prone/supine controls, can be knocked down and get back up
- Can pick up and fight with different types of melee weapons
- Ranged combat (server authoritative, no rollback)
- Advanced movement (climbing, sliding on sloped surfaces, navigating hazardous terrain like fording a river)
- Stats system (attributes, skills, track damage to different body parts)
- Slot system
- Empty world from 1500s geological data
- Populate world with settlements extrapolated from population data (settlements are just a coordinate, name, and population level for now)
- Generic humanoid enemy types like orcs, goblins, and bandits
- Travel system with random hostile encounters
- Randomly generated quests (see: Battle Brothers for good templates)
- Rest system for health recovery
- Inventory management (loot/buy/sell items)
- Food/water/sleep system
- Food and water strategic needs and automatic travel provisioning are implemented; sleep remains.
Polished Product
With these features, the game becomes something that we can imagine players actually wanting to pay for. A fun, unique product rather than a mere tech demo.
- Procedural modeling plugin
- In-game editor for procedural models - design your own clothes or equipment
- Urban levels (houses, castles, etc and quests that involve them like thievery or assassination)
- Improved stealth detection AI (investigate noises, raise alarms, patrol routes?)
- Level editor - design your own house
- PVP
- More humanoid enemy types: beastmen, undead, ogres, and trolls
- Non-humanoid enemy types like wolves or giant spiders
- More detailed downtime (pick a job to earn a wage at)
- Elves and the immortal character system
- Also need to make settlements and assets for them
- Character creator
Simulation
Not required for the basic gameplay loop or polish, but increase the verisimilitude of the world and create opportunities for emergent storytelling.
- Settlement prosperity that is based on population and reduced by unsolved quests (see: Battle Brothers)
- Prosperity affects what items can be bought and how expensive services are
- Disease system
- Marry other characters and have children (Mount and Blade II: Bannerlord has a barebones implementation of this)
- Celebrations like holidays and birthdays - invite friends to hang out at your house
- Werewolf/Mafia/SS13/Among Us-style quests where your job is to infiltrate and do something nefarious at a rival's celebration
Not in roadmap but could be
These are all neat but aren't required for the game to feel complete. However, if someone on the team is very passionate about one of them then we can prioritize it.
- Magic system, implement on a per-element basis in order of whatever is easiest
- Wind magic is probably the easiest, just force fields in Avian that also cause unbalance. Will look much cooler with dust/leaf particles.
- Light seems fairly easy, as long as its just for illumination and some kind of blinding effect
- Shadow would be invisibility, especially in darkness
- A simple version of earth can be just causing tremors that stagger enemies (as opposed to something more complicated like fissures)
- Fire requires its own system for fire spreading and temperature
- Frost would use the opposite side of the same temperature system
- Nature to summon the assistance of wild animals in combat sounds easy, but not anything involving plants
- Lightning seems very complicated to do well (Circuits? Nope.)
- Water seems nightmarishly complicated to do well (Fluid simulation? Fuck no.)
- Become a vampire/necromancer/lich
- Changes the way a lot of systems for your character work, like food, water, sleep, exhaustion, and health
- Become biologically immortal, but still permanently killable unlike an Elf
- Don't need the game to give you quests, every night of feeding/graverobbing is essentially a self-driven quest
- Game generates PVP quests for other players to destroy you as your infamy increases
- Player-run factions
- Basically a faction board/groupchat + shared asset ownership with quests to antagonize rival factions
- Factions can manage a settlement as the mayor/governor/etc
- Would be like being moderators on a forum, plausibly a semi-democratic election for settlements in Northern Italy. Players in other settlements would need to depose bad mayors by force or get invaded
- Cultists and demons
- See: Space Station 13
- Due to the bulletin board social nature of the game, this seems like it might be a significant source of memes.
- Probably the best "outlet" for players with... "indecent" inclinations...
- If we let players customize characters and their outfits, they are going to make ridiculous coomer characters. Moderating this will be an uphill battle.
- We can just own it and tell them do whatever they like within their secret cults, and if they expose their degeneracy to anyone else who reports them then the player-run witch hunter/holy paladin faction will be given quests and empowered to come and purge them unannounced during their sex parties
- Dwarven settlements and assets
- Option of using the underway to travel in addition to the overworld and sea. Much faster through difficult terrain, but also much more dangerous
- Mounts and mounted combat
- Basically when you are charging and are holding the attack button, it can be automatically made against any enemy that comes within reach
- Horse can trample over smaller enemies
- Very high morale effect causes most weaker enemies to route instead of hold their ground
- Huge monsters that require a more detailed combat system
- Dragon, cyclops, giant, griffin, cockatrice, chimera, etc
- Dragon's Dogma does a great job of this... but it looks insanely hard to implement well
- You can climb onto enemies to attack weak points
- Detailed hitboxes for all the different body parts
- Lots of variety in the attack animations
- Total Warhammer does a workable job of this that looks much easier to implement
- They're fast, very tanky, and have sweeping attacks, but their size makes them vulnerable
- Poke them with polearms while ranged units shoot overhead
- Alternatively, anti-large hero characters are good at dueling them due to being able to reliably dodge their big, telegraphed attacks
- Flying units just magically float, their animations give the illusion of more detailed physics
- When their huge attacks send characters flying, they aren't real full-body colliders and ragdoll physics, everyone is still a capsule and the animations give the illusion of detailed physics.
- Poisons, potions, alchemy, herbalism, and gardening
- "We want the Stardew Valley audience"
- Settlements outside of Italy
- Assets between European countries are easy to share, need some kind of set of language skills that give you severe trade penalties when no one in your party speaks the local language
- Can retroactively apply this to communication with Elves/Dwarves
- Arabia, Africa, the East, and the New World need lots of unique assets, don't bother until Europe is very fleshed out (maybe modders will do this?)
- Assets between European countries are easy to share, need some kind of set of language skills that give you severe trade penalties when no one in your party speaks the local language
Game layers
Adventure Simulator alternates between an asynchronous strategic game and a real-time tactical game. The split exists to make realistic travel, recovery, trade, and social play practical without pretending the whole world is one continuous simulation.
Strategic layer
The strategic layer is the persistent world. Players create and manage characters, organize parties, travel between settlements, investigate quests, trade, rest, train, and manage inventory.
Time advances in explicit chunks when a party travels, rests, trains, works, or performs another strategic action. The interface is a server-rendered hypertext application rather than a freely traversable 3D overworld.
Tactical layer
Combat and other immediate physical challenges use a private real-time Bevy simulation for the participating party. The server owns gameplay state and the client owns input and presentation. Positions, enemies, damage exchanges, and other live simulation details disappear with the tactical session; only its validated strategic consequences survive.
Quest combat and strategic incidents currently use this handoff. Tactical stealth and hazardous-terrain scenes remain future extensions.
Client and model layers
The tactical client renders replicated state, sends input, and may begin animations immediately to disguise network latency. Client animation never changes the authoritative outcome.
Procedural model generation is a separate concern. Gameplay code should depend on stable physical and presentation inputs rather than a particular finished mesh or animation.
Shared rules
Strategic autoresolve, tactical play, content validation, and simulation tools share dependency-light Rust calculations for attributes, skills, equipment, combat, health, and other durable mechanics. Sharing a calculation does not move authority between layers.
For the current technical boundaries, persistence rules, transport, and trust model, see Architecture. For deployment topology and networking design, see Networking.
Networking
Adventure Simulator deliberately avoids a continuously simulated MMO overworld. Most players interact with persistent places and people through ordinary strategic web pages, while a temporary private server is created only for real-time play.
Strategic experience
Being “in” a settlement means that the character's persistent strategic location is that settlement. Many players can read and act there without sharing a real-time physics simulation.
Strategic actions use ordinary HTTP requests. Server-sent events refresh relevant page regions when persistent state changes, but the complete page remains usable through normal links and forms. Browsers never connect directly to SpacetimeDB or receive its credentials.
This model should scale by partitioning or replicating strategic services without changing the player-facing metaphor. A crowded city is primarily a busy shared bulletin board, not ten thousand networked bodies in one scene.
Tactical experience
A tactical server is provisioned for an immediate physical encounter. It is authoritative: clients send input and receive replicated state, with no peer-to-peer authority.
The current client and server communicate over WebSockets. Local animation may respond eagerly to input, but it cannot decide hits, movement, damage, enemy state, or mission completion. When the session ends, clients return to the strategic interface and only validated durable consequences are committed.
There is no PvP in the MVP. Future PvP should preserve the same priorities:
- a server-authoritative outcome;
- bounded advantage from dishonest client timing or precision;
- no requirement to persist tactical ticks in the strategic database;
- animation techniques that hide latency without granting authority.
Accounts and authorization
The desired product should let a new player begin with minimal account setup and attach recovery or identity credentials later. That product design is not the current authorization model.
The current local strategic surface uses a selected-character cookie and a trusted gateway identity. It must not be treated as a public multi-user account system until explicit player-to-character ownership is implemented.
See Architecture for the current transport, credential, subscription, and tactical lifecycle boundaries.
Architecture
Static item definitions cross the strategic boundary as a build-time embedded
catalog. YAML is authoring input only; the flattened SpacetimeDB Item table
is its deterministic persistence/client projection. Inventory ownership,
custody, amount, and condition remain strategic state and are not definition
content. See Item definition authoring.
Adventure Simulator separates persistent strategic play from transient real-time tactical play. The boundary is architectural, not merely a difference between screens:
- Strategic state lives in SpacetimeDB and advances through discrete, authoritative actions.
- Strategic presentation is server-rendered by
strategic-webwith Axum, Maud, and Datastar. - Tactical state lives only in a short-lived headless Bevy server and is replicated to the Bevy client over server-authoritative WebSockets.
- Shared rules live in dependency-light Rust crates so strategic autoresolve, tactical play, validation, and tools can use the same calculations without sharing persistence.
Persistence boundary
SpacetimeDB stores durable world and character facts such as:
- characters, parties, progression, schedules, injuries, and needs;
- inventory, equipment condition, currency, and custody;
- settlements, organizations, journeys, cases, contracts, and investigations;
- mission requests, authenticated outcome receipts, final battle results, and finalized loot;
- compiled world identity, source manifests, and strategic route facts.
Tactical servers keep positions, movement, enemies, attacks, per-tick damage, temporary health, physics state, and scene entities in memory. Those values are not mirrored into SpacetimeDB. A tactical session may report an authenticated terminal result, but strategic authority decides which durable consequences that result is allowed to create.
Random encounters are durable strategic journey events only at their boundaries: timing, route position, available choices, and final outcome may be persistent, while the combat simulation remains transient.
Strategic authority
The SpacetimeDB module owns authoritative mutations. Reducers validate the current party, character, location, time, custody, and source identity before changing state. Shared-core functions perform deterministic calculations, but do not grant authority by themselves.
The current deployment boundary is intentionally narrow:
strategic-webholds the registered strategic-gateway identity;- browsers submit ordinary HTTP actions to
strategic-weband never receive a SpacetimeDB credential; - a selected-character cookie chooses presentation context but is not yet a complete player-to-character authorization model;
- non-loopback use therefore requires the explicit insecure-development opt-in until account ownership is implemented;
- tactical dispatch claims are one-use, digest-bound capabilities rather than copies of the gateway token.
Observer-specific truth remains private. Browsers receive only gateway-filtered views of contracts, dialogue, investigations, evidence, physiology, and case-site knowledge. A public or subscribed row is not automatically safe to use as an authorization decision.
The gateway settlement-NPC roster is likewise an explicit player-visible projection rather than the authoritative population row. It includes stable identity, home settlement, visible description, occupation, household, local role, service, and conversation routing. It omits private demographic sex and the internal projection traversal key. Browser quest discovery builds its candidate and commitment from visible age, presentation, profession, role, and presence. Presentation is committed as seen but is not interpreted as private sex; the public developer flow leaves that selector empty.
Strategic web
strategic-web renders complete HTML documents for direct requests and
no-JavaScript clients. In an enhanced session:
- one document-scoped Datastar SSE connection carries invalidation revisions;
- same-origin strategic navigation requests a server-rendered replacement for
the stable
#strategic-pageroot; - enhanced forms execute the same reducer as ordinary forms and receive the redirected strategic root in the original response;
- native links and
303redirects remain the fallback.
The web process owns one generated SpacetimeDB SDK WebSocket connection. Its explicit subscription invalidates live regions and supplies a small, typed public read cache. Private and owner-scoped page data remains authenticated on-demand SQL until a page has a dedicated authorization-scoped read model. See Strategic read cache.
First-character onboarding is a separate entry surface. The browser tab holds a private seed for deterministic candidate previews keyed by life stage. Confirmation sends only the generator version, seed, selected age tier, and slot; strategic authority regenerates and persists that exact candidate. Confirmed or selected character IDs are remembered in a bounded, browser-scoped roster cookie. The strategic header resolves only remembered, non-temporary rows into its character switcher. Both the roster and selected-character cookies are local selectors, not authentication or ownership proof.
First-character authority
First-character generation is a versioned strategic authority boundary. The
age tier (young, adult, or old) is part of the GET, POST, reducer claim,
and generated-ID coordinates alongside the private session seed and slot.
Professional previews are regenerated from the organization catalog and the
same package is persisted on confirmation; the browser never submits trusted
attributes, skills, inventory, religion, or membership rows. Creation inserts
the character, skill and equipment package, current organization membership,
presentation, dues, and required professed religion in one reducer
transaction. Tactical state is not involved.
The public confirmation reducer is restricted to the registered strategic gateway. Membership timestamps are anchored to the character's initialized strategic minute, including the first paid dues interval, rather than assuming that a new character always starts at minute zero.
Tactical lifecycle
The current tactical stack uses Bevy Replicon over Aeronet WebSockets:
- strategic authority creates a pending tactical request bound to a party, scene, mission authority, and required objective;
- the dispatcher receives the request through its SpacetimeDB subscription, provisions a one-use claim, and starts a headless tactical server;
- the child consumes that claim, registers its identity, and opens the WebSocket server;
- party members connect through the Bevy client and send input;
- the tactical server keeps all live simulation state in memory;
- the child calls
end_tactical_serverwith its terminal resolution; - SpacetimeDB validates the registered server and mission, selects any compatible private strategic outcome, and commits durable consequences idempotently.
The current combat prototype calculates attacks but does not yet apply tactical damage, so no client-controlled path can emit authoritative enemy deaths. Required-kill missions therefore fail closed unless the server's future combat pipeline emits the internal authoritative death event.
Mission, hostile-group, battle, and outcome-source identities are separate. Tactical success never chooses a case objective, capture subject, contract state, or reward. See Quest authority.
Authored content
Repository-authored organizations, dialogue, quests, investigations, and bestiary records are validated and embedded during their owning crate's build. Production services deserialize the compiled catalogs at startup; they do not interpret loose deployment YAML. Stable IDs cross persistence boundaries while display names remain presentation.
Generated cases retain the catalog revision and deterministic context used to create them. Canonical truth, weights, hidden evidence thresholds, witness reliability, and generation traces remain private. See Quest generation and investigation.
World compilation
Raw historical and geographic sources are native build inputs, not database
tables. adventuresim-world-import parses source-specific formats and compiles
them through typed stages into the dependency-light
adventuresim-world-schema model. The current canonical artifact uses world
schema 25 and inference rules 9.
The playable region, spatial grid, source identities, inference versions, and
compiled records contribute to artifact identity. Import is resumable only for
the same artifact. The local just load-world workflow therefore explicitly
reset-publishes its selected loopback adventuresim-* database before loading
the pinned artifact; all existing data in that database is disposable and
discarded.
Normal development consumes the separately pinned compiled runtime bundle:
world-1544.json;- the schema-5 strategic map manifest and AVIF tile pack;
- the final schema-6 terrain-routing pack;
- generated licensing and source notices.
The strategic map is presentation data served from immutable file-backed artifacts. Dynamic settlement, party, quest, and route overlays remain server-rendered. The terrain-routing pack is a native strategic planning input; its raster cells and A* search state are never persisted in SpacetimeDB.
Source-specific contracts live in the World Data section of the wiki, beginning with Source manifests, World-data bundles, and Viabundus.
Detailed system references
- Development workflow
- Quest authority
- Quest generation and investigation
- Bestiary authority
- Dialogue architecture
- Measured inventory
- Physiology
- Organizations
- Strategic simulation
- Strategic route terrain
Development Workflow
Item content
Item YAML uses the production build validator. Run just content-check for all
compiled core catalogs plus dialogue, or just content-check items while iterating on
content/items/*.yaml; see
Item definition authoring.
Organization content
Organization YAML is validated during adventuresim-core builds. Validate its
settlement references against the canonical or another exported compiled
Viabundus world with:
python scripts/validate_organization_world.py --world path\to\compiled-world.json
See organizations.md for the schema and authority boundary.
Developer quest spawning
The existing browser-local developer mode is off by default. On settlement pages it reveals a top-right quest-authoring button; it never appears at camp or case sites. The resulting quest remains undiscovered until ordinary tavern/NPC rumor delivery.
This is not an authorization boundary. The HTTP endpoint and
spawn_developer_quest reducer intentionally have no developer credential yet,
so they must not be exposed as an administrative tool on an untrusted
deployment.
The editor, authorization limitation, generated authority, and discovery model are documented in Quest generation and investigation.
Quest content
Quest and bestiary content lives in content/quests/*.yaml using the same
strict JSON-compatible YAML convention as dialogue. Validate it with:
cargo run -p adventuresim-core --bin questgen-check -- validate
The complete authoring and validation contract is documented in Quest generation and investigation.
Simulation and quest evaluation
For deterministic multi-year NPC balance experiments and replay commands, see
strategic-simulation.md, just strategic-sim, and
just test-strategic-sim. The isolated just strategic-sim-core-loop <new-output-dir> command
also evaluates the authoritative strategic incident, escalation, recruitment,
and quest systems. See
strategic-simulation.md.
The separate end-to-end web evaluator is LLM-only and drives the same visible
controls as a player. With a local strategic server running, invoke
just quest-web-eval quest-browser-run-001. It saves index.html,
manifest.json, and a chronological PNG after every action. It requires
Playwright's Chromium browser (npx playwright install chromium) and reads the
model credential from OPENAI_API_KEY by default.
The opt-in authoritative integration driver is
just strategic-sim-core-loop-world <new-output-dir> loads the pinned
target/world-1544.json rather than sample/renderer data and is preferred for
gameplay evaluation. Both recipes create, claim, and delete their own
nonce-named loopback database, compile a one-run bootstrap capability in
memory, and accepts no host, database, or capability override.
The current strategic/tactical boundaries and tactical lifecycle are documented in Architecture. This page is the canonical home for local commands, prerequisites, and operator-safe development workflows.
Quick Start
just dev
Open http://localhost:8080
Ordinary just dev / just web startup publishes without deleting database
data. Publication failures stop startup before the tactical spawner or web
process starts and print the server/database identity plus recovery choices.
Canonical reset recipes are intentionally disabled.
For a disposable demo or worktree, use an explicit isolated profile:
just web-isolated renderer-demo 23100
The profile name is restricted to lowercase letters, digits, and hyphens, and
the base port must leave room for the web and tactical ports. The recipe derives
a stable fingerprint from the resolved worktree path and includes it in the
database name and profile directory. State lives below the current user's local
runtime/cache directory rather than shared /tmp; directories and metadata are
owner-only and symlinks/path escapes are rejected. Thus the same human-readable
profile in two worktrees still has distinct database, data, logs, and process
identities. The three loopback ports remain explicit, and startup fails if any
is already occupied.
The Python lifecycle process holds an exclusive profile lock from the first
port check through web-server exit. It records each child process's resolved
executable and OS creation token, checks that identity throughout readiness and
immediately before reset-publish, and uses the same identity for cleanup. It
will not treat an unrelated listener as its SpacetimeDB or signal a reused PID.
Only this guarded workflow may pass
--delete-data=always; it rejects remote servers, non-loopback binds, mismatched
database names, and unsafe profile strings. It stops its own SpacetimeDB and
spawner when the foreground web process exits. The isolated database files are
retained under the fingerprinted profile directory for inspection and are reset
the next time that exact worktree/profile is run.
The public dev_stack.py publish command is always non-destructive. Reset
publication is not a CLI option: it is an internal lifecycle operation that
requires the held profile lock and re-verifies the captured standalone listener
identity immediately before invoking SpacetimeDB.
Full Development (with Tactical Servers)
To run the complete stack with automatic tactical server spawning:
Terminal 1: Start SpacetimeDB, the strategic web server, and tactical spawner
just dev
Terminal 2: (Optional) Rebuild the WASM client independently
just build-wasm
Now when you click a location in the browser, a tactical server will automatically spawn.
Strategic-Only Development
For strategic-layer work, start SpacetimeDB and the server-rendered browser UI without building the tactical WASM client or tactical server binaries and without running the tactical dispatcher:
just dev-strategic
This preserves the canonical local database just like just dev. It also
stops a canonical dispatcher left by an earlier full-stack run. Tactical
missions can still enter the pending state, but they will not start until the
full stack is running again.
For a disposable, worktree-safe strategic-only database, use:
just web-isolated-strategic renderer-demo 23100
The isolated lifecycle retains the same guarded reset, ownership checks, and
cleanup as web-isolated, but it neither reserves the tactical port nor starts
a dispatcher.
For native tactical testing from WSL on Windows, the equivalent of running
just dev, just tactical, and just client 0 in separate Linux terminals is:
just win-dev
This runs the strategic stack in WSL, cross-compiles and stages the tactical
executables in E:\adventure-sim-dev, then starts one native Windows tactical
server and client 0. Press Ctrl+C to stop the web process and Windows tactical
processes; the detached SpacetimeDB and tactical dispatcher follow the normal
just dev lifecycle and can be stopped with just stop. The recipe installs
the pinned toolchain's x86_64-pc-windows-gnu Rust target when needed; the WSL
package gcc-mingw-w64-x86-64 must already be installed.
Requirements
- Rustup (the repository's
rust-toolchain.tomlautomatically selects the pinned nightly toolchain and required components) - just (
cargo install just) - SpacetimeDB CLI 2.6.1:
curl -sSf https://install.spacetimedb.com | bash, thenspacetime version install 2.6.1andspacetime version use 2.6.1 - Python 3
- Node.js 20 or newer (strategic browser behavior tests)
- wasm-bindgen (
cargo install wasm-bindgen-cli) - for WASM builds - Caddy (for the HTTPS HTTP/2 development entry point)
Run spacetime login before starting the strategic web stack. Canonical local
startup reads the authenticated token without printing it and passes it only to
the trusted strategic-web child process.
The justfile and repository automation do not require Bash. Stateful or
compound recipes are implemented in Python, and simple recipes are compatible
with the host shell. On Windows the default interpreter is python; on other
platforms it is python3. Set PYTHON_BIN when the interpreter has a different
name or lives outside PATH.
Services and Ports
| Service | Port | Description |
|---|---|---|
| SpacetimeDB | 3000 | Strategic database |
| Strategic web | 8080 | Axum server-rendered browser UI |
| Strategic web HTTPS | 8443 | Caddy HTTP/2 and HTTP/3 entry point |
| Tactical Server | 6000+ | Game server (one per mission) |
Core Commands
# Development
just dev # Start the complete browser stack
just dev-strategic # Start only SpacetimeDB and the strategic browser UI
just web-isolated # Reset and start an explicitly isolated local profile
just web-isolated-strategic # Reset and start an isolated strategic-only profile
just web-secure # Start strategic-web at https://localhost:8443
just secure-web-trust # Trust Caddy's local development CA (normally once)
just spawner # Run tactical server spawner
just tactical-isolated # Start a disposable tactical database and request
just client # Run a native tactical client
just build-wasm # Build WASM client
# Testing
just test # Run native Rust/browser tests and validate the SpacetimeDB module ABI
just test-chat # Run only the strategic chat behavior tests
just test-schedule # Run only the training-schedule editor tests
just test-dev-stack # Test local workflow policy without writing bytecode
just tactical # Run a single tactical server (for testing)
just status # Check service status
just stop # Stop all services
# Workspace verification
just fmt # Format all Rust workspace packages
just check # Check all Rust workspace packages
just test # Test native Rust packages and build the SpacetimeDB module
just lint # Run Clippy with warnings denied
# Building
just build-strategic # Build the SpacetimeDB module
just build-tactical # Build adventuresim-tactical-server and adventuresim-tactical-server-dispatcher
just build-wasm # Build the browser tactical client
just build-all # Build everything
# Database
just publish # Publish SpacetimeDB module
just generate-db-client # Regenerate and format the Rust client bindings
just verify-db-client # Fail if committed bindings differ from the module ABI
# World-data source
just init-world-data # Install the pinned full input bundle, including Viabundus and HYDE
just init-world-runtime # Install the small compiled world/map runtime bundle
just verify-world-data-bundle /path/to/archive.zip /path/to/archive.release.json <published-descriptor-sha256> # Verify a reviewed input collection
just install-world-data /path/to/archive.zip /path/to/archive.release.json <published-descriptor-sha256> # Install it without source-by-source downloads
just build-base-terrain # Build documented-road-only inference terrain
just compile-world # Build base terrain, then compile the 1544 world
just build-strategic-map # Build base, world, and final map/terrain artifacts
just normalise-viabundus # Compatibility alias for compile-world
just load-world # Recreate the canonical local database and load it
just load-world http://127.0.0.1:24610 adventuresim-dev-example # Recreate and load an isolated profile database
just test runs the strategic browser tests and the native Rust test suites,
excluding adventuresim-stdb-module. It also runs spacetime build to validate
that module against the SpacetimeDB host ABI. Native linking cannot provide that
host ABI, and including the module would enable its shared schema feature for
the entire workspace. Its pure strategic calculations live in
adventuresim-core and are covered by native unit tests. Reducer integration
tests require a running SpacetimeDB environment.
The workspace pins both the module crate and Rust SDK to SpacetimeDB 2.6.1.
Before building or generating bindings, verify the active CLI with
spacetime --version. Binding generation uses spacetime generate --module-path and intentionally excludes private tables. Both native tactical
and WASM builds run just verify-db-client, which generates and formats into a
temporary directory and compares the result without changing the checkout.
Schema changes are clean pre-launch changes. Regenerate client bindings and
recreate the development database rather than adding a migration or
compatibility path. Routine just dev, just web, and just publish preserve
data. just load-world is the explicit destructive exception: it accepts only
a bare loopback server and a lowercase adventuresim-* database, reset-publishes
the current module, and discards all existing data before importing the pinned
world. web-reset and publish-reset remain unavailable; never pass
destructive publish flags manually against a public or player-bearing database
without explicit approval and a verified recovery copy.
web-isolated owns its loopback server and database, reset-publishes, reseeds
the normal world and visual test fixtures, and discards only that profile's
contents. Its data remains available in the profile directory for inspection
until the next run resets the same profile.
bootstrap_development_world is itself idempotent: it inserts only missing demo
rows. The isolated profile seed workflow then resets Sick Demo and its party
of staggered patients plus a high-Physiology physician so symptoms, diagnosis,
and treatment can be tested immediately. It propagates every reducer failure
instead of treating arbitrary errors as evidence that seeding already happened.
Individual fixture reducers are not published. The isolated profile launcher
creates a 256-bit token, compiles it into that disposable module build,
publishes, invokes the single development-bootstrap reducer, and removes the
token from child-process environments. For a manual disposable publish, set
ADVENTURESIM_DEV_BOOTSTRAP_TOKEN to a 64-character hexadecimal value while
publishing, then pass the same value to scripts/dev_stack.py seed --token.
Spawner metadata contains the resolved repository, profile, server/database, bind/port configuration, hashes of both tactical binaries, actual executable, PID, and OS process-creation token. Start, reuse, and stop are serialized under the profile lifecycle lock. A live process with missing or different metadata is rejected, so a worktree cannot silently reuse another checkout's dispatcher, an out-of-date build, or a recycled PID. Confirmed-dead metadata is safely replaced.
World data workflow
Most developers should use the pinned compiled runtime rather than download and rebuild the full geospatial source collection:
just init-world-runtime
just load-world
just load-world installs the runtime bundle when absent, destructively
reset-publishes the current module into the selected loopback
adventuresim-* database, and then loads target/world-1544.json. This
deliberately discards characters and every other existing row so the database
schema and compiled world always match the checkout. Stop any web process using
the database before loading, then restart it afterward. Pass an isolated
profile's server and generated database name explicitly when targeting that
profile.
Rebuilding world artifacts
Install the reviewed source-separated input bundle only when changing or auditing world generation:
just init-world-data
python scripts/init_viabundus.py --force
just build-strategic-map
The explicit Viabundus initializer adds supplementary upstream inputs, including
water-1500.csv, that are not part of the bounded five-CSV bundle component.
Individual source initializers and verifiers remain available through
just --list for focused work.
The build chain is:
just build-base-terraincreates the documented-road-only inference pack.just compile-worldcompiles and validatestarget/world-1544.json.just build-strategic-mapproduces the schema-5 map manifest, AVIF tile pack, and final schema-6 terrain-routing pack.
The base terrain pack is an inference input and must not be served. The final map and terrain artifacts must be distributed with their generated data-license and source notices.
Source preparation, verification, licensing, and canonical model details live in the World Data references:
- World-data bundles and Source manifests define release and identity rules.
- Viabundus, Elevation, Historical land use, and Forest cover cover the base geographic inputs.
- Potential vegetation, Tree species, Soil, Geology, Religion, Drought, and Hydrology cover enrichment stages.
- Strategic route terrain, Industries, and Canonical spatial grid cover derived gameplay facts and shared build identity.
Strategic UI
The issue #63 cache slice is documented in
strategic-read-cache.md, including the explicit
mutable subscription inventory, static/on-demand exclusions, route read
classification, and a deterministic measurement procedure. The procedure
reports unavailable values rather than inventing latency or subscription-byte
measurements when the disposable database fixture is not running.
The strategic UI is server-rendered by crates/strategic-web. Browser clients
receive live state through the web server rather than connecting directly to
SpacetimeDB. Current rendering and transport boundaries are documented in
Architecture.
The local strategic UI is anonymous and single-user. Its cookie selects the
active character; it does not establish a user identity. The default
127.0.0.1:8080 bind is therefore intentional. A non-loopback development bind
must set ALLOW_INSECURE_NON_LOOPBACK_BIND=true and must remain on an isolated,
trusted network.
Test the server-rendered strategic browser through https://localhost:8443
using just web-secure. Caddy terminates TLS and negotiates HTTP/2 or HTTP/3
with the browser, then proxies to strategic-web on 127.0.0.1:8080. Run
just secure-web-trust once if the browser does not yet trust Caddy's local
certificate authority. Port 8080 remains available for backend diagnostics but
does not exercise multiplexed browser transport.
The certificate-trust recipe uses the host's configured command environment.
If Caddy is not on PATH, set CADDY_BIN to the executable's full path before
invoking it, for example:
$env:CADDY_BIN = "C:\tools\caddy.exe"
just secure-web-trust
Tactical Spawner
The dispatcher subscribes to pending SpacetimeDB tactical requests and starts
adventuresim-tactical-server processes:
just spawner
Each tactical server:
- Starts on an available port
- Consumes its one-use dispatcher claim and registers its server identity
- Opens the Aeronet WebSocket endpoint and runs until completion or timeout
- Calls
end_tactical_serverwith its terminal resolution - Exits after strategic authority validates and commits the durable outcome
Testing a Single Server
For testing without the spawner:
just tactical mission_id="test-123" scene_key="hills"
For a self-contained tactical database and request, prefer
just tactical-isolated; it writes .env.tactical so a subsequent bare
just tactical and just client target the same isolated instance.
Troubleshooting
-
SpacetimeDB not running:
just status, thenjust spacetime-start -
SpacetimeDB failed to start: check
adventure-simulator-1/spacetime.logbelow the platform temporary directory (%TEMP%on Windows and usually$TMPDIRor/tmpelsewhere). -
Tactical spawner can't find binary: run
just build-tacticalfirst -
Mission stuck on "pending": spawner not running or binary not found
-
Cargo cannot create a temporary
targetdirectory: ensure the parent ofCARGO_TARGET_DIRis writable. On Windows or in a restricted sandbox, use a workspace-local directory, for example:$env:CARGO_TARGET_DIR = "$PWD\target\verification" just test
strategic-web logs every HTTP request at info level with a request ID,
method, URI, response status, and elapsed milliseconds. The same request ID is
returned in the X-Request-Id response header for correlation with the browser
network panel. Requests abandoned by a navigating browser are logged as
canceled rather than silently disappearing. Set
RUST_LOG=strategic_web=info if a shell-level log filter suppresses these
diagnostics.
Physiology key material
The strategic database initializes versioned private Physiology key material
from authoritative runtime randomness. There is no build-time or environment
fallback to configure. Pre-launch schema recreation creates a new population;
causal infection and administration rows pin the versions needed for replay.
See physiology.md for the privacy contract.
Social panel demo
Start the isolated strategic stack with the guarded visual fixtures:
just web-isolated-strategic social-demo 23100
Select Social Demo, open Greta the Guard, and press the raised Social icon beside the Morale meter. The fixture includes defeat and injury penalties, established Familiarity, positive Affinity, exact multi-valued observer beliefs, presentation, and one deliberately incorrect perceived sensitivity. Greta professes Lutheranism, and Social Demo has direct Lutheran study plus correlated Catholic knowledge, so the themed Prayer response is immediately usable. The Social rail shows Insight, Charm, Command, Deception, and target-specific Religion; Lighten Mood and Flirt are distinct Charm approaches, and repeated supported observations demonstrate the Transparency-controlled Insight/Deception training split. The bootstrap capability is compiled only for the isolated workflow; there is no standalone public fixture reducer. Schema changes are destructive in this pre-launch workflow, so rerun the isolated profile to recreate its database. Select Zealous Prayer Demo and open Margareta the Pilgrim to inspect the same Lutheran action kept visible, greyed out, and annotated with its unavailable reason.
Strategic read cache
This document records the bounded read-path change for issue #63. Browsers
still talk only to strategic-web; the generated SpacetimeDB SDK connection is
server-side and is not an authorization boundary.
Route classification
| Route/read family | Classification | Boundary |
|---|---|---|
/characters and the shared character page model | Cache-backed when the explicit subscription is ready | Public character projection only; the authoritative case-site projection remains HTTP SQL. |
/ home routing and /camp active-character lookup | Cache-backed for character and party camp state | Inventory, journey, itinerary, encounter, and owner-scoped rows remain HTTP SQL. |
| Party, settlement, quest, dialogue, investigation, evidence, and chat page models | Consolidated/on-demand boundary | Keep HTTP SQL and reducer calls until each page has an authorization-scoped read model. |
| Settlement, item, travel-edge, world-clock, world-node, and world-import reads | Static/on-demand | Not globally subscribed. |
| Reducers, authoritative fallback reads, and tactical state | On-demand/authoritative | Never served from the strategic cache. |
The remaining page surface is intentionally not described as cache-backed. Its many-row reads should be consolidated behind page-specific read models as those pages are migrated; this change does not put SQL or cache logic in templates.
Subscription policy
crates/strategic-web/src/live.rs contains the explicit
STRATEGIC_CACHE_SUBSCRIPTIONS inventory and the matching add_query chain.
The unit test in that module checks that every inventory entry appears in the
chain and that large static/world/import tables do not. Mutable character,
party, journey/itinerary, equipment/medication, inventory, and live invalidation
projections remain subscribed. The cache is incomplete until on_applied; a
connect, disconnect, or subscription error makes it unavailable again.
Authorization rule
Only typed public reads are exposed by LiveState: public character rows and a
party's camp-present boolean. Private chat, dialogue, contract, investigation,
disease, and case projections may remain in the SDK subscription solely so
their callbacks can invalidate live UI, but no cache accessor returns those
rows. The selected
character_id cookie selects a presentation context; it does not prove
ownership. Owner-scoped/private projections continue through authenticated
HTTP SQL predicates and reducers. A future cache migration must add an
explicit selected-character/party authorization boundary before exposing such
rows.
Measurement procedure and current results
SpacetimeClient::query_metrics is a monotonic, clone-safe SQL counter. Take a
snapshot immediately before a controlled page request and another immediately
after it; use after.delta(before) for request count and total elapsed time.
The elapsed value includes HTTP response parsing. Record the SSR boundary with
the request's Instant/curl --write-out timing. Run one request at a time
when attributing a page delta; the counters remain correct under concurrent
requests because they are not reset destructively.
For a representative run:
$env:RUST_LOG = "strategic_web=info"
just web-isolated-strategic cache-measure 23100
# In a controlled harness, snapshot SpacetimeClient::query_metrics(), request
# /characters, /, and /camp, then snapshot again and emit after.delta(before).
The checked-in deterministic tests cover cache state transitions and an injected-latency counter delta. Live before/after values for this checkout are unavailable: the required disposable SpacetimeDB/web fixture was not running during remediation, and no subscription payload-byte telemetry is currently emitted by the SDK. Do not treat the following as measured values:
| Page | SQL requests/page before | SQL requests/page after | Initial subscription rows/bytes | SSR latency |
|---|---|---|---|---|
/characters | unavailable | unavailable | unavailable / unavailable | unavailable |
/ home routing | unavailable | unavailable | unavailable / unavailable | unavailable |
/camp | unavailable | unavailable | unavailable / unavailable | unavailable |
The procedure above is the reproducible follow-up measurement; payload bytes require SDK/client instrumentation or server WebSocket capture before they can be reported honestly.
Strategic NPC simulation
Provision scenarios use multiple food definitions and aggregate useful calories
across independent lots. Drivers must not assume all food is travel_ration.
adventuresim-strategic-sim is a native, deterministic experiment harness for
balance exploration and regression reproduction. It has two deliberately
different backends: a fast native settlement-activity model for multi-year
population sweeps, and an opt-in reducer-backed core-loop driver for behavioral
and integration testing.
Reproducibility contract
Configuration, manifests, profiles, traces, snapshots, and reports are
versioned JSON. Unknown configuration fields are rejected and population,
duration, decisions, trace events, and snapshots are bounded. The profile
generator uses the repository-owned SplitMix64 implementation and a stable
sub-seed per agent. Generation and simulation never depend on hash-map
iteration order or DefaultHasher. Reports use ascending agent ID within each
ascending day. Changing the random algorithm, field interpretation, or
canonical ordering requires a format-version bump.
The canonical BLAKE3 digest covers the manifest and all canonical report data;
the digest field itself is blanked before hashing. replay reruns the recorded
manifest and verifies both the stored report and reproduced digest. There is no
performance or wall-clock metadata in the canonical report. Reproducibility is
guaranteed for the same simulator format. Canonical hashing quantizes floating
values to four decimal places so harmless JSON and cross-platform subprecision
differences do not change the digest; stored balance metrics retain their full
precision. A future fixed-point game-state migration can strengthen guarantees
beyond that explicit tolerance. Config and report inputs are streamed with a 64 MiB limit,
and their vector bounds are revalidated before hashing or replay.
Daily scheduling is intentional and represents repeated one-day player actions.
It gives future incident policies a single canonical decision granularity. Pure
schedule training and settlement activity calculations live in
adventuresim-core; both the SpacetimeDB module and simulator call those same
functions. The live reducer's bulk rest currently trains and then evaluates one
aggregate activity interval; rounded income and single incident interruption
can therefore differ from repeated one-day actions. Bulk-rest strategy testing
is follow-up work rather than part of this first-slice contract. Gold from labor
and thievery is an intentional economic source. Future purchases, provisions, lodging, and losses will be explicit
sinks; the runner therefore does not assert naive currency conservation.
Profiles and results
Profiles retain their seed and all inputs needed for inspection: a deterministic sparse personality (two through four non-neutral axes across thirteen behavioral axes), plus always-assigned Sex, Presentation, and Inclination; correlated, bounded attributes; an explicit personality-by-attribute build role; initial leaf skills; activity allocations that produce training; activity-versus-quest, risk, and recovery preferences; equipment style and utility weights; and provisioning, reserve, and spending preferences. Some fields are recorded for later slices and do not yet affect settlement choices. Build derivation keeps skills, training, activity, quest risk/recovery, and equipment style coherent. Content leaders are activity-only; ambition increases quest propensity. Bravery selects heavy front-line melee only when endurance and both-arm strength make it viable, while fearful agents prefer ranged/light roles when their perception supports one. Followers still defer to the current leader's quest/activity decision; individual follower policy is applied to training, recovery, treatment, and equipment, which is a known party-decision limitation.
Reports include a bounded decision trace, bounded periodic snapshots, terminal
reason, wealth, final and gained skill hours, activity and leisure time,
notoriety, and cumulative risk exposure. Reports also contain the typed Pareto
frontier that maximizes wealth and skill-hour gain while minimizing notoriety
and risk exposure; the human summary prints its stable agent IDs. Risk exposure is a metric, not a fake
combat or incident outcome. Pareto utilities require typed maximize/minimize
objectives, preserve exact ties, and reject nonfinite values. The matched
command holds a profile and seed constant while changing only its declared
labor/thievery activity preference and allocation.
Authoritative core-loop backend
StrategicBackend separates observations and player-like intents from native
state mutation. NativeSettlementBackend remains the fast deterministic
backend. The core-loop command instead connects through generated typed SDK
bindings and serializes ordinary reducer calls, waiting for both reducer
completion and the subscribed state that follows it. It creates several
independent parties through ordinary join request/accept reducers. Each leader
then independently chooses settlement activity or questing from its generated
policy until its in-game duration or cycle bound is reached. Before an outbound
case-site leg, the evaluator derives a conservative public provisioning horizon
from disclosed distance and walking schedule. Four times the ordinary daylight
elapsed projection covers fatigue-expanded outbound travel plus the return leg;
a further one-day reserve covers delays and encounters. It forecasts food and
water for the living party from public needs and inventory and buys an
affordable shortfall through the ordinary merchant reducer. A co-located living
party member may pay using the shared treasury plus their purse, while retaining
their observable medical reserve. Pricing fails closed without that payer's
public local-price effect and exactly one public default merchant present at
that payer's own public character time. If
either staple is unavailable or unaffordable, the party
remains at the settlement and performs sustainable activity instead of
knowingly departing empty. Questing then travels through persisted camp stops,
autoresolves, stores loot, returns,
turns in, liquidates party loot, withdraws the member's earned stake, purchases
from the merchant, and equips an upgrade. Followers travel and run their own
daily schedules. Defeat causes a retreat, bounded settlement convalescence, and
a bounded retry; an incapacitated party is never autoresolved repeatedly in
place.
Core-loop reports are explicitly tagged spacetimedb_authoritative_core_loop
and retain the server origin, disposable database, claimed run nonce, generated
profiles, semantic action trace, final equipment and capabilities, and metrics
for quest results, activity days, camps, loot value, proceeds, earned stake
withdrawals, purchases, upgrades, unexpected reducer failures/retries, stuck
detection, and duplicate semantic events. Final agent rows distinguish the
legacy character gold field, personal gold-coin stacks, party treasury, and
party stake. They also expose the public settlement or exact case-site
occupancy, active public journey destination, and symptomatic/critical flags,
so ready does not conceal illness and a remote party is not mistaken for a
stranded journey. Generated preferences drive quest/activity choice, quest risk, and
weighted equipment utility (protection, mobility, price, and reach) for
unarmored, light, heavy, and ranged styles. An upgrade counts only after the
authoritative equipment row shows the purchased inventory item.
Each autonomous party choice records an observer-safe quest_decision before
the selected action. It includes the deterministic policy selector and quest
propensity, current settlement, count of player-visible offered contracts,
the leader's count of open generated cases and projected investigation actions,
and the selected direct-contract, generated-discovery, generated-case, or
activity path. An owned open generated case takes precedence over the random
activity selector. Repeated daily decisions
are intentionally exempt from semantic-duplicate alarms. Because this evaluator
does not open tactical crime incidents, authored Thievery and Raiding schedule
minutes are explicitly reassigned to legal Labor in the effective schedule
instead of disappearing into leisure; the authored preference remains intact.
When a non-earning schedule such as Prayer meets low-food reserve pressure, its
preferred allocation temporarily becomes legal Labor with
subsistence_reserve_to_labor; the authored preference is restored when the
pressure clears. The chosen effective schedule is installed and verified
before the ordinary rest reducer runs. Temple viability requires one visible
day of food only because settlement water is authoritative and free. Otherwise
the policy selects a full-board Inn only when the purse covers its public cost
plus observable medical commitments and as much of the profile's visible cash
reserve target as is currently attainable. A Temple remains the player-visible
last resort while initial Labor builds that reserve. Activity events retain
the preferred and effective activity, installed schedule, venue, committed
reserve, fallback reason, and public pre/post purse, strategic condition,
hunger, thirst, visible food and water, character minutes, and signed deltas,
with outcome=completed. A rejected settlement-rest attempt
instead emits one outcome=failed activity event before the run stops,
containing its public pre-action state, effective plan and venue, stable stage,
and safe error category without raw reducer text. Camp handling subscribes only
to the public party_journey and party_journey_itinerary projections. At
each stop it applies the same remaining-interval overlap as the web UI to
completed_elapsed_minutes..total_elapsed_minutes, rests exactly to the end of
the active forecast camp through rest_at_camp, logs bounded pre/post public
camp state, and re-reads the journey, itinerary, party, health, and leader
before continue_camp_travel. A missing, overlapping, or non-advancing
projection fails closed instead of guessing a fixed rest duration. Camp
coherence failures distinguish zero active forecast intervals from overlapping
intervals and include bounded public elapsed totals and forecast counts.
The travel driver returns an explicit Completed,
HeldNoActionableActor, or HeldForRecovery outcome. A hold is nonfatal:
the current quest, return, or turn-in step stops without claiming arrival,
marking a case site traveled, or substituting an incapacitated leader.
Before ordinary quest selection on each in-budget cycle, the runner continues
any active public journey. After a completed leg it revalidates the current
leader, owner, party health, and exact public settlement or case-site
occupancy. An open generated owner case then resumes at its arrived site.
A direct contract resumes from the party's public active_contract_id, public
party ownership, and Accepted or ReadyToReport contract status rather than
searching only offered contracts. Resumption does not increment contract
attempt metrics again. Reporting is attempted only after public state proves
arrival at the contract's origin settlement; the ready-to-report status gate
prevents duplicate completion metrics.
Off-settlement health is an explicit expedition state, not a reason to repeat
quest suppression indefinitely. Before selecting another quest action, the
runner reads only public party membership/location, strategic condition,
illness signal, needs, concrete supplies, journey itinerary, and owner-visible
case-site pins. It immediately records a recovery plan and makes at most two
one-day field-rest attempts when pooled concrete stored food and portable water
cover every living member's daily requirement and nobody is critical. Injury
or disease boundaries may clip an attempt, so actual elapsed minutes are
measured rather than assuming a completed day. The expedition resumes
only when there is a living actionable member and every living member is ready
and asymptomatic. A successful field recovery resumes the same bounded policy
cycle, allowing an already-public on-site action to proceed instead of losing
the final cycle to recovery bookkeeping. When recovery began inside the
configured duration but its bounded rests cross the duration threshold, only
that same cycle's one public quest/on-site action is allowed; the next cycle
observes the ordinary duration cutoff. A cycle with no recovery does not cross
the cutoff, and the final bounded rescue pass never selects a quest action.
Evacuation or a fail-closed hold consumes the cycle. Otherwise a ready companion directs an ordinary journey back
to the one public origin settlement. If the leader is unready, reducers
narrowly permit that ready party member to direct off-settlement camp rest,
return-route continuation, and protective (never attack/objective) encounter
choices; this does not transfer leadership or grant contract, combat-objective,
or ordinary quest authority. Evacuation counts as complete only when public
state shows a living party at that settlement with no remaining camp
destination; an incomplete leg is logged as stalled.
The recovery loop reselects a public ready, asymptomatic, noncritical actor
before every individual field rest. If the previous rest leaves nobody
actionable, it does not reuse the stale actor. One narrow passive-recovery mode
may still apply when no living member is publicly actionable. Public symptoms
may be the reason even when a member's condition status is ready; every
living member must have a known ready, staggered, or incapacitated status and
none may be critical. The authoritative leader must be alive, the party must
be off-settlement at a coherent persisted journey camp, and concrete pooled
supplies must cover the requested day. The simulator represents this with a
separate typed PassiveNoActionable rest actor which can reach only the camp
rest call boundary. It cannot continue travel, resolve an encounter, perform a
case action, accept or report a contract, vote on leadership, or invoke any
other reducer. This is passive convalescence—the state is resting—not action
authority.
Passive eligibility and the public member/supply state are recalculated before each of at most two rest attempts. If any member becomes actionable, the ordinary ready-actor recovery path takes over; if everyone becomes ready and asymptomatic, the expedition may resume. A critical member, insufficient supplies, a dead leader, an unresolved public encounter, or a missing, ambiguous, mismatched, completed, or forecast-incoherent public camp journey fails closed to a typed hold before invoking the reducer. Both ordinary and passive recovery rests use this same public camp predicate: party destination, unique matching journey and itinerary, incomplete elapsed journey, and a valid active forecast camp interval.
expedition_passive_rest_attempts counts reducer attempts, while
expedition_passive_rest_minutes sums the actual public maximum-member clock
delta. The event records requested and actual minutes, so a disease or injury
boundary that clips a request never masquerades as a completed day. The
ordinary public per-member and concrete-supply before/after diagnostics remain
attached to phase=passive_no_actionable_rest. The policy never reads private
disease or exposure state.
Every recovery camp and evacuation leg records each member's public before and
after condition, hunger, thirst, food/water days, symptom/critical flags, and
elapsed time. It also records changes in concrete stored food and portable
water. Private disease episodes and exposure are not subscribed; diagnostics
state exposure=not_publicly_projected rather than inferring it. Stable reasons
distinguish health suppression, bounded field recovery, safe resumption, and
settlement evacuation. A journey leg may claim that every member is ready only
after its public post-leg member projection proves that condition; otherwise
it explicitly requests off-settlement recovery on the next cycle.
When no living member is actionable, a
journey_held_no_actionable_actor diagnostic records only bounded public
evidence: elapsed, total, and remaining journey time; destination; remaining
camp movement; the active public forecast interval when present; living-member
count; one-day food and water requirements; concrete stored food and portable
water; and whether those supplies cover one rest day. The report counts these
and health-driven journey holds in expedition_holds. It does not expose or
infer private exposure or disease authority.
A held party is tracked separately from a party that performed an action.
A hold with no public character-time progress does not make the cycle active
and cannot by itself advance authoritative world time. If another party acts,
that independent activity may still advance the shared world; a recovery rest
that advanced public party time also counts as real progress even when a later
step holds.
Successful final-agent rows carry the same public needs, visible food and water,
remote location, journey destination, illness flags, settlement services,
herbalist quote, and inn full-board cost used by failure diagnostics. Failure
artifacts use schema version 5. In addition to the strict
event vocabulary and activity-detail semantics, they retain only an allowlisted
operation name and stable reason code for expected investigation and camp
failures. travel_camps, rest_at_camp, and continue_camp_travel are
allowlisted operations with stable held-journey, daylight-window, and
journey-projection reason codes; raw reducer text is never copied into the
artifact. These fields make poverty,
starvation, unavailable-rest, and stale temporal-action deadlocks diagnosable
without exposing hidden case truth.
In full-world runs, leaders without an offered direct contract discover local generated problems through the same ordinary player dialogue used by the web client. The runner uses only public settlement-NPC facts and current public presence. Discovery is discriminated by the public settlement location and time, not by the source NPC. Each decision performs exactly one ordinary dialogue with the stably first public representative at the inn when one is present. The settlement overview is used only when no inn representative is present, matching the same public discovery rule enforced by the server. Presence alone is insufficient: an inn row must join to a persistent NPC with a valid player/NPC dialogue before it suppresses overview fallback. Orphan presence and unknown or non-dialogue conversations fail closed and are omitted from the gateway's public contact projection. An unproductive action falls back to an ordinary settlement activity day before another discovery decision.
After no_public_rumor_available, the runner waits two official days before
repeating the same discovery dialogue. A change to the public settlement,
visible contact/location set, or active cause-free LocalProblemSymptom set
invalidates that backoff immediately. The runner does not subscribe to private
problem authority, rumor receipts, causes, or private threat disclosures.
The runner records a bounded public attempt and result for that action:
official minute, coarse active-symptom count and oldest-age bucket, bounded
visible-candidate count, selected location class, bounded owner open-case
count, and whether public backoff suppressed the attempt. It does not put case
IDs, causes, receipt data, or private threat disclosures into these discovery
diagnostics. A new owner-visible open case is the postcondition for an
observer-safe rumor_delivered=true. Stable result reasons distinguish
rumor_delivered, no_public_rumor_available, no_visible_contacts, and
unchanged-public-state suppression. Reports separately count actual attempts,
fruitful attempts, unproductive decisions, and backoff suppressions; a
suppressed retry is not counted as an attempted action. The pre-action
quest-decision event distinguishes policy intent from selection with
quest_intended and quest_selected; the later discovery-result event alone
reports the postcondition and whether the ordinary activity fallback follows.
The first observation of each owner-scoped open generated case is a separate
bounded generated_case_intake. Identity is the composite
(owner_character_id, case_id), because the same public case ID may be
continued independently by more than one owner. Dialogue-created intakes use
the public dialogue_rumor source. A case that first appears in the ordinary
owner projection uses owner_projection_continuation; the simulator does not
infer hidden provenance. Every unique intake counts exactly one generic quest
attempt, while dialogue discovery and projection continuation retain separate
metrics. Terminal, exact-site, travel, and seen state use the same composite
identity, so one owner's terminal transition cannot suppress another owner's
case clone.
Generated local-problem authority uses the official world clock for its active
window. Dialogue discovery therefore checks problem starts_at, ends_at, and
resolution against official world time, while journal learned_at and
recorded_at remain on the observing character's elapsed timeline. Travel,
treatment, and bulk settlement activity can move that observer timeline
independently and must not make a world problem appear prematurely active or
expired.
The runner then follows only owner-scoped
case, journal, lead, dialogue-topic, action, action-outcome, and exact-site-pin
projections. Referred witnesses are never inferred from generated truth:
same-named public candidates are tried in stable order and only the candidate
whose projected session exposes referred-testimony for the selected public
case is selected. Investigation actions and their outcomes are likewise
filtered by exact owner and public case before reducers receive the projected
action ID, method, and version. Exact site travel selects only the pin matching
the party's current case-site occupancy. Combat requires an owner-scoped,
public-case-scoped binding matching party, battle, mission, and site. The state
machine rechecks the current leader and every member's public strategic
condition after each time-advancing action, travel leg, and combat. A completed
case returns any surviving party from its occupied site before the case leaves
the active loop. When a projected action advertises night_window with a
bounded wait_minutes, the runner uses the ordinary settlement-rest action
when an affordable service is available, otherwise ordinary field rest, then
re-reads the projection, expected version, leader, and party health before
acting. It never parses action prose or reducer errors to decide to wait. The
runner records bounded pre-call evidence for each generated investigation
attempt: immutable public case subject, public case/action IDs, projected
method and summary, expected version, availability reason and wait, and the
actor/party public clocks. A narrowly allowlisted victim-cohort
moved/changed/unavailable reducer result triggers one observer-safe projection
refresh and then defers to the next cycle without aborting, even if the local
subscription cache has not applied a new row yet. The authoritative gateway
projection independently checks the current private cohort binding and exposes
only generic target_changed unavailability; it never reveals which target
predicate changed. On the next cycle the runner can choose another available
public action instead of deterministically retrying the stale one. Reports
count replans, and neither events nor failure artifacts copy the raw authority
error.
The state machine is bounded per cycle and falls back to sustainable settlement activity when no legal projected step is available.
Reports separately count direct-contract attempts/completions, generated case intakes and owner-projection continuations, generated cases discovered, completed by the simulated party's immediate dialogue/action/autoresolve transition, and closed externally by background resident NPCs. They also count projected investigation actions, temporal waits and wait minutes, observer-safe replans, and witness dialogues. Generated-case trace events contain only public case IDs/subjects, NPC names and locations already visible in the selected dialogue, projected action summaries, and public outcome wording; they never read generation manifests, canonical causes/sites, reliability, hostile authority, custody authority, or outcome authority. The same report exposes unique owner-party discoveries, exact-site-ready cases, finance-blocked cycles, case-site journeys, provision purchases, and actual public gold spent. Identical affordability signatures enter backoff until the required budget or observable funds change. That cache is scoped by party, acting owner, and public case/contract finance key, so two owners cannot inherit one another's backoff. Direct contracts preflight against the greatest public distance among their case destinations before acceptance, then select the disclosed owner-scoped pin by minimum-distance, stable-site ordering and re-run observer-safe provisioning for that exact pin before travel. Thus a temporary shortfall does not withdraw the offer or fund travel to a different destination. An explicit post-defeat cannot-reprovision abandonment is reported as abandonment rather than deferral.
Live simulated NPCs inspect persistent equipment condition before choosing quests or settlement activity. They submit repairable damaged equipment to the appropriate local smith, wait through the ordinary rest reducer until the longest ETA, and retrieve every completed order before continuing. Their replacement utility is discounted by current condition, so maintenance competes coherently with buying a replacement. Reports include submissions, retrievals, repair wait time, worst final condition, and outstanding orders; deterministic simulation setup seeds damage through a reducer guarded to registered simulation characters.
Medical needs are evaluated before repairs, and repairs before equipment upgrades. The disposable
fixture seeds one deterministic influenza episode behind the same claimed-run capability boundary as
other simulator-only setup. Policy observes only public condition and the narrow public
symptomatic/critical signal, buys a fixed concrete preparation, and invokes the generic administration
reducer without reading infection identity, crafting, diagnosing, or selecting an effect by disease.
The policy reproduces the player-visible herbalist quote from the public item definition, visible
storefront stock, and the gateway-projected local-problem trade modifier. Affordability includes the
visible cost of the required one-day rest venue, preferring a free temple to a paid inn. An affordable symptomatic character buys a course;
an unaffordable character, a settlement without an herbalist, or a nonsymptomatic convalescent
instead takes bounded one-day natural recovery. Equipment maintenance retains one locally quoted
course as an emergency reserve rather than consuming every coin before a later symptom becomes
visible. It rests in bounded one-day steps until ready. Before each choice, the
trace records public condition, symptomatic status, settlement, purse, quote,
affordability, action, and reason. Recovery completion records
recovery_context=public_symptoms and keeps the pre-rest symptomatic
observation separate from the newly read post-rest observation; it does not
claim a private physiological cause.
For a nonsymptomatic patient who cannot afford the inn and lacks a supplied
temple rest, the policy deterministically selects the solvent co-located living
party member with the greatest public purse after retaining that payer's own
visible medical reserve (lowest character ID breaks a tie). The patient still
pays normally whenever able. Sponsorship invokes a narrow ordinary reducer:
the patient contributes their available purse and the authenticated payer pays
only the remaining portion of the inn's exact authoritative one-day quote
directly for the named patient. It rejects stale quotes, self-sponsorship,
affordable patients, missing party membership, different settlements, missing
Inn service, insufficient payer funds, and patients without a public recovery
need; it never transfers arbitrary coin. Party treasury and payer stake are
diagnostic context, not an extra source of spendable personal funds. If neither
self-payment nor sponsorship is available, an available Temple remains a
free, time-advancing last resort even without a full day of visible food, with
ordinary hunger consequences instead of a zero-time suppression loop.
Sponsored-rest metrics and bounded events record payer, patient, public quote
and split, the payer's reserve and spendable funds, public treasury/stake, exact spend,
pre/post purses, and pre/post public condition. They do not read private disease
or exposure state. Medication itself remains patient-funded; sponsorship is
deliberately limited to lodging until treatment purchase and custody can be
extended without broad transfer authority.
The sponsored-rest requested-minute and elapsed-minute metrics are separate:
elapsed time is the public patient-clock delta observed after the reducer, so a
terminal zero-minute or partial interval is not reported as a full day.
sponsored_settlement_rests counts successful reducer callbacks, including a
zero-time terminal clip; sponsored_settlement_rest_elapsed_minutes also
contributes to the broader treatment_rest_minutes, and sponsored payment
contributes to treatment_gold_spent, so those aggregates intentionally
overlap rather than representing disjoint categories. Medical-decision events
derive rest_venue from the selected action: natural, sponsored, and emergency
recovery use the natural venue, while buy-and-rest uses the medicated venue.
While recovery is active it authoritatively replaces the saved
personality schedule with pure rest, then restores that profile schedule after recovery so labor or
thievery cannot interrupt convalescence with an incident. Quests remain suppressed while a member is unsafe. Reports audit
diagnosis attempts/results, crafting or purchases, medication equips, treatment gold and time,
recoveries, suppression, and terminal deaths.
Because preparation and treatment can advance time, both generated-case and
direct-contract drivers re-read the public current leader, owner relationship,
party membership, life, and readiness after each such batch. They defer before
choosing a projection or invoking the next quest reducer if ownership changed
or any member remains unsafe.
Safety is intentionally strict. URLs are parsed structurally and must be an
exact credential-free HTTP loopback origin with no path, query, or fragment.
The command accepts only an adventuresim-sim-* database and refuses any
pre-existing run, character, or party state. Fixture mode also refuses any
settlement or import state. Full-world mode permits settlements only when a
completed world_data_import proves they came from the pinned compiled world,
and records its artifact ID and manifest digest in the report. It atomically
claims the database with an owner identity and nonce before creating simulation
characters. Bootstrap
configuration requires that claim and permanently marks each simulated
character by run and agent ID; simulated and ordinary characters cannot merge
parties. In addition, ordinary module builds compile with simulation claims
disabled. The recipe creates 32 random bytes in memory, exposes them to exactly
one module build and runner process through ADVENTURESIM_SIM_BOOTSTRAP_TOKEN,
and never accepts the capability as a CLI argument or writes it to a report.
The public claim reducer checks that build-time capability before inspecting
database freshness. The recipe creates a nonce-named database, exposes no
host/database override, and deletes it on exit. Population, duration, cycles,
action waits, camp continuation, defeat retries, and recovery loops are bounded.
There are two distinct random streams. The CLI seed deterministically controls profiles and policy choices. Combat is authoritative: the autoresolver obtains its seed from server RNG, and reports record that actual seed together with rounds, summary, and log. Consequently the native backend supports exact replay, while a core-loop rerun reproduces policy inputs but not necessarily combat outcomes. Its trace is the debugging artifact. Reports identify the server, database, and claimed run; the current SDK does not expose a deployed module binary digest.
Current limitations are:
- no native Raiding execution until an authoritative equipped-capability observation exists (generated schedules exclude it and custom schedules are rejected);
- no parity claim for live bulk multi-day rest, whose aggregate rounding and incident interruption semantics differ from repeated one-day actions;
- the bounded bootstrap applies generated attributes, initial skills, and downtime schedules, while equipment starts from the normal character creator before policy-driven upgrades;
- party loot is liquidated through the shared party treasury; upgrades must be funded by withdrawing the character's earned stake before a personal trade;
- duplicate detection covers the simulator's semantic action stream, not the strategic-web rendered DOM;
- no tactical ticks and no persistent production NPC rows.
Commands
cargo run -p adventuresim-strategic-sim -- run --seed 42 --population 100 --days 1095 --output report.json
cargo run -p adventuresim-strategic-sim -- replay --report report.json
cargo run -p adventuresim-strategic-sim -- matched --seed 42 --days 365
# Safe disposable integration run (requires local SpacetimeDB 2.6.1):
just strategic-sim-core-loop target/sim-runs/fixture-001 42 8 20 30 2
just strategic-sim-core-loop-world target/sim-runs/world-001 42 8 20 30 2
Quest evaluators
There are two gameplay evaluator surfaces and one offline content-analysis surface, with deliberately different boundaries.
Offline generated-content analyzer
quest-analyze projects deterministic generated investigations into an
observer-safe, in-memory PlayerFrame. It is useful for generator regression,
route diversity, policy fingerprints, dead ends, loops, correction persistence,
and separate public/developer audit artifacts. It does not call
SpacetimeDB reducers, exercise the browser, perform tactical combat, or prove
production gameplay behavior.
The default recipe is credential-free and the mock policy round-trips through the same strict JSON response parser used by an OpenAI-compatible provider:
just quest-analyze-mock 41 4
This creates quest-analysis-public.json, quest-analysis-developer.json, and
quest-analysis-stories.md as distinct artifacts and refuses to overwrite
them unless direct invocation passes --overwrite. The public report contains
only player-visible traces, bounded run provenance, classifications, and
aggregate behavior. Seeds, catalog revisions, canonical truth, structured
factor traces, bridge IDs, generator marginals, and truth-joined
classification/counterfactual audits remain in the developer report. Their
digest join is one-way.
All three artifacts are rendered in memory first. Per-artifact and combined byte budgets are checked against the exact pretty JSON and Markdown bytes before any output file is created. Existing-ancestor canonicalization also prevents distinct-looking paths through symlinked or junction parents from collapsing public and developer output onto the same file.
Each trace records pre/post observation digests, opaque legal choices, exact
dialogue, public discoveries and corrections, preparation, costs, exhaustion,
and termination. Classifications are bounded answers, never chain-of-thought.
Prepared/unprepared solve rates are descriptive quality slices, not causal
skill or equipment-benefit estimates.
Counterfactual comparisons are made only when different hidden cases naturally
share an identical player-visible initial prefix; absence of such a group is
reported as not_measured.
Contract completion, language benefit, tactical combat benefit, causal skill
benefit, and accidental perception discovery are also explicitly
not_measured because this projection does not implement those mechanics.
No proxy number should be treated as evidence for them. The privacy audit is a
structural type boundary plus canary scan, not a formal proof.
An observed non-solving run can be promoted into a reviewable deterministic fixture candidate:
just quest-analyze-promote 41 recurring-depredation
cargo run -p adventuresim-strategic-sim -- quest-analyze-replay `
--fixture quest-analysis-replay-candidate.json
# Replay the reviewed checked-in regression:
just quest-analyze-replay-fixture
The versioned fixture joins catalog revision and generator manifest, records opaque decisions, and checks stable outcome fields. Promotion never silently commits or approves a fixture. Only step-limit and dead-end failures can be promoted; provider failures, loop detection, and exhausted runtime budgets cannot be reproduced from an opaque action list alone.
Persistent hostile escalation
The reducer-backed core loop advances the same scheduled recurring incidents as
normal settlement activity. NPC adventuring companies remain persistent
recruiting entities, but there is no NPC quest-intervention policy, candidate
view, strategy reducer, outcome table, or story anthology. Unresolved hostile
RecurringDepredation cases continue until players resolve them; deterministic
incident ordinals drive bounded combat escalation and public notoriety.
Because the production world clock is tied to elapsed wall time, a claimed disposable simulation has one additional bounded reducer that advances that same authoritative clock by a requested number of game minutes. The core loop uses it once per active simulated day, then invokes ordinary settlement activity so follow-up incidents, escalating penalties, notoriety, and recruitment all occur through the production systems. The capability is absent from normal module builds. Simulation characters receive a small starting purse so an inn-only seed settlement cannot deadlock before its first labor day; all accommodation and food costs still use ordinary currency rules.
Use the normal isolated recipe:
just strategic-sim-core-loop-world target/sim-runs/world-001 42 8 20 30 2
Direct expert invocation needs no NPC policy options:
cargo run -p adventuresim-strategic-sim -- core-loop `
--host http://127.0.0.1:3000 --database adventuresim-sim-UNIQUE `
--run-nonce UNIQUE-NONCE `
--imported-world --expected-world-manifest-digest PINNED-DIGEST `
--output report.json
End-to-end browser quest evaluator
The browser evaluator is deliberately separate from the strategic NPC evaluator. It always uses an LLM and interacts with the running local game only through visible web controls. The model receives the current screenshot, visible page text, and opaque handles for visible enabled controls. It cannot name a reducer, use a quest authority ID, invent an action, or navigate directly to a guessed route.
Each run writes an immutable screenshot log: index.html, manifest.json, and
one viewport PNG for the initial state and every subsequent action. The log
therefore shows exactly what was on screen when the model made each decision.
Use a new output directory for every run:
just quest-web-eval quest-browser-run-001 `
http://127.0.0.1:24301 /characters OPENAI_API_KEY gpt-4.1-mini
Network use must be explicit, the game URL must be loopback, and provider endpoints must use HTTPS unless they are loopback test fixtures. The command fails closed when the named API-key variable is absent. CI exercises the strict decision protocol and a loopback model fixture; it does not make paid model requests.
Direct core-loop invocation is intentionally an expert-only path: its process
must inherit the same ADVENTURESIM_SIM_BOOTSTRAP_TOKEN used to compile and
publish that disposable module. There is no token CLI option. Prefer the recipe,
which keeps the capability confined to one shell process and always cleans up.
The full-world recipe is the authoritative core-loop workflow. It publishes a
nonce database with the simulation capability, loads exactly
target/world-1544.json, verifies the completed import through the simulator's
typed subscription, checks the file's size and SHA-256 against
world-runtime-release.lock.json, requires the observed import manifest to
match that verified file, then runs without calling seed_simulation_world. It
chooses the lexicographically first imported settlement ID so the loaded-world
start is deterministic. The explicit output directory must not exist. A
successful run contains report.json and launcher.json; failed launches
retain launcher.json with the failed stage.
The launcher attempts to delete the disposable database on every exit path and
reports cleanup_failed with a nonzero exit if deletion is not confirmed.
The reducer-backed core loop subscribes to strategic encounters and resolves
each through the same public reducer used by the Map/camp UI. Its report records
encounter frequency; sneak, detour, attack, run, and surrender choices; escape
eligibility; exact surrendered item/value losses; encounter defeats; and full
party wipes. Encounter events in the trace retain the canonical encounter ID,
chosen action, and authoritative outcome for replay diagnostics.
After any encounter resolves with a living party, the runner re-reads public
encounter, journey, itinerary, health, and destination projections. A normal
journey with an unsafe member holds for recovery, while an evacuation may
continue under its existing ready-companion policy. Exactly one active forecast
camp proceeds through ordinary camp handling; zero active intervals calls
continue_camp_travel once, and overlapping intervals fail closed. The
encounter reducer owns its delay once; continuation neither repurchases
provisions nor increments camp-stop metrics, and the next public state may be
another encounter, a journey hold, a camp, or the destination.
LLM-oriented project documentation
This directory contains compact, repository-specific orientation material for
coding agents and other tools. It is part of the unified wiki/ documentation
tree and complements the root README.md.
project-map.mdis a generated inventory of source, configuration, documentation, and asset files.
Keeping the map current
After adding, deleting, renaming, or substantially repurposing a tracked file, regenerate the map from the repository root:
python scripts/update_project_map.py
Use the check mode in reviews or before completing a task:
python scripts/update_project_map.py --check
The generator deliberately excludes Git internals, Cargo build output, third-party dependency directories, and generated browser artifacts.
Project map
Generated by python scripts/update_project_map.py; do not edit by hand.
It inventories repository files that are relevant to implementation and orientation.
Build output, Git internals, dependency directories, and generated browser artifacts are excluded.
How to use this map
Start with AGENTS.md, then read the root README and the relevant architecture,
development, or other wiki document before changing a subsystem.
Files (1334)
.cargo/config.toml— Tooling or build configuration..codex/hooks.json— Repository support file..codex/hooks/prevent_early_stop.js— Repository support file..codex/skills/orchestrate/SKILL.md— Project documentation..envrc— Repository support file..gitattributes— Repository support file..github/workflows/gh-pages.yml— Repository support file..gitignore— Repository support file.AGENTS.md— Project documentation.CNAME— Repository support file.Caddyfile.dev— Repository support file.Cargo.lock— Locked Rust dependency versions.Cargo.toml— Cargo package/workspace manifest.LICENSE— Repository support file.MAP_DATA_LICENSE.md— Project documentation.README.md— Component overview and usage notes.THIRD_PARTY_NOTICES.md— Project documentation.assets/TownA.glb— Binary game or UI asset.assets/TownB.glb— Binary game or UI asset.assets/world-data/ieg-religion-1544.csv— Repository support file.book.toml— Tooling or build configuration.content/dialogue/examples.yaml— Repository support file.content/dialogue/organizations.yaml— Repository support file.content/dialogue/services.yaml— Repository support file.content/items/catalog.yaml— Repository support file.content/organizations/catalog.yaml— Repository support file.content/quests/bestiary.yaml— Repository support file.content/quests/generation.yaml— Repository support file.content/quests/investigation.yaml— Repository support file.content/settlement-policies.yaml— Repository support file.crates/adventuresim-character-creator/Cargo.lock— Locked Rust dependency versions.crates/adventuresim-character-creator/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-character-creator/README.md— Component overview and usage notes.crates/adventuresim-character-creator/src/lib.rs— Rust source module for this component.crates/adventuresim-character-creator/src/main.rs— Rust source module for this component.crates/adventuresim-core/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-core/build.rs— Rust source module.crates/adventuresim-core/src/activity.rs— Rust source module for this component.crates/adventuresim-core/src/alcohol.rs— Rust source module for this component.crates/adventuresim-core/src/attribute.rs— Rust source module for this component.crates/adventuresim-core/src/autoresolve.rs— Rust source module for this component.crates/adventuresim-core/src/battle_rewards.rs— Rust source module for this component.crates/adventuresim-core/src/bestiary.rs— Rust source module for this component.crates/adventuresim-core/src/bin/content-check.rs— Rust source module.crates/adventuresim-core/src/bin/questgen-check.rs— Rust source module.crates/adventuresim-core/src/body.rs— Rust source module for this component.crates/adventuresim-core/src/capability.rs— Rust source module for this component.crates/adventuresim-core/src/case.rs— Rust source module for this component.crates/adventuresim-core/src/combat.rs— Rust source module for this component.crates/adventuresim-core/src/composite.rs— Rust source module for this component.crates/adventuresim-core/src/developer_quest.rs— Rust source module for this component.crates/adventuresim-core/src/disease.rs— Rust source module for this component.crates/adventuresim-core/src/durability.rs— Rust source module for this component.crates/adventuresim-core/src/encounter.rs— Rust source module for this component.crates/adventuresim-core/src/equipment.rs— Rust source module for this component.crates/adventuresim-core/src/essential.rs— Rust source module for this component.crates/adventuresim-core/src/filth.rs— Rust source module for this component.crates/adventuresim-core/src/food.rs— Rust source module for this component.crates/adventuresim-core/src/foraging.rs— Rust source module for this component.crates/adventuresim-core/src/inventory_measurement.rs— Rust source module for this component.crates/adventuresim-core/src/investigation.rs— Rust source module for this component.crates/adventuresim-core/src/investigation_action.rs— Rust source module for this component.crates/adventuresim-core/src/item_catalog.rs— Rust source module for this component.crates/adventuresim-core/src/item_catalog_schema.rs— Rust source module for this component.crates/adventuresim-core/src/item_catalog_validation.rs— Rust source module for this component.crates/adventuresim-core/src/item_references.rs— Rust source module for this component.crates/adventuresim-core/src/leadership.rs— Rust source module for this component.crates/adventuresim-core/src/lib.rs— Rust source module for this component.crates/adventuresim-core/src/local_problem.rs— Rust source module for this component.crates/adventuresim-core/src/mission.rs— Rust source module for this component.crates/adventuresim-core/src/morale.rs— Rust source module for this component.crates/adventuresim-core/src/organization.rs— Rust source module for this component.crates/adventuresim-core/src/organization_catalog_validation.rs— Rust source module for this component.crates/adventuresim-core/src/physiology.rs— Rust source module for this component.crates/adventuresim-core/src/provisioning.rs— Rust source module for this component.crates/adventuresim-core/src/quest_catalog.rs— Rust source module for this component.crates/adventuresim-core/src/quest_catalog_validation.rs— Rust source module for this component.crates/adventuresim-core/src/quest_generation.rs— Rust source module for this component.crates/adventuresim-core/src/settlement_economy.rs— Rust source module for this component.crates/adventuresim-core/src/settlement_population.rs— Rust source module for this component.crates/adventuresim-core/src/simulation_security.rs— Rust source module for this component.crates/adventuresim-core/src/skill.rs— Rust source module for this component.crates/adventuresim-core/src/social.rs— Rust source module for this component.crates/adventuresim-core/src/starting_character.rs— Rust source module for this component.crates/adventuresim-core/src/strategic_currency.rs— Rust source module for this component.crates/adventuresim-core/src/strategic_economy.rs— Rust source module for this component.crates/adventuresim-core/src/strategic_schedule.rs— Rust source module for this component.crates/adventuresim-core/src/strategic_time.rs— Rust source module for this component.crates/adventuresim-core/src/stub.rs— Rust source module for this component.crates/adventuresim-core/src/surgery.rs— Rust source module for this component.crates/adventuresim-core/src/threat_escalation.rs— Rust source module for this component.crates/adventuresim-core/src/threat_escalation_limits.rs— Rust source module for this component.crates/adventuresim-core/src/weather.rs— Rust source module for this component.crates/adventuresim-dialogue/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-dialogue/build.rs— Rust source module.crates/adventuresim-dialogue/src/authoring_schema.rs— Rust source module for this component.crates/adventuresim-dialogue/src/bin/dialogue-check.rs— Rust source module.crates/adventuresim-dialogue/src/lib.rs— Rust source module for this component.crates/adventuresim-stdb-client/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-stdb-client/README.md— Component overview and usage notes.crates/adventuresim-stdb-client/src/abandon_contract_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/accept_contract_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/accept_party_join_request_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/administer_preparation_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/advance_simulation_world_time_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/agricultural_commodity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/agricultural_limitation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/agriculture_industry_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/alcohol_consumption_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/alcohol_consumption_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/answer_dialogue_prompt_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/approach_dialogue_witness_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/approve_party_action_request_planned_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/approve_party_action_request_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/authorize_tactical_server_claim_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/automatic_social_chat_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/autoresolve_mission_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/autoresolve_report_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/autoresolve_report_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/available_water_capacity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_automatic_social_chats_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_bestiary_deduction_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_bestiary_deductions_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_case_battle_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_case_battles_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_case_site_pin_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_case_site_pins_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_character_affinities_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_character_case_site_location_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_character_case_site_locations_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_character_familiarities_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_character_personalities_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_committed_cuts_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_contract_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_contracts_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_dialogue_event_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_dialogue_events_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_dialogue_participant_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_dialogue_participants_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_dialogue_prompt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_dialogue_prompts_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_dialogue_session_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_dialogue_sessions_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_dialogue_topic_option_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_dialogue_topic_options_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_dialogue_witness_claim_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_dialogue_witness_claims_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_forage_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_forage_receipts_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_investigation_action_outcome_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_investigation_action_outcomes_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_investigation_action_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_investigation_actions_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_investigation_case_summary_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_investigation_cases_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_investigation_journal_entry_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_investigation_journal_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_investigation_lead_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_investigation_leads_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_local_chat_message_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_local_chat_messages_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_local_problem_rumor_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_local_problem_rumors_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_local_problem_trade_effect_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_local_problem_trade_effects_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_physical_evidence_inspection_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_physical_evidence_inspections_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_physical_evidence_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_physical_evidence_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_physiology_administration_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_physiology_administrations_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_physiology_chart_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_physiology_charts_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_physiology_differential_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_settlement_npc_relationship_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_settlement_npc_relationships_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_settlement_npc_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_settlement_npcs_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_social_addresses_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_social_beliefs_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backend_social_chat_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/backend_social_chat_receipts_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/backfill_character_deaths_and_leadership_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/backfill_equipment_condition_and_smiths_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/backfill_solo_parties_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/battle_loot_item_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/battle_loot_item_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/battle_participant_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/battle_participant_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/battle_result_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/battle_result_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/begin_world_data_import_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/belief_axis_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/bestiary_hours_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/blood_exposure_checkpoint_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/bootstrap_development_world_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/built_settlement_cover_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/camp_duration_mode_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/canal_watercourse_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/cancel_mission_request_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/canopy_density_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/case_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/case_custody_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/case_finale_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/case_finale_execution_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/case_outcome_fact_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/case_outcome_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/case_resolution_status_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/case_site_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/case_site_id_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/catholic_lutheran_church_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/catholic_reformed_church_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/cation_exchange_capacity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/change_inventory_item_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/character_affinity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_attributes_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_attributes_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_capability_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_capability_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_case_site_occupancy_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_condition_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_condition_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_death_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_death_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_equip_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_equip_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_familiarity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_filth_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_filth_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_illness_status_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_illness_status_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_limbs_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_limbs_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_morale_source_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_morale_source_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_needs_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_needs_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_notoriety_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_notoriety_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_personality_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_skills_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_skills_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_stats_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_stats_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_strategic_condition_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_strategic_condition_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_time_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_time_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_topic_knowledge_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_training_schedule_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_training_schedule_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/character_virtue_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/character_virtue_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/charcoal_burning_industry_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/chat_with_party_member_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/choose_dialogue_topic_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/claim_simulation_run_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/clear_organization_presentation_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/committed_cut_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/configure_simulation_character_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/connected_player_item_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/connected_player_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/connected_players_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/conscience_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/construction_commodity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/construction_industry_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/continue_camp_travel_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/contract_interaction_stage_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/contract_issuer_interaction_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/contract_status_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/contract_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/conviction_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/cook_food_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/cooking_method_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/courtship_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/create_character_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/create_named_character_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/create_named_character_with_id_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/create_recruitment_role_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/create_starting_character_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/create_tactical_server_for_request_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/create_temporary_character_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/cropland_cover_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/crossing_traversal_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/crossing_watercourse_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/custody_holder_kind_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/custody_object_kind_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/death_cause_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/death_source_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/delete_recruitment_role_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/delete_saved_recruitment_role_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/deposit_party_inventory_item_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/derived_historical_vegetation_cover_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/derived_historical_vegetation_method_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/derived_historical_vegetation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/derived_industry_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/dialogue_action_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/dialogue_answer_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/dialogue_event_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/dialogue_investigation_binding_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/dialogue_participant_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/dialogue_prompt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/dialogue_session_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/dialogue_topic_option_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/dialogue_witness_capability_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/dialogue_witness_claim_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/direct_historical_vegetation_cover_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/direct_historical_vegetation_method_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/direct_historical_vegetation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/disband_party_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/discard_inventory_items_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/discover_investigation_lead_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/disease_notice_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/dismiss_party_action_request_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/dominant_aspect_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/dominant_leaf_type_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/drive_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/drought_history_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/drought_profile_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/eat_food_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/edge_endpoint_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/edge_progress_permille_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/elevation_meters_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/end_tactical_server_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/ensure_settlement_activity_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/enter_mission_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/equip_item_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/evidence_presentation_kind_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/fallback_historical_vegetation_cover_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/fallback_historical_vegetation_method_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/fallback_historical_vegetation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/fallback_industry_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/ferry_route_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/ferry_waterway_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/filth_disease_snapshot_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/filth_origin_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/filth_provenance_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/filth_substance_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/finale_kind_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/finale_status_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/finalize_merchant_trade_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/finalize_party_offer_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/finalize_storefront_trade_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/finish_world_data_import_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/fish_commodity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/fishing_industry_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/flow_persistence_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/flowing_water_access_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/food_contamination_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/food_lot_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/food_lot_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/food_preparation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/forage_attempt_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/forage_current_vicinity_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/forage_environment_attestation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/forest_commodity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/forest_cover_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/forestry_industry_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/generated_problem_incident_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/geologic_age_evidence_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/geologic_era_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/geologic_lithology_evidence_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/geologic_setting_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/geologic_unit_id_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/habitat_suitability_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/historical_vegetation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/historical_wetland_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/historical_woodland_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/hostile_group_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/hostile_group_disposition_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/hostile_resolution_kind_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/hygiene_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/igneous_rock_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/immediate_activity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/import_settlement_aliases_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/import_settlement_descriptions_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/import_settlements_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/import_travel_edges_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/import_world_nodes_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/incident_id_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/incident_kind_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/incident_source_id_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/incident_status_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/inclination_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/industry_evidence_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/infection_episode_row_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/inferred_geologic_setting_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/inferred_industry_profile_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/inferred_tree_species_profile_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/inland_water_access_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/inland_water_size_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/inspect_physical_evidence_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/inventory_item_amount_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/inventory_item_amount_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/inventory_item_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/inventory_item_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/inventory_quantity_target_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/inventory_quantity_target_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_action_attempt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_action_capability_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_action_outcome_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_action_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_area_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_belief_revision_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_belief_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_bestiary_deduction_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_bestiary_diagnostic_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_bestiary_report_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_case_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_claim_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_event_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_evidence_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_evidence_knowledge_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_generated_action_output_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_journal_notice_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_lead_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_observation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_pattern_target_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_received_testimony_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_recollection_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_safe_claim_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_safe_lead_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_sharing_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_testimony_bundle_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/investigation_witness_referral_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/item_condition_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/item_condition_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/item_kind_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/item_slot_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/item_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/item_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/join_dialogue_session_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/join_organization_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/journey_camp_interval_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/journey_case_site_endpoint_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/journey_endpoint_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/journey_precipitation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/journey_route_leg_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/journey_route_plan_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/journey_route_point_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/journey_settlement_endpoint_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/journey_terrain_kind_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/journey_terrain_span_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/journey_terrain_weights_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/kill_simulation_character_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/land_route_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/land_use_fraction_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/land_use_profile_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/land_water_crossing_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/leave_mission_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/leave_party_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/lib.rs— Rust source module for this component.crates/adventuresim-stdb-client/src/limb_injury_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/limb_injury_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/limb_region_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/liquidate_party_inventory_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/local_chat_message_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/local_problem_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/local_problem_generation_explanation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/local_problem_incident_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/local_problem_outcome_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/local_problem_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/local_problem_rumor_delivery_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/local_problem_symptom_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/local_problem_symptom_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/located_route_landform_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/lutheran_reformed_church_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/mapped_surface_geology_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/marine_water_access_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/metamorphic_rock_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/mined_commodity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/mineral_soil_texture_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/mineral_soil_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/mining_industry_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/mirth_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/mission_approach_capability_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/mission_attempt_status_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/mission_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/mission_outcome_candidate_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/mixed_lithology_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/mod.rs— Rust source module for this component.crates/adventuresim-stdb-client/src/modeled_tree_species_profile_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/modeled_tree_species_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/morale_event_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/morale_event_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/native_range_evidence_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/nerve_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/npc_adventuring_party_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/npc_age_band_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/npc_presentation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/npc_sex_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/objective_continuity_guard_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/objective_continuity_kind_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/official_religion_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/oral_language_hours_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/organic_soil_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/organization_membership_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/organization_membership_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/organization_presentation_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/organization_presentation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/other_non_textured_soil_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/outcome_source_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/outlook_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/palmer_drought_severity_index_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_action_request_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_action_request_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_case_site_tracking_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_inventory_item_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_inventory_item_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_inventory_state_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_inventory_state_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_item_amount_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_item_amount_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_item_condition_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_item_condition_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_join_request_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_join_request_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_journey_encounter_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_journey_itinerary_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_journey_itinerary_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_journey_route_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_journey_route_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_journey_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_journey_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_leader_vote_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_leader_vote_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_member_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_member_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_recruitment_role_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_recruitment_role_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_stake_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_stake_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/party_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/party_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/pasture_cover_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/pay_organization_dues_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/peat_cutting_industry_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/perform_immediate_activity_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/perform_investigation_action_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/perform_social_action_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/physical_evidence_inspection_action_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/physical_evidence_inspection_attempt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/physiology_administration_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/physiology_key_material_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/physiology_presence_span_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/potential_vegetation_class_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/potential_vegetation_posterior_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/potential_vegetation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/pottery_commodity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/pottery_industry_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/present_organization_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/presentation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/production_scale_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/profile_fact_provenance_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/projectile_kind_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/promote_organization_membership_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/prosperity_tier_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/public_threat_disclosure_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/purchase_from_herbalist_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/quarry_commodity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/quarrying_industry_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/quest_generation_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/receive_investigation_claim_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/receive_local_problem_rumor_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/recruitment_offer_id_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/recruitment_offer_status_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/recruitment_offer_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/recruitment_offer_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/recruitment_requirements_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/recruitment_source_id_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/refresh_capabilities_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/refresh_strategic_condition_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/register_strategic_gateway_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/reject_party_join_request_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/religion_hours_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/religious_demand_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/religious_demand_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/remove_party_member_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/rename_saved_recruitment_role_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/repair_order_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/repair_order_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/report_contract_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/request_general_party_join_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/request_party_action_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/request_tactical_server_for_scene_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/request_tactical_server_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/request_to_join_party_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/resolve_religious_demand_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/resolve_strategic_encounter_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/resolved_party_action_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/rest_at_camp_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/rest_at_settlement_hours_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/rest_at_settlement_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/retained_projectile_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/retained_projectile_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/retrieve_repaired_item_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/retrieve_repaired_items_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/revoke_tactical_server_claim_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/river_access_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/river_and_canal_access_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/river_watercourse_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/rock_outcrop_soil_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_elevation_profile_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_elevation_sample_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_encounter_tag_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_landform_kind_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_relief_meters_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_risk_severity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_roughness_meters_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_seasonal_hazard_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_seasonal_risk_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_signed_grade_permille_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_slope_permille_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_terrain_class_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_terrain_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_vertical_meters_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_water_adjacency_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/route_water_feature_kind_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/salt_source_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/saltmaking_industry_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/save_recruitment_role_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/saved_recruitment_role_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/saved_recruitment_role_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/schedule_allocation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/sedimentary_rock_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/seed_simulation_disease_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/seed_simulation_equipment_damage_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/seed_simulation_world_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/seed_standalone_tactical_mission_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/self_knowledge_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/self_regard_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/send_local_chat_message_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/set_automatic_social_chat_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/set_character_religion_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/set_inventory_quantity_target_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/set_party_camp_fatigue_percent_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/set_party_travel_itinerary_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/settlement_alias_batch_row_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_alias_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/settlement_alias_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_category_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_description_batch_row_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_description_kind_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_description_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/settlement_description_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_economy_profile_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_hydrology_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_import_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_language_profile_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_npc_morale_event_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_npc_presence_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/settlement_npc_presence_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_npc_relationship_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_npc_seed_explanation_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_npc_social_state_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_npc_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_outbreak_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/settlement_outbreak_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_religious_status_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_service_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_smith_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/settlement_smith_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_stock_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/settlement_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/settlement_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/sex_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/share_investigation_belief_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/share_investigation_lead_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/simulate_contract_issuer_interaction_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/simulation_character_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/simulation_character_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/simulation_run_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/simulation_run_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/sociability_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/social_action_cooldown_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/social_address_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/social_belief_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/social_chat_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/social_interaction_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/soil_acidity_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/soil_basis_points_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/soil_depth_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/soil_evidence_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/soil_fertility_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/soil_profile_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/soil_properties_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/soil_substrate_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/soil_water_regime_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/spawn_developer_quest_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/spend_time_with_settlement_npc_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/sponsor_party_member_inn_rest_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/stage_investigation_lead_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/start_dialogue_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/starting_age_tier_coordinate_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/starting_character_claim_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/starting_character_claim_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/stock_category_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/stone_content_percent_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/stop_preparation_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/store_battle_loot_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/strahler_order_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/strategic_encounter_loss_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/strategic_encounter_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/strategic_encounter_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/strategic_gateway_authority_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/strategic_gateway_authority_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/strategic_incident_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/submit_all_repairable_items_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/submit_item_for_repair_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/suitability_basis_points_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/surface_geology_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/surface_lithology_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/synchronize_character_time_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/tactical_mission_resolution_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/tactical_server_claim_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/tactical_server_request_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/tactical_server_request_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/tactical_server_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/tactical_server_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/temperance_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/topsoil_organic_carbon_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/track_case_site_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/transfer_party_item_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/transparency_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/travel_edge_load_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/travel_edge_provenance_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/travel_edge_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/travel_edge_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/travel_filth_progress_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/travel_route_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/travel_to_case_site_planned_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/travel_to_case_site_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/travel_to_settlement_planned_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/travel_to_settlement_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/treat_limb_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/tree_species_id_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/tree_species_profile_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/unconsolidated_deposit_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/update_character_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/update_party_check_targets_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/update_recruitment_role_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/update_training_schedule_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/upgrade_manual_surgery_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/vote_for_party_leader_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/water_distance_meters_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/weapon_skill_distribution_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/western_christian_arrangement_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/withdraw_party_inventory_item_reducer.rs— Generated SpacetimeDB reducer interface.crates/adventuresim-stdb-client/src/witness_social_action_receipt_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/woodland_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/world_clock_schedule_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/world_clock_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/world_clock_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/world_data_import_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/world_data_import_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/world_node_import_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/world_node_table.rs— Generated SpacetimeDB table interface.crates/adventuresim-stdb-client/src/world_node_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/wrb_reference_group_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-client/src/written_language_hours_type.rs— Generated SpacetimeDB data type.crates/adventuresim-stdb-module/.gitignore— Repository support file.crates/adventuresim-stdb-module/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-stdb-module/README.md— Component overview and usage notes.crates/adventuresim-stdb-module/src/alcohol.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/capability.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/character.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/condition.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/disease.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/equipment_law.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/filth.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/food.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/foraging.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/inventory_amount.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/investigation.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/item.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/lib.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/local_problem.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/npc_adventurer.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/organization.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/personality.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/repair.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/settlement_population.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/simulation.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/social.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/strategic.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/surgery.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/tactical.rs— Rust source module for this component.crates/adventuresim-stdb-module/src/time.rs— Rust source module for this component.crates/adventuresim-stdb-module/static/tactical.html— Browser UI page.crates/adventuresim-strategic-sim/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-strategic-sim/fixtures/quest-analysis-failure-v3.json— Repository support file.crates/adventuresim-strategic-sim/src/analysis.rs— Rust source module for this component.crates/adventuresim-strategic-sim/src/config.rs— Rust source module for this component.crates/adventuresim-strategic-sim/src/investigation_eval/environment.rs— Rust source module.crates/adventuresim-strategic-sim/src/investigation_eval/mod.rs— Rust source module.crates/adventuresim-strategic-sim/src/investigation_eval/policy.rs— Rust source module.crates/adventuresim-strategic-sim/src/investigation_eval/provider.rs— Rust source module.crates/adventuresim-strategic-sim/src/investigation_eval/report.rs— Rust source module.crates/adventuresim-strategic-sim/src/investigation_eval/types.rs— Rust source module.crates/adventuresim-strategic-sim/src/lib.rs— Rust source module for this component.crates/adventuresim-strategic-sim/src/live_core.rs— Rust source module for this component.crates/adventuresim-strategic-sim/src/main.rs— Rust source module for this component.crates/adventuresim-strategic-sim/src/profile.rs— Rust source module for this component.crates/adventuresim-strategic-sim/src/rng.rs— Rust source module for this component.crates/adventuresim-strategic-sim/src/runner.rs— Rust source module for this component.crates/adventuresim-strategic-sim/tests/core_loop_live.rs— Rust source module.crates/adventuresim-strategic-sim/tests/simulation.rs— Rust source module.crates/adventuresim-tactical-client/.cargo/config.toml— Tooling or build configuration.crates/adventuresim-tactical-client/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-tactical-client/README.md— Component overview and usage notes.crates/adventuresim-tactical-client/assets/SNPro-VariableFont_wght.ttf— Binary game or UI asset.crates/adventuresim-tactical-client/assets/crosshair.png— Binary game or UI asset.crates/adventuresim-tactical-client/assets/ui.css— Browser UI styling.crates/adventuresim-tactical-client/src/debug.rs— Rust source module for this component.crates/adventuresim-tactical-client/src/main.rs— Rust source module for this component.crates/adventuresim-tactical-client/src/player.rs— Rust source module for this component.crates/adventuresim-tactical-client/src/ui.rs— Rust source module for this component.crates/adventuresim-tactical-core/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-tactical-core/src/combat.rs— Rust source module for this component.crates/adventuresim-tactical-core/src/inventory.rs— Rust source module for this component.crates/adventuresim-tactical-core/src/lib.rs— Rust source module for this component.crates/adventuresim-tactical-core/src/physics.rs— Rust source module for this component.crates/adventuresim-tactical-core/src/player.rs— Rust source module for this component.crates/adventuresim-tactical-core/src/scene.rs— Rust source module for this component.crates/adventuresim-tactical-netcode/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-tactical-netcode/src/client.rs— Rust source module for this component.crates/adventuresim-tactical-netcode/src/lib.rs— Rust source module for this component.crates/adventuresim-tactical-netcode/src/message.rs— Rust source module for this component.crates/adventuresim-tactical-netcode/src/replication.rs— Rust source module for this component.crates/adventuresim-tactical-netcode/src/server.rs— Rust source module for this component.crates/adventuresim-tactical-server-dispatcher/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-tactical-server-dispatcher/src/main.rs— Rust source module for this component.crates/adventuresim-tactical-server/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-tactical-server/src/combat.rs— Rust source module for this component.crates/adventuresim-tactical-server/src/main.rs— Rust source module for this component.crates/adventuresim-tactical-server/src/stdb.rs— Rust source module for this component.crates/adventuresim-tactical-server/src/terrain.rs— Rust source module for this component.crates/adventuresim-terrain/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-terrain/src/builder.rs— Rust source module for this component.crates/adventuresim-terrain/src/lib.rs— Rust source module for this component.crates/adventuresim-world-import/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-world-import/src/bin/build-strategic-map.rs— Rust source module.crates/adventuresim-world-import/src/bin/build-strategic-map/raster.rs— Rust source module.crates/adventuresim-world-import/src/bin/build-strategic-map/tiles.rs— Rust source module.crates/adventuresim-world-import/src/builder.rs— Rust source module for this component.crates/adventuresim-world-import/src/cultivation.rs— Rust source module for this component.crates/adventuresim-world-import/src/draft.rs— Rust source module for this component.crates/adventuresim-world-import/src/error.rs— Rust source module for this component.crates/adventuresim-world-import/src/lib.rs— Rust source module for this component.crates/adventuresim-world-import/src/main.rs— Rust source module for this component.crates/adventuresim-world-import/src/manifest.rs— Rust source module for this component.crates/adventuresim-world-import/src/sources/drought.rs— Rust source module.crates/adventuresim-world-import/src/sources/economies.rs— Rust source module.crates/adventuresim-world-import/src/sources/elevation.rs— Rust source module.crates/adventuresim-world-import/src/sources/environment_synthesis.rs— Rust source module.crates/adventuresim-world-import/src/sources/forest_cover.rs— Rust source module.crates/adventuresim-world-import/src/sources/geology.rs— Rust source module.crates/adventuresim-world-import/src/sources/hydrology.rs— Rust source module.crates/adventuresim-world-import/src/sources/industries.rs— Rust source module.crates/adventuresim-world-import/src/sources/land_use.rs— Rust source module.crates/adventuresim-world-import/src/sources/mod.rs— Rust source module.crates/adventuresim-world-import/src/sources/potential_vegetation.rs— Rust source module.crates/adventuresim-world-import/src/sources/religion.rs— Rust source module.crates/adventuresim-world-import/src/sources/road_inference.rs— Rust source module.crates/adventuresim-world-import/src/sources/route_terrain.rs— Rust source module.crates/adventuresim-world-import/src/sources/soil.rs— Rust source module.crates/adventuresim-world-import/src/sources/tree_species.rs— Rust source module.crates/adventuresim-world-import/src/sources/viabundus/descriptions.rs— Rust source module.crates/adventuresim-world-import/src/sources/viabundus/mod.rs— Rust source module.crates/adventuresim-world-import/src/sources/viabundus/names.rs— Rust source module.crates/adventuresim-world-import/src/spatial.rs— Rust source module for this component.crates/adventuresim-world-import/src/validation.rs— Rust source module for this component.crates/adventuresim-world-import/tests/fixtures/viabundus/alternativenames.csv— Repository support file.crates/adventuresim-world-import/tests/fixtures/viabundus/descriptions.csv— Repository support file.crates/adventuresim-world-import/tests/fixtures/viabundus/edges.csv— Repository support file.crates/adventuresim-world-import/tests/fixtures/viabundus/nodes.csv— Repository support file.crates/adventuresim-world-import/tests/fixtures/viabundus/population.csv— Repository support file.crates/adventuresim-world-import/tests/viabundus_fixture.rs— Rust source module.crates/adventuresim-world-schema/Cargo.toml— Cargo package/workspace manifest.crates/adventuresim-world-schema/src/language.rs— Rust source module for this component.crates/adventuresim-world-schema/src/lib.rs— Rust source module for this component.crates/strategic-web/AGENTS.md— Project documentation.crates/strategic-web/Cargo.toml— Cargo package/workspace manifest.crates/strategic-web/Dockerfile— Container build definition.crates/strategic-web/README.md— Component overview and usage notes.crates/strategic-web/package-lock.json— Repository support file.crates/strategic-web/package.json— Repository support file.crates/strategic-web/src/config.rs— Rust source module for this component.crates/strategic-web/src/live.rs— Rust source module for this component.crates/strategic-web/src/main.rs— Rust source module for this component.crates/strategic-web/src/medical.rs— Rust source module for this component.crates/strategic-web/src/routes/characters.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/data.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/developer_quests.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/dialogue.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/evidence.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/foraging.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/home.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/inventory_forms.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/investigation.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/local_chat.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/missions.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/mod.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/parties.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/party_actions.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/quests.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/settlements.rs— Strategic web HTTP route handler.crates/strategic-web/src/routes/travel.rs— Strategic web HTTP route handler.crates/strategic-web/src/session.rs— Rust source module for this component.crates/strategic-web/src/spacetimedb/client.rs— Strategic web SpacetimeDB integration module.crates/strategic-web/src/spacetimedb/mod.rs— Strategic web SpacetimeDB integration module.crates/strategic-web/src/spacetimedb/types.rs— Strategic web SpacetimeDB integration module.crates/strategic-web/src/strategic_map.rs— Rust source module for this component.crates/strategic-web/src/templates/character.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/components.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/inventory_browser.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/investigation.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/layout.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/mission.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/mod.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/quest.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/recruitment.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/settlement/character_details.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/settlement/character_health.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/settlement/character_skills.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/settlement/chrome.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/settlement/context.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/settlement/mod.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/settlement/rest.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/settlement/social.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/settlement/trade.rs— Strategic web server-rendered template.crates/strategic-web/src/templates/settlement/travel.rs— Strategic web server-rendered template.crates/strategic-web/static/background-fetch.js— Repository support file.crates/strategic-web/static/building-state.js— Repository support file.crates/strategic-web/static/character-action-dialog.js— Repository support file.crates/strategic-web/static/character-candidates.js— Repository support file.crates/strategic-web/static/character-switcher.js— Repository support file.crates/strategic-web/static/chat-resize.js— Repository support file.crates/strategic-web/static/cooking.js— Repository support file.crates/strategic-web/static/css/base.css— Browser UI styling.crates/strategic-web/static/css/components.css— Browser UI styling.crates/strategic-web/static/css/layout.css— Browser UI styling.crates/strategic-web/static/css/reset.css— Browser UI styling.crates/strategic-web/static/css/strategic.css— Browser UI styling.crates/strategic-web/static/css/utilities.css— Browser UI styling.crates/strategic-web/static/developer-mode.js— Repository support file.crates/strategic-web/static/developer-quest-editor.js— Repository support file.crates/strategic-web/static/dialogue-client.js— Repository support file.crates/strategic-web/static/equipment-toggle.js— Repository support file.crates/strategic-web/static/icons/game/ATTRIBUTION.md— Project documentation.crates/strategic-web/static/icons/game/acrobatic.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/ancient-sword.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/anvil.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/arm-bandage.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/arm.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/armor-coverage-0.png— Binary game or UI asset.crates/strategic-web/static/icons/game/armor-coverage-full.png— Binary game or UI asset.crates/strategic-web/static/icons/game/armor-coverage-half.png— Binary game or UI asset.crates/strategic-web/static/icons/game/armor-coverage-quarter.png— Binary game or UI asset.crates/strategic-web/static/icons/game/armor-coverage-three-quarter.png— Binary game or UI asset.crates/strategic-web/static/icons/game/armor-cuisses.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/armor-vest.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/awareness.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/bandage-roll.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/barbute.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/bed.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/beer-stein.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/belt-armor.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/biceps.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/bleeding-eye.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/bleeding-wound.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/bo.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/bordered-shield.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/bow-arrow.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/bowie-knife.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/bracer.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/brain.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/bread.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/breastplate.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/broad-dagger.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/broadsword.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/brodie-helmet.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/broken-heart.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/bullseye.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/byzantin-temple.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/caduceus.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/calendar.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/campfire.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/camping-tent.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/castle.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/chain-mail.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/check-mark.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/chest-armor.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/church.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/clothes.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/coins.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/coma.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/conversation.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/crested-helmet.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/cross-mark.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/crossbow.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/crossed-swords.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/crown.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/daggers.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/death-skull.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/dodge.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/duration.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/eye-target.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/flanged-mace.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/gothic-cross.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/greaves.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/halberd.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/hammer-nails.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/hammer-sickle.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/heart-beats.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/heart-minus.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/heavy-helm.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/helmet.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/help.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/holy-symbol.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/hood.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/house.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/human-ear.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/inner-self.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/juggler.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/knapsack.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/layered-armor.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/leg.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/light-helm.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/lockpicks.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/mail-shirt.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/mailed-fist.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/meal.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/medical-pack.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/metal-skirt.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/mounted-knight.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/musket.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/night-sleep.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/open-book.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/open-chest.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/person.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/piercing-sword.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/plain-arrow.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/plain-dagger.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/pocket-bow.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/prayer.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/pteruges.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/relic-blade.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/rifle.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/roman-shield.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/rose.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/round-shield.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/running-ninja.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/saber-slash.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/samara-mosque.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/scales.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/scalpel.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/shield-echoes.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/shield.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/shirt.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/shop.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/skirt.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/sleeveless-jacket.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/spear-hook.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/spears.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/spiked-halo.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/split-cross.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/stiletto.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/stomach.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/stopwatch.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/sun.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/sword-brandish.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/sword-clash.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/sword-hilt.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/templar-shield.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/terror.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/tightrope.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/torch.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/treasure-map.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/trousers.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/two-handed-sword.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/visored-helm.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/warhammer.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/water-bottle.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/water-drop.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/waterskin.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/weight.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/wingfoot.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/wood-axe.svg— Vector UI or texture asset.crates/strategic-web/static/icons/game/wood-club.svg— Vector UI or texture asset.crates/strategic-web/static/icons/religion/ATTRIBUTION.md— Project documentation.crates/strategic-web/static/icons/religion/canterbury-cross.svg— Vector UI or texture asset.crates/strategic-web/static/icons/religion/catholic-cross-bottony.png— Binary game or UI asset.crates/strategic-web/static/icons/religion/catholic-crucifix.svg— Vector UI or texture asset.crates/strategic-web/static/icons/religion/fontawesome-cross.svg— Vector UI or texture asset.crates/strategic-web/static/icons/religion/fontawesome-star-and-crescent.svg— Vector UI or texture asset.crates/strategic-web/static/icons/religion/fontawesome-star-of-david.svg— Vector UI or texture asset.crates/strategic-web/static/icons/religion/huguenot-cross.svg— Vector UI or texture asset.crates/strategic-web/static/icons/religion/luther-rose.svg— Vector UI or texture asset.crates/strategic-web/static/icons/religion/orthodox-cross.svg— Vector UI or texture asset.crates/strategic-web/static/icons/settlement-services/armor.png— Binary game or UI asset.crates/strategic-web/static/icons/settlement-services/clothing.png— Binary game or UI asset.crates/strategic-web/static/icons/settlement-services/herbalist.png— Binary game or UI asset.crates/strategic-web/static/icons/settlement-services/inn.png— Binary game or UI asset.crates/strategic-web/static/icons/settlement-services/market.png— Binary game or UI asset.crates/strategic-web/static/icons/settlement-services/travel.png— Binary game or UI asset.crates/strategic-web/static/icons/settlement-services/weapons.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/attributes/agility-arm.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/attributes/agility-leg.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/attributes/immunity.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/attributes/strength-arm.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/attributes/strength-leg.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/ATTRIBUTION.md— Project documentation.crates/strategic-web/static/icons/stats/bestiary/beast.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/bestiary.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/construct.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/draconid.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/dwarf.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/elf.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/fey.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/greenskin.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/human.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/insectoid.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/spirit.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/undead.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/werekin.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/bestiary/wildmen.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/terrain/forest.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/terrain/hills.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/terrain/plains.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/terrain/snow.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/terrain/terrain.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/terrain/urban.png— Binary game or UI asset.crates/strategic-web/static/icons/stats/terrain/wetlands.png— Binary game or UI asset.crates/strategic-web/static/immediate-activity.js— Repository support file.crates/strategic-web/static/inventory-browser.js— Repository support file.crates/strategic-web/static/journal-tab.js— Repository support file.crates/strategic-web/static/live-regions.js— Repository support file.crates/strategic-web/static/live-state.js— Repository support file.crates/strategic-web/static/local-chat.js— Repository support file.crates/strategic-web/static/numeric-editor.js— Repository support file.crates/strategic-web/static/party-notifications.js— Repository support file.crates/strategic-web/static/party-recruitment.js— Repository support file.crates/strategic-web/static/party-trade.js— Repository support file.crates/strategic-web/static/physical-evidence.js— Repository support file.crates/strategic-web/static/physiology-dialog.js— Repository support file.crates/strategic-web/static/rest-duration.js— Repository support file.crates/strategic-web/static/service-quests.js— Repository support file.crates/strategic-web/static/social-menu.js— Repository support file.crates/strategic-web/static/strategic-condition.js— Repository support file.crates/strategic-web/static/strategic-map.js— Repository support file.crates/strategic-web/static/strategic-mutations.js— Repository support file.crates/strategic-web/static/strategic-navigation.js— Repository support file.crates/strategic-web/static/strategic-time.js— Repository support file.crates/strategic-web/static/styles/timber-framed/ATTRIBUTION.md— Project documentation.crates/strategic-web/static/styles/timber-framed/background/city/coastal.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/background/city/inland.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/background/city/river.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/background/town/coastal.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/background/town/inland.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/background/town/river.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/background/village/coastal.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/background/village/inland.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/background/village/river.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/background/wilderness/forest.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/background/wilderness/grassland.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/background/wilderness/hills.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/city/armor.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/city/clothing.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/city/herbalist.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/city/inn.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/city/keep.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/city/map.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/city/merchants.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/city/public-square.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/city/religion.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/city/residences.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/city/weapons.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/town/armor.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/town/clothing.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/town/herbalist.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/town/inn.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/town/keep.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/town/map.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/town/merchants.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/town/public-square.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/town/religion.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/town/residences.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/town/weapons.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/village/armor.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/village/clothing.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/village/herbalist.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/village/inn.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/village/keep.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/village/map.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/village/merchants.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/village/public-square.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/village/religion.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/village/residences.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/building/village/weapons.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/ornament/camp-tent/ornament.png— Binary game or UI asset.crates/strategic-web/static/styles/timber-framed/ornament/encounter-boulders/ornament.png— Binary game or UI asset.crates/strategic-web/static/tooltips.js— Repository support file.crates/strategic-web/static/training-schedule.js— Repository support file.crates/strategic-web/static/travel-planner.js— Repository support file.crates/strategic-web/tests/building-assets.test.cjs— Repository support file.crates/strategic-web/tests/building-state.test.cjs— Repository support file.crates/strategic-web/tests/character-action-dialog.test.cjs— Repository support file.crates/strategic-web/tests/character-candidates.test.cjs— Repository support file.crates/strategic-web/tests/cooking.dom.test.cjs— Repository support file.crates/strategic-web/tests/cooking.test.cjs— Repository support file.crates/strategic-web/tests/currency-backend-source.test.cjs— Repository support file.crates/strategic-web/tests/developer-mode.test.cjs— Repository support file.crates/strategic-web/tests/developer-quest-editor.test.cjs— Repository support file.crates/strategic-web/tests/dialogue-client.test.cjs— Repository support file.crates/strategic-web/tests/environment.test.cjs— Repository support file.crates/strategic-web/tests/equipment-toggle.test.cjs— Repository support file.crates/strategic-web/tests/food-remediation.test.cjs— Repository support file.crates/strategic-web/tests/icon-rendering.test.cjs— Repository support file.crates/strategic-web/tests/immediate-activity.test.cjs— Repository support file.crates/strategic-web/tests/inventory-browser.dom.test.cjs— Repository support file.crates/strategic-web/tests/inventory-browser.test.cjs— Repository support file.crates/strategic-web/tests/journal-tab.test.cjs— Repository support file.crates/strategic-web/tests/live-refresh.test.cjs— Repository support file.crates/strategic-web/tests/local-chat.test.cjs— Repository support file.crates/strategic-web/tests/numeric-editor.test.cjs— Repository support file.crates/strategic-web/tests/party-action-contract.test.cjs— Repository support file.crates/strategic-web/tests/physical-evidence.test.cjs— Repository support file.crates/strategic-web/tests/quest-web-eval.test.cjs— Repository support file.crates/strategic-web/tests/rest-duration.test.cjs— Repository support file.crates/strategic-web/tests/service-quests.test.cjs— Repository support file.crates/strategic-web/tests/social-menu.test.cjs— Repository support file.crates/strategic-web/tests/strategic-map-behavior.test.cjs— Repository support file.crates/strategic-web/tests/strategic-mutations.test.cjs— Repository support file.crates/strategic-web/tests/strategic-navigation.test.cjs— Repository support file.crates/strategic-web/tests/tooltips.test.cjs— Repository support file.crates/strategic-web/tests/training-schedule.test.cjs— Repository support file.crates/strategic-web/tests/travel-planner-behavior.test.cjs— Repository support file.flake.lock— Nix environment configuration or lockfile.flake.nix— Nix environment configuration or lockfile.justfile— Repository support file.rust-toolchain.toml— Tooling or build configuration.scripts/build_wasm.py— Development or documentation automation script.scripts/dev_stack.py— Development or documentation automation script.scripts/init_forest_cover.py— Development or documentation automation script.scripts/init_jung_pnv.py— Development or documentation automation script.scripts/init_owda.py— Development or documentation automation script.scripts/init_soilgrids.py— Development or documentation automation script.scripts/init_viabundus.py— Development or documentation automation script.scripts/init_world_data.py— Development or documentation automation script.scripts/init_world_runtime.py— Development or documentation automation script.scripts/just_tasks.py— Development or documentation automation script.scripts/quest_web_eval.mjs— Repository support file.scripts/test_init_forest_cover.py— Development or documentation automation script.scripts/test_init_jung_pnv.py— Development or documentation automation script.scripts/test_init_owda.py— Development or documentation automation script.scripts/test_init_soilgrids.py— Development or documentation automation script.scripts/test_init_viabundus.py— Development or documentation automation script.scripts/test_init_world_data.py— Development or documentation automation script.scripts/test_init_world_runtime.py— Development or documentation automation script.scripts/test_world_data_bundle.py— Development or documentation automation script.scripts/test_world_runtime_release.py— Development or documentation automation script.scripts/test_world_source_init.py— Development or documentation automation script.scripts/tests/test_dev_stack.py— Development or documentation automation script.scripts/tests/test_just_tasks.py— Development or documentation automation script.scripts/update_project_map.py— Development or documentation automation script.scripts/validate_organization_world.py— Development or documentation automation script.scripts/world_data_bundle.py— Development or documentation automation script.scripts/world_runtime_release.py— Development or documentation automation script.scripts/world_source_init.py— Development or documentation automation script.utils/generate_certificates.py— Development or documentation automation script.wiki/SUMMARY.md— Project documentation.wiki/client/animation.md— Project documentation.wiki/client/controls.md— Project documentation.wiki/client/models.md— Project documentation.wiki/client/slots.md— Project documentation.wiki/implementation.md— Project documentation.wiki/meta.md— Project documentation.wiki/networking.md— Project documentation.wiki/reference/architecture.md— Project documentation.wiki/reference/bestiary.md— Project documentation.wiki/reference/developing.md— Project documentation.wiki/reference/dialogue.md— Project documentation.wiki/reference/drought.md— Project documentation.wiki/reference/elevation.md— Project documentation.wiki/reference/equipment.md— Project documentation.wiki/reference/food-and-cooking.md— Project documentation.wiki/reference/foraging.md— Project documentation.wiki/reference/forest-cover.md— Project documentation.wiki/reference/geology.md— Project documentation.wiki/reference/historical-land-use.md— Project documentation.wiki/reference/hydrology.md— Project documentation.wiki/reference/industries.md— Project documentation.wiki/reference/item-authoring.md— Project documentation.wiki/reference/llm/project-map-maintenance.md— Project documentation.wiki/reference/measured-inventory.md— Project documentation.wiki/reference/organizations.md— Project documentation.wiki/reference/physiology.md— Project documentation.wiki/reference/potential-vegetation.md— Project documentation.wiki/reference/quest-authority.md— Project documentation.wiki/reference/quest-generation-and-investigation.md— Project documentation.wiki/reference/religion.md— Project documentation.wiki/reference/route-terrain.md— Project documentation.wiki/reference/soil.md— Project documentation.wiki/reference/source-manifests.md— Project documentation.wiki/reference/spatial-grid.md— Project documentation.wiki/reference/strategic-read-cache.md— Project documentation.wiki/reference/strategic-simulation.md— Project documentation.wiki/reference/tree-species.md— Project documentation.wiki/reference/viabundus.md— Project documentation.wiki/reference/world-data-bundles.md— Project documentation.wiki/roadmap.md— Project documentation.wiki/scenario.md— Project documentation.wiki/shared/encumbrance.md— Project documentation.wiki/shared/energy.md— Project documentation.wiki/shared/health.md— Project documentation.wiki/shared/inventory.md— Project documentation.wiki/shared/magic.md— Project documentation.wiki/shared/morale.md— Project documentation.wiki/shared/stats.md— Project documentation.wiki/shared/strata-map.md— Project documentation.wiki/shared/terrain.md— Project documentation.wiki/strategic/character.md— Project documentation.wiki/strategic/quests.md— Project documentation.wiki/strategic/settlement.md— Project documentation.wiki/strategic/time.md— Project documentation.wiki/strategic/trade.md— Project documentation.wiki/strategic/travel.md— Project documentation.wiki/tactical/combat.md— Project documentation.wiki/tactical/stealth.md— Project documentation.world-data-release.lock.json— Repository support file.world-runtime-release.lock.json— Repository support file.
Controls
This page only covers controls relating to movement, attacking, and blocking/dodging. Hotkeys are described on the slots page, and menus are described in their respective pages:
Much of this page is liable to change in the near future. We assume many of our developers will be interested in taking ownership and providing input on game design, which we strongly encourage. Thus, the goal of this page isn't really to describe the game's controls; the top priority is to provide a list of design goals and principles for the controls, mostly downstream of the principles laid out in the readme. After that, we provide a tentative proposal/outline for a control scheme which meets those goals.
Goals
In order of importance, we want controls which are unambiguous; comprehensive; immediate; convenient; and intuitive.1
Unambiguous
We are opposed to "context-sensitive actions" where the response to user input depends on the state of the game, particularly when the state is something continuous like your character's position or what you're looking at.

Certainly context-sensitive actions can make your controls "simpler" in the sense that you're using fewer buttons, but it'll also make them a lot more cumbersome than they have to be, and your player may end up quite surprised by what his character ends up doing in response to a given input. There are games with "simple" controls, enabled by context-sensitive actions, where no matter how much you play them, you can't perform these actions on instinct because you must always ensure the context is appropriate for them. That's not to mention anything with menus, which are worse for this on an entirely different level.
None of this for us! Space Station 13's controls are nowhere close to ideal, but what they have going for them is that once you get used to the controls, they're very good at becoming instinct: a quality very much worth replicating. Our contrarian view is that reliable beats simple every time.2
Comprehensive
You should be able to make your character do anything he or she could physically do which would be situationally advantageous.3
Immediate
In cases where a button does one thing if single-tapped but something else if double-tapped or held, some games will start an invisible timer after a first tap is registered, delaying the onset of the following animation until the player's intention is made clear (with a second tap/hold or lack thereof).
We will not be one of those games. It's a fair enough solution, but we want immediate feedback for the player, and we don't want any invisible timers to delay post-tap animation starts solely for the sake of single-/double-tap differentiation. Note this doesn't preclude all timers; we may certainly have two time-differentiated inputs which share the same starting animation. For instance, since jump and crouch animations begin the same way, it would be safe to use a hold or double-tap to differentiate the two.
Convenient
Buttons that you press often should be near your fingers. Buttons that you press often, like to grab an item, or need to be able to press immediately, like to dodge, should not use the same fingers as those needed to look and move.4
Intuitive
All else being equal, it'll be nice if the game's controls are intuitive and easy to learn, but this isn't a priority.
To the extent that the game's controls are complex, it'll be great if the complexity is optional, especially at the character level. Playing as an alchemist with a bandolier of different potions and doodads might require you to use more buttons than a naked barbarian with a big stick, and starter players may be encouraged to play characters more like the latter.
But ultimately, our contrarian view is that new players don't actually want "simple" controls; they want reliable controls. Insofar as that's true, following the previous guidelines should already get us where we want to be vis-à-vis accessibility.
Proposal
With our design goals established, we now suggest a tentative outline for the control scheme.
One feature we quite want in the game eventually is a split between direct and indirect controls. By default, the player has direct control of his character, and the game plays like an action game, but with a toggle, the player may relinquish control to an AI and direct it with RTS-like controls. You'd generally use this feature to navigate large, boring areas, to order around multiple characters, or simply because you don't like action games.
We posit control schemes for both modes below.
Direct controls
These are designed primarily for the first person, but we can add a third-person camera option after the MVP.
Halbe: I assume that first person is easier because with third-person cameras, you need to handle a lot of edge cases to avoid awkwardness in tight spaces or near thin obstacles like trees, not to mention smoothing out the motion or reconciling the shoulder offset when aiming. However, if I am mistaken in my assumptions, we should implement whichever is easier for the MVP.
| M+KB | Controller | Function | Notes |
|---|---|---|---|
| WASD | Left Stick | Movement. | |
| Mouse | Right Stick | Look. | |
| LMB | RT | Attack! | |
| Release SPACE | Full LT | Dodge or jump. |
|
| SHIFT | Partial LT | Crouch or duck. |
|
| CTRL | Left Stick Click | Prone–standing toggle. |
|
| Scroll | Right Stick Click | Aim. |
|
| MMB or RMB | LB or RB | Grab with left/right hand. |
|
This is somewhere between a real action game and an RPG wearing an action game's skin. We aren't actually simulating everything based on hitboxes and projectile trajectories, but we still want to use some of the player's mechanical skills, specifically accuracy and reaction time.5
- Precision is the value between 0 and 1 representing how much of the hitbox the attack has penetrated. Each hitbox is a skin of the body part, while its scaled version is a core; and the ratio between distance of the hitreg to skin to full distance between skin to core is hit precision.
- Reflex is the value between 0 and 1 representing how quickly the defender pressed the dodge/parry button after the attack began. Like with precision, we aren't sure exactly how to derive this, but a value of 1.0 would correspond to pro gamer reaction time (0.1s) and ~0.75 would correspond to old person reaction time (0.25s).
- There is no need to "time" your input to correspond with when an attack will actually hit as is convention in most action games. As soon as an enemy begins its attack animation, you should press the button.
For CPU-controlled characters (NPCs or indirect mode), the server... usually... randomly samples these parameters from a normal distribution with some mean and variance of our choice.
Indirect controls
We may have some version of this concept in the MVP if only because much of the underlying behavior is shared with NPCs controlled by the server. You are essentially giving NPCs orders through the same system that the AI uses to give them orders.
The following outline gives an idea of how things might work when controlling an army with a recursive chain of command, but as we aren't actually doing any RTS stuff for the MVP, it's more here as a distant if (hopefully) attainable aspiration, or in case an implementer is a passionate RTS/RTT enthusiast. The recursive nature of the system means that the controls should still make sense for small parties or individuals.
| M+KB | Controller | Function | Notes |
|---|---|---|---|
| RMB | RT | Move to. | While in combat, characters automatically attack enemies in range, so moving to an enemy is just attacking it in melee. |
| LMB | RB | Select. |
|
CTRL+LMB/Group | LB+RB/Group | Select multiple. |
|
| MMB+Mouse | Left Stick | Rotate camera. | |
| WASD | Right Stick | Pan camera. | |
| Scroll | Hold Left Stick Click + Left Stick Up/Down | Adjust camera Y-level. | |
| SHIFT+LMB | Select everything between current selection and new selection. | ||
| SHIFT+RMB | LT+RT | Place waypoint. | |
| SPACE+RMB | LT+RB | Ping. | |
| LT+Left Stick Click | Swoop camera to selected character. |
Universal
These inputs would theoretically find use in both direct and indirect modes.
Halbe: I have not thought too hard about their mapping. They should be remapped so that the most commonly pressed buttons are the most convenient to move your finger to.
Bruno: It'll likely be the case that these inputs are unavailable when a grab or select button is held due to overlapping with slots. When you hold RB, X is a slot/group button representing a holster on your left hip; if you aren't holding RB, X can be used for one of the menu inputs below.
- Toggle inventory menu.
- Toggle logout/character menu.
- Toggle quest menu.
- Toggle rest menu.
- Toggle direct/indirect control.
- Skip time.
- In normal use, this toggles between real time and sim-time. When camped, it skips straight to the end of your rest. Either way, it's automatically interrupted if the party spots an enemy or encounters difficult terrain.
- In the strategic layer (GSG mode), you continue to see the map at a consistent speed. In the direct camera or tactical layer (RTS mode), we can display a cinematic montage of travel or night passing. Not in MVP.
-
Also, as a broad note, we would rather follow operating system conventions than video game conventions wherever we can, especially for menus and any RTS-like controls. Video game conventions are designed for people who've played a lot of video games; OS conventions are designed for people. ↩
-
By "every time", we mean it. We believe that given enough playtime, someone who's never played a game in his life would prefer a game with SS13-like unambiguous controls to a game with Hitman's. You can generally compare our view on mass appeal to that of Stanley Kubrick:
Kubrick likened the understanding of his films to popular music, in that whatever the background or intellect of the individual, a Beatles record, for instance, can be appreciated both by the Alabama truck driver and the young Cambridge intellectual, because their "emotions and subconscious are far more similar than their intellects". He believed that the subconscious emotional reaction experienced by audiences was far more powerful in the film medium than in any other traditional verbal form, and was one of the reasons why he often relied on long periods in his films without dialogue, placing emphasis on images and sound... When deciding on a subject for a film, there were many aspects that he looked for, and he always made films which would "appeal to every sort of viewer, whatever their expectation of film".
In short, contrary to popular belief, if you want to best appeal to the masses, you don't actually want to simplify things. 2001: A Space Odyssey is the highest-grossing film of 1968 in the United States and Canada. There's a Pareto frontier of artistic merit and mass appeal, on which sat Kubrick and the Beatles, and we're aiming right for it. ↩
-
That's an important caveat. There are situations where you might want to go prone, but dedicated yoga position buttons are not a priority. (This may sound like an argument for context-sensitive actions, and in some cases it may be; as stated above, our primary opposition is to continuous-state context-sensitive actions, which leaves things open for contexts depending on discrete states, say in yoga class vs. not in yoga class.) ↩
-
Thus, grabbing and dodging shouldn't use the thumbs on a controller. ↩
-
This is trivial to cheat, but since combat is still (largely) based on stats and (entirely) mediated by the server, it's not a huge deal. ↩
-
This is a bit awkward, but you shouldn't normally be skipping the chain of command anyway. ↩
Models
As a rule, we'll be keeping game assets as simple as possible. We want to make it easy for players to create content that fits with the art style; it's a barrier to entry if that style uses high-fidelity handmade assets.
Subject to that constraint, however, we want the game to look as good as possible, so the game will use procedural models: high-fidelity algorithmically created assets. In theory, an eight-year-old should be able to use our algorithms to make content that looks as good as the rest of the game.
Halbe's proposed algorithm for humanoids
Below we propose a method of generating a character mesh. By the fourth version, we have a humanoid body with muscle and flesh, and the tools we've used to get us there can give us clothing and armor with with little additional required functionality.
Mesh resolution
smooth_theta is a variable passed into the mesher which describes approximately the detail that it should be created at. We define it as:
The theta in radians between the surface normals of any two faces on an ostensibly round surface.
So, for example, a value of π / 8 implies that a cylinder ought to have 16 vertices in its rings.
The value of smooth_theta depends on how far away the mesh is from the camera, how large its bounding box is, and screen resolution. It should be set so that when looking at a sphere, it is difficult to see the flat polygonal edges of it against the background. This also means there is a dynamic LOD system: meshes are regenerated if the distance gets, say, close enough that the ideal smooth_theta is half the current value, or so far that it's twice the current value.
First version: collider-based
The simplest possible mesh for a character is one which conforms precisely to the shape of his bones' colliders.
Let's say each bone is a capsule. Traverse the skeletal hierarchy and generate a capsule mesh for each bone with the vertices all skinned to that bone.
Second version: distance fields and vertex weights
Next, in order to connect the bones, we convert their capsule meshes into 3D signed distance fields (SDFs). This affords two distinct advantages: it's easy to combine SDFs in a way that smooths out the joints, and for each point p on the surface, to estimate a given bone b's influence on p, we can just evaluate b's SDF (a real-valued function) on p; taking this "influence" estimate on each bone and point automatically gives us the skinning weights for all bone-on-point pairs.
With the 3D distance field capsule primitives giving us a function to place vertices onto, we now have the basis for constructive solid geometry. We place vertices and build triangles according to a modified version of advancing front.
Polar-space advancing front tree (rename to whatever you want)
Every vertex begins as a polar UV coordinate on a bone,
$$U,V\in[0,1]$$
with some arbitrary angle picked for the orientation of U = 0 (probably whatever places it at the back, assuming T-pose, like along the spine for the torso).
A bone is sort of between a capsule and a cylinder. V = 1 on a leaf bone (or V = 0 on the root bone) is always the center, like the apex of a capsule, and has no defined U. However, V = 1 on a bone which has another bone connected to its end has a defined U. There is essentially a V > 1 value due to the connection between the bones acting like a capsule, rather than a cylinder. You can think of this however you want, but essentially when two bones are 90 degrees from each other, the joint between them is still nice and spherical.
We begin at (0, 0) at the base of the pelvis and start constructing a triangle fan. The heuristic for placing vertices is based on the smooth_theta parameter, both for what V to place it at as well as what U. Once we have a fan, all of the vertices except for the apex are our advancing front.
There are two types of ways that the advancing front on a given bone handles connected bones. In the simple case, the other bone is a continuation of the shape of the current one. The relationship between the upper arm and forearm, or along spine bones, follows this pattern. In this case, the front seamlessly transitions between the bones using the pseudo-capsule method described above.
The second scenario is when you have bones that branch off of the current one. In this case, the front will essentially go around it by diverging at one of the vertices. The vertices in the gap created by this divergence are kept as a new, separate front which will be used later once the current bone or contiguous chain of bones has finished meshing (the mesher is depth-first). Eventually, the front will pass completely over the gap and the divergence will be restored, also creating a continuous ring for the new front to begin from, repeating the advancing front process.
When the front reaches the distal end of a bone with no more connected bones, it must place an apex vertex and connect the front to it with a triangle fan.
Because the front is advancing in UV space, not 3D space, an important optimization and simplification is available:
- The heuristic for placing vertices can forbid placing any vertex to the left of a neighbor to its left, in UV space, or to the right of its neighbor to the right.
- We assume that the hierarchy of bones is not self-intersecting.
- Because of this, in theory this can greatly speed up the algorithm since there's no need to test for intersections.
But what about the shoulder?
Many a plan to procedurally generate a skinned character mesh has been defeated by the most infamous of joints: the shoulder.
Essentially, our plan is to forget about trying to weight the shoulder correctly, or even do the topology correctly. Instead, after the mesh is generated (say, for example, in a T-pose), we apply an animation which lowers the arms, putting it in the worst-case scenario, but then calculate where each given vertex would be if it were placed on the surface again (as described above). Since these are distance fields, which combine in a smooth metaball-like fashion, the surface of the armpit will actually be quite a bit lower now as the arm and chest distance fields now nearly overlap.
There will no doubt need to be a lot of tweaking -- perhaps the armpit is now too low -- but this may just produce a mesh that looks more correct when the arms are down. Thus, we will save this as a morph target and animate it according to how much the upper arm bone is currently lowered. This can be done to every joint; even though none are quite as bad as the shoulder, none of them will have particularly good topology or thoughtful vertex weights1, so they may still benefit from the process.
Third version: heightmaps
This is the feature that enables characters, specifically body meshes, to actually look pretty realistic. As established, each bone has a UV semi-cylinder/semi-capsule space used for placing vertices. We can reuse this, not only as a universal space for textures, but also to apply a heightmap to the distance field which converts UV to 3D.
Each bone has a heightmap defined in its UV space. Though we've been treating bones as capsules up to this point, it is the heightmap which actually encodes the spherical curvature of the top of the head or tips of the fingers, i.e. it is the heightmap which gives a bone its "shape" and lets us stop treating it like a pure capsule. (However, it is still capsule-ish on connections between bones to avoid it looking jarring if they aren't perfectly aligned.) To actually produce these heightmaps, we can either bake them from a sculpt/scanned medical model or paint them with an in-game editor.
Halbe: I've experimented with both methods in Blender. They each work well enough.
Cruicially, we can also composite these heightmaps to produce many different character meshes. This has worked great in manual Blender experiments. Essentially, you can have a base heightmap representing a smooth, slender character, and you can add a bone layer, muscle layer, and fat layer on top of it. Each layer is both a mask for the layer below and additive with it, which makes for a pretty good approximation of how they're physically layered in the body.
Fourth version: face
We are under no illusion that a system as simple as this can handle geometry like ears, nose, eyes, etc. Even a sharp chin will be a little awkward to handle. For the face, we can just use a system similar to what Nintendo used for Miis (and reused for its recent Zelda games).

The simplest version of this doesn't even include separate meshes for the facial features; they're just textures that get overlaid onto the face.
Fifth version: clothing and armor
By now, we have a mesher with an unambiguous, universal coordinate system for the surface of the body (from the second version) and a way to encode height (from the third). We can use this same mesher to produce meshes for body-conforming equipment entirely through texture data.
The main thing we need to support this is an alpha mask which specifies where the clothing is and isn't. For instance, on a T-shirt, past (say) V = 0.3 on the upper arms, the value of this mask would go from 1 to 0. We could also use this to create holes in clothing, which the mesher would treat similarly to intersections between bones.
Once the outer surface of a clothing mesh is completed, we can use a solidify algorithm to turn it into a proper model.
Unlike in the third version, we probably wouldn't have the heightmap for clothing with respect to the surface of the skin -- otherwise, a baggy shirt on a ripped guy would have abs -- but rather with respect to a "convex-only version" of the base body mesh. That is, we would generate a version of the base body's heightmap where any concave surface is pushed outwards until it is no longer concave.
Armor is like clothing, except in the case of non-flexible material like metal plates, all vertices need to have 1.0 weight with a single bone regardless of their location. Without an extremely detailed physics simulation, this will mean lots of clipping, but this is acceptable.
-
All automated weighting techniques are mediocre, ours included. But in our approach, SDFs give us the ability to generate morph targets to refine the mediocrity. ↩
Slots
A slot is a physical location on your character's body which an item can be stored in, or attached to, for quick access via hotkey.
For example, the right hip can be a slot:
- If you have a belt on, you can place a sheath on your left hip with Q.
- If you have a sheath on your left hip, you can put a sword in it.
The goal is for slots to replace menus to access most of your inventory. Anything not inside a bag should be immediately accessible with a slot button. Slot buttons can overlap with other buttons because they only function as slot buttons when a grab button is held.
Keybindings
Ideally, the location of each slot button should correspond roughly to the slot's physical location. A button on the left should be used for a slot on the left side of the body, and we should attempt to group the buttons so that adjacent slots have adjacent buttons.
| M+KB | Controller | Slot |
|---|---|---|
| Q | X | Left belt. |
| E | Q | Right belt. |
| F | Y | Front belt. |
| X | A | Back belt. |
| Tab | Select | Left shoulder. |
| R | Start | Right shoulder. |
| 2 | ⇐ | Left pocket. |
| 3 | ⇒ | Right pocket. |
| 1 | ⇓⇐ | Back-left pocket. |
| 4 | ⇓⇒ | Back-right pocket. |
| T | ⇑ | Head. |
| ⇓ | Face? | |
| ` | ⇑⇐ | Left arm. |
| 5 | ⇑⇒ | Right arm. |
| Glasses? | ||
| Ears? | ||
| Z | ⇓⇓⇐ | Left foot. |
| C | ⇓⇓⇒ | Right foot. |
The controller doesn't have quite enough buttons to give every slot its own button. To get around this, we can assign certain slots to combinations of buttons; because slot inputs require the grab button be held to initiate them, and we don't execute any action until the grab button is released, no ambiguity is possible. (For instance, while holding the grab button, the face slot can be ⇑ and glasses can be ⇑⇑; only when we release the grab button does it actually perform the action.) This is also helpful for controllers that only support four D-pad directions; the diagonals can just be pressing two directions in either order. (That is, "down-left" can be "down and then left.")
In the map proposed above, we rely on button combinations for directly adjacent slots, which we imagine as lying on a navigable grid navigated by the D-pad.
Layers
Pressing a slot button once selects the outer layer of that slot. Pressing it again -- without releasing the grab button -- selects one layer deeper. For example, press Q once to draw your sword from your sheath, twice to remove the sheath itself, and three times to remove your belt.
Multi-slot items
Many items, generally clothing and armor, occupy multiple slots. A belt occupies all four belt slot buttons. It can be equipped and removed using any of these buttons.
Slot restrictions
Many items may only occupy specific slots. When such an item is held in your hand and you hold the corresponding grab button, all buttons not corresponding to those slots are unavailable.
GUI
The screen normally gives no indicator for what is in your slots or your hands. However, holding down any grab button brings up a "map" of your slots with a few properties:
- This map includes icons for each button and approximately corresponds to the keyboard/controller; the relative position of each slot should be based on the relative position of each button.
- When holding an item, any slot it may be placed in is white, and all others are grayed out; if your hand is empty, slots with items in them are white, and empty ones are greyed out.
- Each layer of item in a slot is visible in this interface. Layers for items that occupy multiple slots contiguously span all relevant slots.
Bags
Your entire inventory won't necessarily fit into the slot system, which is fine. The slot system is intended not to replace "standard inventory management" altogether but to make a significant subset of your inventory more manageable, that being the subset of items that you need readily accessible. If you don't need a given item readily accessible, you can put it in a bag.
A bag still occupies a slot, but it can hold multiple items. For example, a backpack is a bag which is slung over your shoulder(s).1 To access a bag's internal contents, you must grab the bag into one of your hands; when you are holding the bag, the grab button for the hand opposite the hand holding the bag is used to grab/place into it, and the hand holding the bag functions normally: if you simply press the associated grab button, you will drop the bag, and if you hold it and press a slot, you will place the bag in that slot.
Alternative controls
The goal of the slot system is to obviate the need for menus in inventory management, for the most part, and thereby simplify most aspects of inventory management. Following the philosophy laid out in the Controls page, it will likely be hard to learn but ultimately speed up gameplay for experienced users on account of its consistency and unambiguity.
If this is not the case, we can also try more of a middle ground with conventional systems. For instance, we could turn all slot buttons into hotkeys untethered to any physical locations on the body. Players would still place items onto these hotkeys to equip them, but it wouldn't matter which button they pressed. This would make layering and multi-slot buttons a mess, so clothing and armor would just have to be done through a normal inventory menu. Sheaths and holsters would be handled like clothing; players would have to equip them from a menu and be forbidden from hotkeying a weapon without having equipped a sheath to put it in.
-
In the real world, carrying a backpack on one shoulder can lead to strain, pain, and posture problems. It is always recommended to use both shoulder straps. ↩
State
There may be four layers to the state of a character:
- Their input/NPC AI (what action are they attempting to do?)
- What action are they actually doing? Let's call this "skeleton state".
- The forward/inverse kinematic animation playing
- Secondary animation (physics)
Input and NPC AI
When you press W, there is an input system that determines that you are trying to move forward. When an NPC has a goal to move to a position that is in front of it, it has a pathfinding system that tells it to move forward. Both of these set the same "skeleton state", which is a component that keeps track of the fact that this character is trying to move forward.
Skeleton State
The state of the skeleton tells you everything that you need to know about what a character is doing this frame. Are they moving? Attacking? Both? Grabbing an item? Staggered? Jumping? Being knocked down? This is the minimum information necessary to reproduce the current animation for your character, and thus is what would be synchronized over the network. From a client's perspective, it is not (necessarily) clear whether another character is an NPC or a player, at least were it not for the existence of voice/text chat, because the only thing your client sees is their skeleton state, not input or AI.
The skeleton state may also be more complicated than what the controls imply due to needing to include information like "which direction are they ducking in?" or "when in a combat stance, which side is currently facing the enemy?" (when you slash with a weapon, this switches since you take a single step).
Forward and Inverse Kinematics
There is a system which recieves the skeleton state and actually animates the bones. It may not actually be strictly necessary to run this on the server, its possible that only clients need to see this since the only thing that the location of your individual bones affects is your input precision. We may want to use this as a reference for producing these animations.
Halbe: There might also be a layer before this for an animation graph, if the person implementing this feels that such an abstraction would be helpful
Secondary Animation
Clients may also apply animations via joint motors to make them more realistic, as each bone can now have inertia which may be affected by movement or collisions with the environment or weapons. This is not in-scope for the MVP though.
Stylistic Principles
We want to try and keep animations realistic, in accordance with the meta-level heuristics that govern our decisionmaking for this project. This means, for example, that melee attack animations should generally be inspired by HEMA. In general this means that there is a lot less "leading" or "anticipation" to attacks than you typically see in action game or movie choreography, at least for professional characters. But beyond the MVP, we can make multiple animation sets for different skill levels of characters. Untrained characters like goblins, for example, can have lots of anticipation in their attacks which is what makes them relatively easy to dodge.
Due to the fact that this is an online game, we also have to account for the fact that animations will have different speeds depending on perspective. Since all networking will be mediated by the server, we want to be able to extend animations by the duration of a network round-trip so that they can start immediately when you press the button but end at the same time for all players. This is a pretty good excuse to add some otherwise unnecessary leading to the player character's animations, which may look better than simply slowing the attack animation down.
const LOWER_MUSCLE_MASS_PER_LEG_STRENGTH = 5
const WEIGHT_CAPACITY_PER_LOWER_MUSCLE_MASS = 30
fn encumbrance_term(character):
average_leg_strength = (character.legs.left.strength + character.legs.right.strength) / 2
lower_muscle_mass = average_leg_strength * LOWER_MUSCLE_MASS_PER_LEG_STRENGTH
weight_capacity = WEIGHT_CAPACITY_PER_LOWER_MUSCLE_MASS * lower_muscle_mass
return 1 - ((player.calculate_body_weight() + player.inventory.calculate_weight()) / weight_capacity)
Encumbrance is linear. The term above is the remaining multiplier, clamped
between 0 and 1; the displayed penalty is 1 - encumbrance_term. A character
at half capacity therefore has a 50% penalty, and reaching or exceeding
capacity gives the maximum 100% penalty.
The burden includes body weight, carried water at 1 kg per litre, and every
carried inventory row multiplied by its quantity. Equipped items are already
inventory rows and count exactly once. Injury-adjusted capacity uses the
average of left and right leg strength after multiplying each leg by its
current health. Party calculations use each living member's authoritative body
weight from character_condition; invalid or missing legacy values use the
70 kg schema default rather than silently treating every member as the same
weight.
Inventory rails split the summary into equal-width halves. The left half shows the exact burden and capacity to one decimal place with the exact penalty to one decimal percent directly below it. The right half is a stable-width green-to-yellow-to-red meter whose marker follows the same linear penalty. Merchant Player tabs use the personal summary; merchant Party tabs use the living-party aggregate. The party chest also shows this aggregate: all living party members' burdens and capacities are summed, with the shared chest burden added once. Dead members contribute neither burden nor capacity, but the shared chest remains part of the aggregate.
The recruitment Athletics tag combines climbing and swimming performance and uses this same shared encumbrance penalty, so a packed inventory lowers the recommendation.
Energy is an abstraction representing approximately the maximum amount of calories can be used in a day. The lower it gets, the less effective you are, because your body is resorting to more difficult-to-extract energy sources (fat or protein instead of blood sugar, depleted glycogen reserves in muscles, etc). Additionally, your metabolism can only actually absorb so much nutrients in a given day, it takes time to digest food and extract nutrients from it.
The exact biological functions aren't really relevant to gameplay, but they are physical processes to base our equations on.
Points of reference
- A small, sedentary person uses ~1500 kcal/day
- An average male soldier marching all day uses ~6000 kcal/day
- An Olympic athlete in an endurance event can use as high as ~12000 kcal/day
- Calories metabolized by humans per-gram
- Fat: 9
- Protein: 4
- Uncooked starch: 2
- Cooked starch: 4
- Sugars: 4
- Cellulose: 0.2
- Alcohol: 7
Equations
Current food and water needs
Food and water advance only with a character's authoritative strategic clock. Settlement life currently assumes that ordinary meals and drinking water are provided, including lazy catch-up and explicit rest. Entering a settlement restores one day of short-term food and hydration reserve and refills every owned waterskin.
Travel uses 6,000 kcal and 4 litres of water per full day, applied proportionally for partial days. A travel ration supplies 6,000 kcal. A waterskin carries 4 litres. Characters automatically eat personal rations and drink carried water whenever their short-term reserve would otherwise become negative.
Unsupported hunger reaches full strategic incapacitation after three marching days beyond the food reserve. Unsupported thirst reaches it after one marching day beyond the hydration reserve. Both curves are quadratic, begin only below zero reserve, and combine with pain, blood loss, fear, and fatigue. They do not currently kill a character.
The character panel presents each signed physiological balance as a centered meter. Positive reserve fills right, normalized to the ordinary one-day reserve. Once the balance crosses zero, hunger or thirst fills left according to its quadratic incapacitation contribution. Carried rations and water are not included; they affect the meter only when automatically consumed.
Health
Adventure Simulator uses durable injuries, blood loss, disease, pain, and recovery rather than a single rapidly refilling combat-health pool. The strategic/tactical split keeps that depth playable: immediate danger happens in combat, while treatment and long recovery can advance through strategic time.
Body regions and injuries
Damage is associated with seven body regions. Injury reduces the function of the affected region and therefore its related attributes. Damage beyond incapacitation still matters because it lengthens recovery and may destroy or sever tissue.
Strategic injuries distinguish:
- open cuts, which bleed and deteriorate until bandaged;
- bruising, which heals without a procedure;
- fractures, which recover better after splinting;
- retained projectiles, which slow every healing component on that limb.
Autoresolve commits these durable results after the battle. Real-time tactical combat will eventually produce the same result summary without persisting its live hit-by-hit state.
Blood loss and incapacitation
Open wounds drain blood in proportion to their severity. Bandaging stabilizes a cut; stitching and projectile extraction require more specialized tools and skills. Extremely low blood volume is fatal.
Pain, blood loss, fear, fatigue, and physical injury all contribute to whether a character is ready for strategic activity. A character can survive a battle yet remain unable to travel or fight safely.
Disease
Characters do not automatically know which disease they have. They can observe outward symptoms and seek help from someone trained in Physiology, but the result is a fallible differential rather than an authoritative diagnosis.
Diseases use specific transmission routes such as close contact, food and water, vermin, wounds, or infected blood. Filth raises some risks. Blood on a character remains visibly dirty until washed, while its infectiousness fades over strategic time.
Prepared interventions act on the patient's condition rather than naming a disease they magically cure. Treatment may improve or worsen the evidence available to an observer, but never reveals hidden truth directly.
The detailed privacy, meter, Humour, and notebook contract lives in Physiology.
Treatment
Treatment is performed on one patient and one body region at a time:
- bandaging uses Anatomy and a bandage;
- splinting uses Anatomy and a splint;
- projectile extraction combines Anatomy and Knife;
- stitching combines Anatomy and Tailoring;
- cleaning consumes water, soap, and—where appropriate—disinfectant.
Procedures advance the participants' personal strategic time. They may be interrupted by terminal events, and supplies are consumed only according to the validated procedure boundary.
Treating another character can transfer their blood to the caregiver. Clean tools, soap, bandages, and suitable alcohol therefore matter beyond simple UI flavor.
Recovery
Recovery occurs during unallocated leisure rather than every elapsed minute. A fully scheduled day grants no passive convalescence.
The wound category determines its base recovery, modified by the party's Physiology support. Retained projectiles impede recovery. Blood volume restores over time once bleeding is controlled.
Settlements offer the safest place to convalesce because a party can advance time, obtain food and shelter, and access services. Recovering in the field is possible but competes with travel, supplies, exposure, and continuing danger.
A living party member at the same settlement may pay an inn directly for one day of another member's publicly necessary convalescence when the patient cannot pay. The patient contributes the coin they have, and the inn receives only the remaining authoritative price directly from the payer; the patient never receives transferable coin. This cooperative lodging does not grant authority over treatment, diagnosis, inventory, or arbitrary stretches of the patient's time.
Fantasy relief
Real recovery can be slow. The game uses two ways to keep realistic injuries from becoming dead time:
- strategic time can skip uneventful recovery;
- fantasy ancestry, rare preparations, or other setting-specific content may provide costly exceptions without changing the physical baseline.
The intent is for wounds to shape an expedition and its aftermath without forcing the player to watch every hour pass.
- Broadly speaking, anything supernatural in the setting originates from collective belief
- People believe that the spirits of the dead coexist in the world alongside them.
- The "spirit world" is essentially just a separate layer of the world that generally maps 1:1 to it.
- Heaven is just being in the spirit world in good standing with God
- Hell is in poor standing
- Many spirits are in neutral standing (think: nature spirits, non-malicious pagan deities, fairies and such).
- You can't really go to or see into the spirit world, so from the player's perspective this is all very vague
- Magic is the intervention of spirits in the world of the living
- Its hard magic in the sense that there are different kinds of spirits with specific, knowable things that they can do and specific, knowable ways of communicating with them
- Its soft magic in the sense that communication with them is unreliable and since they are conscious entities, they are capricious and often unpredictable
- There is no "mana" or any explicit resource like that. How much they choose to help you is no different than how much an actual person might choose to help you, and their help may be conditional on what exactly you want their help for.
- The closest thing to "mana" or a measurement of how much magic power you have is essentially just the sum of how much various specific spirits are willing to help you out.
- You don't even have an accurate way of determining what this hypothetical number is though, due to communication being difficult and unreliable.
- Some spirits can help you out in combat, functioning as handicaps for players with poor mechanical skills
- These function at the input-level. If you are already very accurate with aiming your attacks, they won't really help you much and they can't help you be more accurate than a skilled gamer
- There are three different kinds depending on which mechanical skill you are lacking in
- Precision spirits act as if your aiming reticle is closer to the target
- Reflex spirits act as if your reaction time when performing a dodge was faster
- Leading spirits act as if you were leading a target by the correct amount, like if an enemy is moving horizontally relative to you then you can simply aim at where the enemy currently is, not where they will be, and the spirit makes up the difference
- These spirits are not discrete, you can have a small magnitude of assistance of them if you are not quite pro-gamer level or a large amount if you are extraordinarily bad at the game
- They do not apply in any PvP "dueling" context
- Spirits, by default, try and help you in a time of need
- They can cause enemies to whiff attacks, fail to detect you, or turn a lethal blow into a non-lethal one
- This works better the weaker your character is
- This lets you play as a child or a halfling or an old person and not just be dead weight in your party
- Many players will not like the realistic, hardcore nature of the world and should invest much of their favor into this kind of spirit to compensate so that it's less brutally punishing
- Alternatively, spirits can be instructed to not generally act of their own volition to help you, instead being commanded directly by the player
- To do this, you must "equip" the spirit from a slot
- A spirit can be associated with a given slot
- It can be associated with a specific trinket, like an heirloom or a crystal. It may be affixed to a wand, scepter, or staff that you hold. Or you can just hold the trinket itself. This is a western convention.
- It can be bound to a tattoo on your character at that location on your body
- It can be imbued within your body itself, aesthetically this has something to do with chakras. This is an eastern convention.
- If its a tattoo or chakra you can't have any armor covering it, otherwise you can't access it
- Using a tattoo/chakra for magic makes it very fast to equip/use, using a staff is more powerful but unwieldy
- Once equipped, a spirit may change what your actions do
- Attacking with a spirit will cause the spirit to attack with whatever sort of effect or element that its associated with.
- If you are powerful and an element is readily available in the environment the effects of the attack may extend far beyond your weapon/hand
- Attacking with a staff/wand using a spirit is essentially what a wizard normally does. The "spell" is just based on what kind of attack and spirit you do.
- Not so much "fireball", more pointing it at a lantern hanging on a building and flaring it up, causing some embers to spread around and ignite a fire
- The UI might give you some indication of what's going to happen: when you look at a source of a given element you can see some embers/snowflakes/static over the area that it might affect if you were to attack
- These "spells" are all things which spirits may normally do of their own volition, you're just causing them to do these at-will rather than automatically when you are in danger
- Even as a generalist, you don't usually see a lot of opportunities for this. If you aren't near a plentiful source of a given element there isn't a ton you can do
- Even though wind and earth might comparatively be very plentiful, you actually need the wind to be blowing or to be near a faultline to do anything significant with them
- Elemental magic is not a reliable way to fight enemies, its something that is situationally extremely useful but usually irrelevant.
- An unarmed martial artist using a spirit is essentially bending, as in Avatar: TLA
- The effects would be much less fantastical though
- In comparison to using a staff, it would be faster but with less magnitude in its effects and shorter range. Essentially you could go toe-to-toe with an equally skilled armed and armored combatant while yourself unarmed and unarmored. Staves are too unwieldy for this if an enemy gets close.
- Attacking with a regular weapon just imbues that weapon with the element. This is how you light your sword on fire or electrify it. It still has to plausibly (though improbably) obey the laws of physics though, so if you want a flaming sword it should have some oil on it to burn.
- Some types of spirits may specifically help non-attack actions more. Wind spirits help with dodging, ducking, running, and jumping. Water spirits help with swimming. Fire spirits prevent you from catching fire when walking through fire. Earth spirits help you maintain your poise.
- These defensive actions might not necessitate the spirits being actually equipped in your hands. But you probably do need appropriate slots to be unarmored.
- Some spirits will want to help you with a specific cause
- Divine spirits try and help you do good, they will not only be useless if you try and do evil but will outright work against you
- When your character prays to God, they are essentially saying "God, please lend me some divine spirits and I will use them to carry out Your will"
- There can also be lesser nature spirits that aren't necessarily acting on behalf of God, yet are similarly conditional with their assistance. This is how a druid gets their magic, and they will normally only function within a specific area (conventionally a forest) that the spirit is associated with
- Some spirits are purely transactional
- You can obtain their assistance through explicitly transactional means. Make an offering to them, help out their descendants
- This is how a wizard goes about obtaining spirits
- Some spirits can be entertained
- Play or sing music or dance to entertain spirits
- This is how a bard obtains spirits
- This isn't purely within the span of combat, if you want them to keep following you around you should be dedicating a good fraction of the day towards entertaining them
- Favor with spirits are the default sort of quest reward, rather than every random peasant having enchanted weapons lying around to give you or heaps of gold or you just receiving abstract "experience points" (not a thing)
- In general, its very rare for spirits to care much about NPCs. They are one of the factors that differentiate your characters from NPCs, which crucially doesn't require you to actually be physically powerful
- You don't normally need to worry about random NPCs being saved via divine intervention by spirits and screwing up your combat.
- You do need to worry about this in PvP. If you want to go randomly murder some poor noob your assassination might not only be foiled by their spirit, but you may be cursed if you manage to succeed in spite of this.
- If your character is powerful physically or involved in faction warfare, you will not be protected against "unfair" PvP assassinations
- Spirits try and communicate with you
- Usually to warn you of danger or guide you on a quest
- The means of communication depends on the type of spirit
- Wind spirits may cause ominous howling wind from a particular direction
- Fire spirits might flare a candle or cause it to go out
- Earth spirits might cause a tremor
- Nature spirits cause plants to wither
- Non-elemental spirits might just communicate via music, representing your character's gut intuition
- The more attuned your physical senses are the better you can sense them
- You want good hearing with no helmet on to best hear wind spirits
- You shouldn't be wearing shoes to sense earth spirits
- Outright being blind or deaf can help you focus on whatever other senses you are relying on
- You might do this situationally with a blindfold. Like you sense that something is awry, then close your eyes or put on a blindfold to better listen to the wind
- Some characters prefer to be attuned with a particular type of spirit, others are more general
- You could specifically be a pyromancer/firebender, or generalize across two or more
- The more you generalize the less effective you will be with any particular kind of spirit
- The exact taxonomy of spirits is setting-dependent
- In a modern/sci-fi setting
- Rather than there being separate "fire" and "ice" spirits there's just thermodynamic "spirits" that now obey the law of conservation of energy
- Wind/earth/water essentially become telekinesis
- Lightning/metal become electromagnetic
- Rather than being "magic" this is stylized as "psionics" or "the force"
- In a modern/sci-fi setting
Morale is a signed strategic stat. Zero is emotionally neutral. Negative morale creates fear incapacitation, while surplus morale above zero lets a character with Command lift the spirits of allies who are below zero.
Witness conversations reuse the relationship and morale resolution curve used by party social actions. Affinity, familiarity, diagnosis quality, approach risk, and private personality fit all affect an attempt. Quest disclosure is a separate consequence: a successful approach can release bound testimony, while a benign concern yields only clarification. This separation prevents morale or affinity results from becoming a hidden-truth oracle.
The character-sheet morale meter is informational. A raised Social meta-skill icon beside its heading opens the observer-specific morale sources, beliefs, and available social actions in a modal dialog without replacing either character rail. The icon remains inset while that dialog is open; the privacy boundary remains the active observer's beliefs rather than authoritative personality state. Manual response buttons show a response-specific icon and short label; hovering the icon explains the contextual approach, skill, and risk. Every manual response spends five strategic minutes. Automatic responses occur within already-consumed downtime and do not advance the clock again.
Companion responses also include Pray for defeat, injury, fatigue, hunger, and faith concerns, but not filth. Prayer uses the companion's authoritative professed tradition and the actor's effective Religion knowledge for that exact tradition. At least some direct study of the tradition is required before correlated knowledge contributes; the actor need not profess it. Zealous actors will not lead another character's prayer, so the action remains visible but disabled. Target Conviction changes prayer fit: Zealous targets respond more strongly to a successful prayer but are more sensitive to a poor one, while Irreverent targets are harder and riskier to reach. The prayer catalog shares mechanical topic profiles while supplying Catholic, Lutheran, Reformed, Anglican, Orthodox, Islamic, and Jewish devotional language. Opt-in automatic social care considers Prayer alongside the other eligible approaches using the same target-specific check, personality fit, cooldown, and authoritative resolver.
Living, co-located companions show an observer-specific Party Rail badge for each current actionable negative source row that this character has not yet successfully addressed. Separate sources count separately even when they share a topic. Failed attempts, another actor's successes, and successes for another target do not clear the badge. Selecting a notified portrait or its badge opens that companion's Social dialog directly; the hover actions retain personal inspection.
Successful addresses are kept as a compact actor-target-source projection only while that exact morale source remains current. The durable interaction log still retains both successes and failures for history, but routine Party Rail reads never replay that append-only history.
Morale sources
Every current morale effect is retained as a named signed source for the UI. Positive and negative sources are ranked separately by absolute magnitude. The strongest source on each side contributes fully, the second contributes one half, the third one third, and so on. Will mitigates only the ranked negative contributions:
positive_contribution = positive_source / positive_rank;
negative_contribution = negative_source / negative_rank / will_check.max(0.25);
base_morale = sum(positive_contributions) - sum(negative_contributions);
The current strategic sources are:
- Injuries.
- Recent victories and setbacks, which decay linearly over seven days of the affected character's strategic time.
- The difference between allied and enemy power at a quest location. Undead use a 1.5 fear multiplier and demons use 3.0; other enemies use 1.0.
- Religious conviction and mixed-faith discord.
- Morale restored by individual allies.
- Standing cleanliness: Neutral characters moderately dislike filth; Slovenly characters ignore it; Cleanly characters suffer a severe scaling penalty and receive a modest benefit while completely clean.
- Mastery enjoyment: effective training rejected at any skill's governing aptitude cap feeds one shared cross-skill source. It approaches a four-point limit with a 40-hour e-fold scale. All rejected gains in one logical clock interval are combined before saturation. Existing enjoyment decays linearly through that interval before the combined award refreshes it at the endpoint, and reaches zero after seven days without another award. Changing skills does not reset or multiply the saturation.
Food quality and disease will become additional named sources when those systems are implemented. Comfort-seeking/Ascetic is a good follow-up personality axis only after food and lodging distinguish quality levels, so it has meaningful conditions to react to.
Personality reactions
Personality stores thirteen immutable behavioral axes. The existing nine are joined by Merry/Grave, Amorous/Proper, Open/Guarded, and Introspective/Self-deceiving. Generated profiles still activate exactly two to four non-neutral behavioral axes. Presentation and Inclination are always assigned and do not count toward sparsity. Conscience is present but has no morale hook until outcomes carry durable moral context.
Alcohol preference is evaluated for every absolute nightly rest opportunity: the sleep window begins at 18:00 and continues through 08:00, so a rest that starts after 18:00 still processes that evening. Temperate characters neither seek alcohol nor react to its absence. Neutral characters seek 15 ml pure ethanol on ordinary evenings and 45 ml on the first evening without another qualifying heavy evening in the prior seven days; satisfaction grants +1 or +3 morale and failure gives -1 or -3. Drunkards seek 45 ml every evening, gaining +5 when satisfied and -5 when unsatisfied. The values are named balancing constants. A durable per-character/evening row records consumed ethanol and whether morale was evaluated, so long rests, short-rest sequences, departure clock synchronization, and emergency drinking cannot duplicate an evening. The latest result replaces one refreshable alcohol morale source at that evening's absolute 18:00 timestamp; nightly bonuses and penalties therefore age correctly and never accumulate as an unbounded series. This is preference, not physiological dependence.
Carousing remains a social leisure activity and Charm-training allocation. Its existing schedule effect is not interpreted as an additional inventory- backed alcohol reward; the nightly alcohol event is the only alcohol-specific morale source.
Reactions modify each raw source before positive/negative ranking and Will mitigation. Brave/Fearful halves/doubles outmatched fear; Ambitious/Content multiplies victory and defeat by 1.5/0.5; Sanguine favors positive sources by 1.25 and negative sources by 0.75 while Brooding does the reverse. Sanguine negative events last half the normal duration and Brooding ones last twice as long. Proud multiplies victory by 1.5 and defeat by 3, while Humble multiplies both by 0.75. Zealous/Irreverent multiplies religious conviction, prayer, discord, neglect, and religious events by 1.5/0.5. Gregarious/Solitary multiplies incoming named ally restoration by 1.5/0.5 before the existing cap at neutral morale.
Personality changes what an event means, but true tags are never appended to public source labels. Will governs coping with ranked negative morale, Command governs the party restoration budget, Religion governs tradition-specific knowledge, and Conviction governs personal and cohort ardor.
Lifting allies
Party Command does not contribute a permanent flat morale source. Instead, positive-morale party members share one party-wide restoration budget. Command uses its own social-coverage aggregation rather than the generic party skill formula. The strongest member supplies the base check. Additional members provide a rapidly saturating coordination bonus, then help or hinder according to how far their individual check is above or below the neutral 2.5 baseline:
let coordination = 1.125 * (1.0 - (1.0 / 3.0).powi(supporter_count));
let support = supporters.map(|check| 0.5 * (check - 2.5)).sum();
let party_command = (best_check + coordination + support).clamp(0.0, 5.0);
This produces approximately 4.5 from one character at 4.5, three characters at 3, or a 4 and a 2. Adding large numbers of characters at 1 or 2 lowers the result once their limited coordination benefit is exhausted.
The party's positive base-morale values are aggregated with the same ranked diminishing returns. The resulting restoration percentage approaches, but never reaches, a limit of 5% per point of the aggregate party Command check:
let party_command = aggregate_party_command(member_command_checks);
let party_surplus = cumulative_morale(member_positive_base_morale);
let saturation = 1.0 - (-party_surplus / 10.0).exp();
let party_restoration = saturation * 0.05 * party_command;
Ten aggregated surplus morale reaches about 63% of the party's limit, 20 reaches about 86%, and 30 reaches about 95%. Party Command is capped at 5, so the shared restoration limit cannot exceed 25% regardless of party size.
The party budget is divided among positive-morale members in proportion to their individual surplus, allowing the UI to show who is doing the encouraging without applying the party bonus more than once. All surplus values are calculated before receiving help from allies. This makes the relationship acyclic: two high-morale characters cannot recursively increase one another's output. If support would restore more than the listener's entire deficit, the named contributions are reduced proportionally. Ally support can lift a character only to zero and can never create surplus morale.
Fear and the morale meter
Within the negative half, successful social_interaction morale is shown as a
separate purple striped segment beside the remaining fear. Its size is computed
from the same current, ranked morale-source projection shown in the Social
dialogue, and is capped by the gross current actionable negative contribution.
It is therefore an explanation of the current net morale, like a treated
injury segment, rather than extra morale. Color, striping, and meter text all
identify the segment.
Each negative morale point produces one percentage point of fear incapacitation, so -100 morale is the meaningful left endpoint of the meter. The center represents neutral morale. The right side shows the character's allocated share of the party's current ally-restoration percentage relative to the party's present 5% × aggregate Command limit. Selecting the meter opens the dedicated social panel and source actions.
The strategic condition and morale-source tables are refreshable projections. Durable state remains in character condition, injuries, strategic time, time-stamped morale events, and static personality. A negative event's persisted expiration already includes its Sanguine or Brooding duration adjustment, so expires_at_minute is authoritative rather than a projection-only reinterpretation. Personality is assigned before ordinary NPC events are recorded and is immutable thereafter. Personality is strategic identity, never tactical tick state. A missing legacy personality row is safely treated as fully neutral.
Religion
A character makes or changes their religious profession by speaking with a priest at a church. Each settlement currently has one church and one fixed faith; its priest can convert a character only to that faith. Religion is a dialogue topic even when the priest also has a quest to discuss, rather than a service-menu choice. A priest cannot make a character faithless. Characters renounce their current faith from the Religion entry on their own biography instead. Large cities may eventually support multiple churches, but that is outside the current settlement model.
Only a professed Zealous character receives the positive religious-leadership morale source. Its magnitude comes from the party's aggregate effective Religion check for that character's own tradition, and any living member may contribute regardless of profession. Same-profession social pressure instead comes from personality Conviction: Zealous contributes 5.0, Neutral 2.5, and Irreverent 0.0. A character with no professed religion receives neither this source nor religious pressure.
For each believer, the other religious cohorts are combined into foreign faith pressure. Mixed-faith tension is deliberately subtractive: party Command is subtracted from that pressure, and only the uncovered remainder becomes raw negative morale. This means capable social leadership can remove discord entirely rather than merely dividing it down:
let foreign_pressure = aggregate_party_check(other_cohort_checks).clamp(0.0, 5.0);
let discord = 3.0 * (foreign_pressure - party_command).max(0.0);
The resulting Religious discord source then receives the same negative-source ranking and Will mitigation as other morale penalties. A unified party therefore gets the largest available conviction benefit without discord; a mixed party retains the conviction of each faith but generally pays a leadership-dependent cost.
Fervor
Fervor is a bounded strategic pressure meter, not another morale source. It shows how close religious conviction is to becoming inflexible behavior. Individual personality Conviction, the character's same-profession Conviction cohort, and surplus morale raise pressure; aggregate party Command is subtracted as restraint. Characters with no professed religion always have zero Fervor.
let pressure = (individual_conviction + cohort_conviction + positive_morale / 10.0
- party_command - 2.5).max(0.0);
let fervor = 1.0 - (-pressure / 5.0).exp();
The curve lets arbitrarily high pressure approach 100% without reaching it. The strategic character rail displays this value from Calm through Fervent to Frenzy.
Daily prayer is an activity in the settlement-downtime schedule rather than a dialogue-style demand. Its existing saturating morale is multiplied by the party's tradition-specific Religion check divided by five, then receives the normal religious personality reaction. It trains direct hours in the professed tradition at 25% of explicit study speed. Fervor creates a continuous desired prayer allocation of up to two hours per day. A character without a profession instead meditates: this gives 25% of the saturating morale independently of Religion checks and personality religious scaling, and creates no Religion study, Fervor, or neglect. Scheduled prayer and meditation are not attempted while traveling.
Sunday remains an explicit demand rather than a random Fervor event. Day 7 and every seventh calendar day thereafter is Sunday. A professing character with nonzero Fervor who is at a settlement receives the choice once that Sunday:
- Observe: spend one full day in settlement. The character receives a small positive morale event.
- Do not observe: keep complete freedom of action. The raw morale penalty is
max(0, 8 × Fervor − 1.6 × party Command), so both Fervor and Command change the result continuously and a Command check of 5 eliminates even the maximum penalty.
Any strategic journey that overlaps Sunday counts as choosing not to observe it and applies exactly the same penalty once per character for that Sunday. This includes leaving Saturday night and returning Monday morning. A pending Sunday prompt is resolved as refused on departure, while an already answered Sunday cannot be charged twice. Demands are choices, not involuntary character actions, but spending Sunday on the road is itself the party leader's choice.
A Quarrel at the Gate
The first severe Fervor incident is a single cross-faith settlement scenario. When a party arrives at a settlement, the highest-Fervor member who follows a different religion rolls against their current Fervor. On success they insult the local faith and draw an armed crowd. A character at 20% therefore has a 20% arrival chance and one at 80% has an 80% chance, with no unlock threshold. Each party can trigger this incident only once per settlement.
The incident deliberately reuses the quest-location combat flow. Arrival is interrupted at a zero-distance encounter named A Quarrel at the Gate. The party can:
- Initiate tactical combat using the normal tactical-server request.
- Autoresolve using the normal quest autoresolve damage and battle-result path.
- Open the encounter map and travel away without fighting.
The incident temporarily occupies the party's active-encounter slot while preserving any real active quest. Winning or leaving restores that quest. Leaving marks the incident avoided and does not immediately trigger another incident at the destination reached by that retreat. The same shared encounter machinery also handles Thievery and Raiding discoveries, although those activities use their own scenario text and risk formulas. There are no religious quest-choice demands; quest dialogue consequences for mixed-faith parties remain future work.
Social responses
Morale sources are not dialogue memories. Each source has a closed topic such as defeat, injury, fatigue, hunger, faith, or filth. The character page's Morale meter opens a social panel where a party member can listen, commiserate, use humor, rally with Command, offer a deceptive reframe, or flirt. Labels are generic and grounded in the durable source; the game does not invent incidental details about a battle or conversation.
Joke and Flirt remain distinct actions even though both use Charm. Actor personality determines whether those approaches are available: Grave characters cannot Joke, and Proper characters cannot Flirt. Their reserve has a compensating benefit when Rallying: Grave and Proper each add 0.35 to the actor's Command check, stacking to 0.70 when both apply. Merry and Amorous characters retain the corresponding action, while target personality and mutual inclination/presentation still determine how well it lands.
Successful realized morale improvement increases the recipient's directional Affinity toward the actor. Gains diminish near the positive cap. Failure, exposure, or a boundary-crossing response can lower Affinity. Repeating the same approach to the same source has a 24-hour target-clock cooldown. Passive party morale projection uses Command but never creates Affinity.
Command contributed to another party member is multiplied by that pair's best shared Oral-language coefficient. The same coefficient scales other directed Social skill checks; self-directed reflection is unaffected.
Attributes
The Terrain family includes Snow, a mental, intuitive, Intelligence-governed skill with a 30,000-hour curve. Snow has symmetric 0.20 ordinary correlation with Plains, Forest, Hills, Wetlands, and Urban. It is an overlay skill: snow-covered forest still uses and trains Forest while Snow blends into the check. Cover conservatively splits the existing road-discounted exposure between Snow and the underlying biome.
The strategic interface represents attributes, skills, schedule activities, condition metrics, Fervor, Morale, Age, Virtue, and Religion with recolourable CSS masks. Most use locally vendored monochrome Game Icons; arm and leg Strength and Agility plus Immunity retain the original strategic-interface artwork for legibility at compact sizes. Labels and tooltips remain available to assistive technology. The maximum value of your characters' attributes is determined by their genetics, but the actual value may be quite a bit lower if they are not properly conditioned. For example, even if you have the theoretical ability to build a large amount of muscle, if you have poor nutrition or don't exercise then you will realize very little of it. Conditioning is different for each attribute, but generally no one will be able to condition all of their attributes to their maximum potential due to there only being 24 hours in a day.
Attributes are grouped between Chest/Stomach/Head/Limbs (L/R, A/L). Damage to one of these areas will affect all attributes within.
Chest
Endurance
Represents the strength of your heart, capacity of your lungs, and proportion of slow-twitch/fast-twitch muscle fiber. It determines how long you can go without suffering from exhaustion and how fast you move when traveling. Conditioned by traveling on foot.
- Asphyxiated
- Dainty sheltered nobles
- City-folk
- Knights and peasants
- Professional soldiers
- Adventuring heroes
- Undead (cannot be tired)
Stomach
Immunity
This is essentially a combination of the liver, spleen, and other organs which regulate your immune system and ability to filter out toxins.
- AIDS
- Infants
- Sheltered nobles and children
- Rural commoners and knights
- Elves and city-folk
- Vampires (immune to disease)
Gut
Your stomach, intestines, pancreas, and other organs involved with your digestive system. Determines how edible food needs to be in order for you to effectively digest it and how much variety you need to be decently healthy. Cooking makes food more edible, but food that is more fibrous and less nutritious can only be improved by so much.
- Vampires (cannot digest food, must get calories directly from blood glucose)
- Elves (can only eat meat, fat, and luxurious elven plants)
- Nobles, orcs
- Professional soldiers, goblins
- Peasants (can survive almost entirely on grains without huge penalty)
- Livestock
Limbs
These attributes are separate among 4 limbs:
- Right arm
- Left arm
- Right leg
- Left leg Every physical check will use some proportion of these. For example, swinging a sword in your right hand is largely dependent on your right arm, but your left arm is also being used for balance and your legs are helping put force into it. Your torso is also twisting to support this, but rather than being a separate limb, your torso is essentially a fuzzy mix of all limb attributes (mostly arms).
Strength
Proportional to the total muscle mass of the limb. Arm-strength is important for attack damage, climb speed, and how well you keep your balance while blocking attacks. Leg-strength is important for movement speed and jump height.
- Cripple
- Child
- Adult woman, pubescent boy
- Adult man
- Trained knight
- Olympic athlete
Agility
The speed of your muscular reflexes and your ability to control them. Arm-agility is important for accuracy and parrying, leg-agility is important for stealth and dodging.
- Paralyzed, unaware, or tied up
- Drunken oaf, orcs, zombies
- Clumsy, goblins, skeletons
- Professional soldiers and knights
- Heroes, surgeons, locksmiths
- Elven heroes
Head
In theory eyesight/hearing should be further subdivided into eyes/ears for damage purposes, while intelligence and instinct are brain. In fact, ask a neurologist but intelligence/instinct would be correlated with different physical locations in the brain. But this is fine for now, we do not need infinite detail for the MVP.
Intelligence
The depth at which your character can think. Intelligence governs learning and mastery for Physiology, Anatomy, Cooking, Religion, Bestiary, and the Terrain leaves. It does not add to their final checks.
- Not capable of conscious thought
- Low-functioning autistic, toddler
- Would struggle to learn even basic math
- Can learn high-school-level math
- Can learn college-level math
- Can meaningfully contribute to the field of mathematics
Instinct
Your ability to make snap judgements without thinking. Instinct governs learning and mastery for Will, Insight, Charm, Command, and Deception. It does not add to their final checks.
- Unconscious
- Takes a couple seconds to respond if you ask them a question
- Absentminded
- Alert
- Veteran captain
- Enlightened monk
Eyesight
- Blind
- Needs glasses, many fantasy enemies like goblins or zombies
- Below average human
- Above average human
- Elven warrior
- Hawk, elven archer
Hearing
- Deaf
- Muffled, many fantasy enemies like goblins or zombies
- Attended too many rock concerts
- Has never been to a rock concert
- Deer
- Blind monk
Skills
Every skill has exactly one governing aptitude: Intelligence, Instinct, or Agility. Aptitude controls training speed and the effective-rank limit; trained skill rank supplies the check itself.
Training
Skills increase on a much longer timescale than is conventional for RPGs. They are not increased via an abstract XP/leveling system, and very little of their value comes from using them during tactical play. Instead they are trained through activities in the character's off-screen settlement-downtime schedule. Individual skill-study allocations are not available.
Combat Training practices the leaf skills relevant to the equipped weapons plus Dodge, Block, Balance, and Will; it includes both sparring and target practice. Carousing trains Charm, improves Morale, and carries a small Virtue penalty. Prayer, Labor, Thievery, and Raiding retain their related training and strategic results. Profession activities cover Physiology, Anatomy, Knife, Tailoring, Smithing, Command, and knowledge of the settlement church's religious tradition. An activity conserves its training time when it covers several skills rather than awarding the full allocation to every skill. Travel never performs scheduled settlement activities. Activity rows preview the signed Gold, Virtue, Morale, and Fatigue generated per day by the current allocation; notoriety-producing activities display that cost as negative Virtue. Leisure is the unallocated remainder and includes sleep.
Selecting an explicit activity icon previews and performs one continuous one-to-24-hour interval using the same training and outcome rules. Its preview is based on the chosen duration; Prayer/Meditation and Carousing are nonlinear, so increasing their duration has diminishing Morale returns. Immediate activity never includes implicit Leisure or modifies the recurring allocation.
A character may join multiple YAML-defined organizations. Each organization chooses its own name, chapters, recognition, admission fee, recurring dues, rank names and requirements, curriculum, rewards, and privileges. Requirements may freely mix skills and professed religion; skills never imply membership. A character can present as exactly one active, dues-current organization at a time (or none). Presentation controls recognized privileges such as bearing arms or wearing armor where settlement policy would otherwise forbid them.
Organization training and professional activity are available through the schedule while the character is at a chapter. Rank advancement follows the next rank's YAML requirements rather than universal apprentice/journeyman/master thresholds. Skills with no invested training hours remain omitted until training first awards hours.
An ordinary day generates 600 fatigue-reservoir units before tiring activities. Leisure removes 100 units per hour, so six hours exactly offsets ordinary wakefulness. Labor adds another 50 units per hour. Leisure beyond six hours first removes activity fatigue, then fatigue carried into the interval; only the portion of the interval after the reservoir reaches zero earns morale, approaching 4 points per full qualifying day with a 200-unit diminishing-return scale. The schedule displays a one-day preview, but the server awards the result proportionally to the settlement-downtime time actually applied. Earned Leisure morale is kept as one refreshable source capped at 4 points, rather than being projected from the post-rest schedule or stacked into separate events. It decays at a fixed rate when no qualifying Leisure is occurring; qualifying Leisure refreshes it while adding the newly earned amount. This makes the result independent of whether downtime is applied all at once or through frequent synchronization. The compact schedule preview shows one Fatigue point per 100 reservoir units: Labor therefore shows +0.5 per hour, while Leisure includes baseline and recovery so all visible Fatigue rows sum to the authoritative net change. Positive preview values remain green and negative values red, including negative Fatigue values that represent recovery.
The rank meter is a five-segment display using the same yellow-green, yellow, orange, red, and violet progression as equipment repair difficulty. Daily allocations are changed in 15-minute steps with the left/right buttons or mouse wheel. Clicking a displayed allocation opens a time field. It accepts h or hh as whole hours, h:mm or hh:mm, and compact three- or four-digit times such as 830 or 0830; entered values snap to the nearest 15 minutes and may not exceed 24:00. The underlying schedule stores minutes, and the Leisure allocation shows the unallocated remainder. The editor updates these values immediately, serializes background saves, and reconciles with the server after the latest change is saved so live updates cannot momentarily restore an older plan. A failed save leaves the optimistic plan visible and presents a Retry action; making another edit also retries using the newest plan. Compact column icons label Currency (💎), Virtue (⚖️), Morale (🙂), Fatigue (💤), and daily allocation (⌛); each icon exposes the same label to assistive technology.
Character summaries use that same five-color rank progression on compact, keyboard-focusable icons. Equipped hands contribute one icon for every unique weapon leaf they exercise, including every leaf of a hybrid weapon. Armor contributes one silhouette icon for the highest equipped coverage tier, including an outlined unarmored silhouette at zero coverage, and uses the stronger healthy Dodge or Block rank for its color. The quarter, half, three-quarter, and full silhouettes progressively fill the body regions protected by that armor. These combat and armor icons always precede non-combat skills.
An exact non-combat skill appears in the summary at healthy rank 3 or higher. Expandable families remain one icon: Social and context-free Terrain use their means, Oral and Written languages use their strongest effective language and that language's displayed identity, Religion uses the same contextual primary tradition (or strongest-effective fallback) as its rail, and Bestiary uses its aggregate effective coverage. Family tooltips list only qualifying leaves and their exact ranks. Standalone skills use their own icon and rank. The visible color is supplementary: every icon exposes its identity and score through the shared instant tooltip and accessible name.
The main difference between this and directly allocating skill points is that if your character is convalescing or traveling they cannot train. Not all skills are equal though in terms of how much training time they need to be effective, they all have their own falloff curve. The number in parentheses next to a listed skill is its asymptotic training calibration; half that many effective hours produces rank 2.5. The rate of increase from training is lower the higher they get, providing an upper asymptote for skill rank.
Real training time is converted to effective learned hours by the governing aptitude:
training_multiplier = max(0, 1 + 0.5 * (aptitude - 2.5))
Thus aptitude 0/1/2.5/4/5 learns at 0×/0.25×/1×/1.75×/2.25×. An activity first conserves and divides its real-hour budget, then applies each target skill's multiplier. Healthy conditioned aptitude, before injury, determines both this multiplier and the maximum effective rank. Stored hours above a lowered cap remain latent and become effective again if aptitude returns. Effective gain that crosses or exceeds the cap is rejected exactly at the boundary and feeds one shared, saturating Mastery enjoyment morale source. Forty excess effective hours reaches about 63% of its four-point limit. All rejected gains in one logical interval are combined before saturation; the existing enjoyment first decays linearly through the interval, then the combined award refreshes it at the endpoint. It reaches zero after seven days without another award. Aptitude zero earns neither effective hours nor mastery morale.
The skill rail has three computed combat groups: Melee, Ranged, and Defense. They have no stored hours and are never used directly for a tactical check. Melee expands to Polearm, Axe, Bludgeon, Sword, and Knife; Ranged expands to Bow, Crossbow, Firearm, and Throw; Defense expands to Dodge, Block, Balance, and Will. Equipped weapon distributions determine the relevant weapon leaves. A shield gives Block full relevance; without one, the best-balanced equipped melee weapon gives Block a weight of 1 - balance. Combat Training and Raiding divide their conserved activity award deterministically across those relevance weights.
Every weapon stores a nine-field skill distribution. A halberd uses Polearm, Axe, and Bludgeon equally; a glaive uses Polearm and Sword; short swords and daggers use Sword and Knife; a hand axe uses Axe and Knife. An attack averages the complete leaf-skill checks using those weights, including each check's attributes and penalties. Knife means short weapons rather than only literal knives.
Intuitive vs Trained
This distinction applies only to correlated training. An intuitive target may benefit from correlated hours without formal training in that target. A trained target evaluates to zero until it has target-specific direct hours, regardless of correlated knowledge. Correlation is derived in one pass, never stored, and never produces mastery morale. Physiology, Anatomy, Religion and Bestiary leaves, and Written languages are trained; Oral languages are intuitive.
Ordinary skill transfer uses a deliberately sparse symmetric matrix: Cooking and Knife transfer at 0.15; Sword and Knife, Dodge and Balance, and every pair of Terrain leaves transfer at 0.20. Only direct hours enter this one pass. A trained target receives at most as many transferred hours as it has direct hours: zero remains zero and an introductory lesson cannot unlock a lifetime of related experience all at once. Intuitive Terrain leaves do not use this direct-study cap. Skill rails show direct, correlated, and resulting effective hours separately in their tooltips. The meter's background extent is the uncapped rank projected from effective hours, including correlation; it is not a separate direct-hours layer. The brighter foreground remains the aptitude- and injury-limited effective check.
Formula
# TODO: pain_penalty, morale_penalty
struct LimbWeights {
left_arm: f32,
right_arm: f32,
left_leg: f32,
right_leg: f32
}
impl LimbWeights {
fn with_side(self, side) {
match side {
Side::Left => self,
Side::Right => Self {
left_arm: self.right_arm,
right_arm: self.left_arm,
left_leg: self.left_leg,
right_leg: self.right_leg
}
}
}
}
const CALORIES_PER_ENDURANCE = 1000
const FATIGUE_EXPONENT = 5
fn fatigue_penalty(player):
fatigue = player.calories_used_today / (player.endurance * CALORIES_PER_ENDURANCE)
1 - fatigue^FATIGUE_EXPONENT
const MAX_CHECK = 5
fn skill_check(character, skill, limb_weights: LimbWeights):
hours = character.hours_trained(skill)
mut check = min(
MAX_CHECK * (hours / (hours + skill.half())),
character.healthy_governing_aptitude(skill),
)
check *= character.injury_usability(skill, limb_weights)
if skill.type() == physical:
# armor penalty ranges from 0-0.4, with full-plate being 0.4
if skill.is_upper_body():
check *= 1 - player.upper_body_armor_penalty()
else:
check *= 1 - player.lower_body_armor_penalty()
check *= player.encumbrance_penalty()
return check
Each skill is represented in the stats window with its uncapped rank projected from effective hours (direct plus correlated) behind its current aptitude- and injury-limited rank. Hover text reports the direct hours, correlated contribution, resulting effective hours, and governing aptitude. Penalties such as encumbrance, armor, or injuries reduce only the current effective portion.
Mental
Will (intuitive, 5000 hours)
Ability to resist pain or avoid morale penalties. 0. Generalized anxiety disorder / panic disorder
- Coward
- Cautious, sensitive to pain
- Professional soldier
- Brave hero
- Zen monk
Social skills (intuitive)
There's no persuasion system or anything for the MVP, this is primarily a morale and relationship system. All current Social leaves are governed by Instinct.
Insight reads others and oneself, Charm powers both humor and compatible flirtation, Command rallies and coordinates, and Deception sustains false impressions. Joke and Flirt remain separate morale actions: Grave actors cannot Joke and Proper actors cannot Flirt, while each of those reserved traits adds 0.35 to Rally Command. Party Command is led by the strongest individual check. Additional members receive a saturating coordination benefit, then contribute half of their deviation from a 2.5 baseline. Checks above 2.5 help and checks below 2.5 burden the party's social leadership. The result is capped from 0 to 5; adding arbitrarily many low-Command members cannot manufacture a high result. Character sheets summarize these four skills with an expandable Social meta-skill whose rank is their average.
- Autistic
- Cold and aloof
- Boring
- Friendly
- Funny
- Professional bard
Physiology (trained, 10000 hours)
Physiology governs what a particular character can discern about illness. Observation is passive while characters share a party and location; there is no separate examination action. The observer's individual capability band sets notebook cadence, symptom recognition, localization, confidence, and awareness of interventions. When capability crosses a band boundary, a new presence span begins so earlier notes retain the capability that produced them. Cuts and blunt trauma remain visible without Physiology; private meter causes are disclosed only through the deliberately many-to-one four-humour projection. The chart never names a disease or recommends a treatment.
Characters may explicitly administer a preparation they possess, choosing its amount and optional canonical body region through the preparation's supported route, or stop an active administration. These actions operate on generic, versioned physiology profiles rather than disease-keyed cures.
Physiology uses a bounded party-check equation. Individual checks are sorted strongest-first, the leader receives full weight, and successive contributors receive weights of 1/2, 1/4, 1/8, and so on:
[ P = 5\left(1-\prod_{i=1}^{n}\left(1-\frac{x_i}{5}\right)^{(1/2)^{i-1}}\right) ]
A solo character retains their exact individual check. The result never exceeds 5 and needs no final clamp. Because all supporting weights together equal the leader's weight, arbitrarily many equally skilled supporters can add at most the influence of one additional copy of the leader: Physiology 1 approaches 1.8, Physiology 2 approaches 3.2, Physiology 3 approaches 4.2, and Physiology 4 approaches 4.8.
- Provides no help to anyone injured
- Knows to disinfect wounds with alcohol
- Can treat common diseases (flu/cold)
- Can treat some organ damage and uncommon diseases
- Can treat most organ damage and rare diseases
- Can treat all organ damage and all diseases
Religion (trained, 5000 hours per tradition)
Religion represents knowledge, not conviction. It includes Roman Catholicism, Lutheranism, Reformed Christianity, Anglicanism, Eastern Orthodoxy, Islam, and Judaism. Canonical state records only hours learned in each tradition. Effective hours for a tradition are derived once by multiplying those hours by the symmetric correlation matrix; derived hours are never stored or recursively correlated. Prayer activity teaches the character's professed tradition. The skill rail can expand a primary tradition to show other traditions with nonzero direct knowledge, and each meter's hover text reports effective and directly learned hours.
The diagonal is 1.0. The upper-triangle correlations in stable order (Roman Catholic, Lutheran, Reformed, Anglican, Eastern Orthodox, Islam, Judaism) are: RC to the remaining traditions 0.80, 0.75, 0.80, 0.65, 0.10, 0.10; Lutheran 0.90, 0.85, 0.50, 0.10, 0.10; Reformed 0.85, 0.45, 0.10, 0.10; Anglican 0.55, 0.10, 0.10; Eastern Orthodox 0.15, 0.10; and Islam to Judaism 0.35.
A party's check for a particular religion includes every living member's effective knowledge of that tradition, regardless of what they personally profess. This permits a knowledgeable nonbeliever or member of another religion to lead prayers and sermons. The generic recruitment summary uses the character's maximum effective Religion check as a UI-only measure of coverage; authoritative morale and prayer always select the relevant tradition.
Conviction lives on the personality axis instead: Zealous contributes 5.0 pressure, Neutral 2.5, and Irreverent 0.0. A profession and conviction are separate; an Irreverent character may still officially profess a religion.
Bestiary (trained, 5000 hours per category)
Bestiary is a meta-skill, following the Religion model, whose leaf skills represent learned physical knowledge of Beast, Undead, Human, Werekin, Elf, Dwarf, Fey, Spirit, Greenskin, Insectoid, Draconid, Construct, and Wildmen creatures. A creature has one main type and may have several secondary types. Skeletons and ghouls are primarily Undead with Human as a secondary anatomical type; werewolves are primarily Werekin with Human and Beast secondary types. One evidence result always evaluates exactly one category. Transformed animal tracks can support Werekin without identifying whether the host is Human, Elf, or Dwarf.
Direct category hours are canonical. Effective hours use one symmetric, nonrecursive correlation pass after the target category has received direct study. Wildmen knowledge correlates strongly with Human knowledge and more modestly with Fey knowledge. The expandable skill rail shows effective and directly studied hours for every category with transferred knowledge. The parent Bestiary value is the mean effective coverage across every category, not the character's single best category. A question-mark cursor marks category icons as inspectable. Their hover/focus tooltips separate creatures for which the category is the main type from creatures for which it is a secondary type, and clicking pins the tooltip. Hovering or focusing an enemy type then shows only strengths and weaknesses derived from mechanics currently consumed by combat. Each strength appears on its own green line and each weakness on its own red line; no category-wide generalizations or unimplemented folklore are shown.
Against a creature, the attacker averages the Bestiary checks for every category on that creature. Excess-accuracy damage is capped at 2× plus that average check (up to 7×); even an untrained character retains the ordinary 2× head-and-throat cap.
Physical evidence first requires its ordinary inspection check. The inspecting character then makes hidden category-specific Bestiary checks for relevant authored implications. One canonical record keeps the original physical observation. Returning after later study may add newly recognized categories, but never removes or duplicates an existing result. Results also remain in the investigation journal after leaving the evidence site. No scheduled Bestiary training activity is currently implemented.
Physical
Polearm, Axe, Bludgeon, Sword, and Knife (intuitive, 8000 hours)
These are the five melee weapon leaves. Their fixed training aptitude is the average healthy Agility of both arms. Hybrid weapons use a weighted average of all tagged leaves. Knife covers short weapons, including daggers, short swords, hand axes, and compact butchery tools.
- Has never been shown how to use a weapon or observed for an extended period of time
- Can split firewood with an axe, zombies
- Peasant levy, orcs, goblins
- Professional soldier
- Knight
- Elven warrior
Bow, Crossbow, Firearm, and Throw (intuitive, 15000 hours)
These are the four ranged weapon leaves. Their fixed training aptitude is the average healthy Agility of both arms. Input precision remains a future client-to-combat signal, while weapon accuracy remains an equipment statistic; neither is a character attribute. Hybrid or throwable weapons may use more than one leaf.
- Never practiced even throwing a baseball
- Orcs, untrained peasants
- Goblins, militia
- Professional soldiers
- Huntsmen
- Elf rangers
Dodge (intuitive, 20000 hours)
- Zombies
- Drunk or very old people, orcs
- Peasant levy, goblins
- Professional soldier
- Knight
- Elven warrior
Block (intuitive, 12000 hours)
The larger your shield is, the less you rely on your block skill to use it effectively. A pavise requires almost none (though considerable strength), a buckler or weapon require high skill to use effectively.
- Never been in a fight
- Has been in some barfights
- Fresh recruit
- Professional soldier
- Knights, heroes
- Elven swordmasters
Stealth (intuitive, 8000 hours)
Stealth uses the average healthy Agility of all four limbs for training speed and mastery. Injury still penalizes current performance.
- Has never even attempted to steal cookies from the cookie jar
- Most people
- Can tiptoe around the house in socks without waking anyone, usually
- Novice hunter, professional mercenary trained in ambush tactics
- Practiced thief, veteran hunter
- Master thief, elven hunter
Balance (intuitive, 30000 hours)
Relevant for poise in melee. Strategic terrain speed is handled by the separate Terrain meta-skill and does not stack with Balance.
- Cannot walk upright
- Bit of a klutz, orcs
- Can walk in high-heels, can dance in normal shoes, professional soldier
- Can run and dance in high heels, amateur gymnast
- Skilled gymnast or martial artist, can walk a tightrope
- Graceful elf
Anatomy (mental, trained, 10000 hours)
Anatomy represents trained knowledge of bodies and wounds. Intelligence governs its training speed and mastery cap, while head injury remains a performance penalty. Herbalist apprenticeship and practice divide their training between Physiology (one half) and Anatomy, Knife, and Tailoring (one sixth each).
Surgery is a procedure, not a skill. Projectile extraction averages the treating character's Anatomy and Knife checks; stitching averages Anatomy and Tailoring. Bandaging and splinting use Anatomy alone. Self-treatment applies the shared 2.5-point penalty after the applicable skills are combined.
The recruitment rail's general Surgery coverage is the lower of the extraction and stitching checks. A specialist therefore cannot appear able to perform both procedures merely because their unrelated third skill is high.
Terrain (computed meta-skill; intuitive subskills, 30000 hours each)
Terrain stores no hours of its own. Its expandable subskills are Plains, Forest, Hills, Wetlands, and Urban. Each is an intuitive, Intelligence-governed mental skill. A route cell supplies a normalized mixture, and the displayed route/local Terrain value is the weighted combination of those subskills; without context the character rail shows their unweighted mean. Urban is stored and displayed now but has zero routing weight until the world pipeline has an authoritative urban-coverage source. Roads exercise the underlying terrain rather than Urban and reduce training in proportion to the time they save.
Tailoring (trained, 10000 hours)
Tailoring makes and repairs cloth goods. Settlement tailors and field maintenance use it for clothing durability, and stitching combines it with Anatomy.
Smithing (trained, 10000 hours)
Smithing makes and repairs weapons, armor, and shields. It does not repair clothing.
Languages
Oral and Written are expandable skill families rather than generic leaf Skill values. Oral includes East-central, West-central, Low, Yiddish, Latin, Romani, Elven, and Dwarfish; Written includes German chancery, Low, Latin, Hebrew, Yiddish, Elven, and Dwarfish. Direct hours are authoritative. Effective hours are a one-pass symmetric correlation, following the Religion model. A pair uses the language with the highest shared coefficient, with stable enum order breaking ties.
struct StrataMap {
map: [[SmallVec<Layer, 4>; 32]; 32]
materials: SmallVec<Material, 4>
}
struct Layer { material_index: u4, depth: UnitScalar #0.0..1.0 }
enum Material {
Skin { melanin: f32, carotin: f32, ???},
Fat,
Muscle,
Bone,
Steel,
Paint { rgba: Rgba },
Cloth,
...
}
impl Material {
pub fn albedo(&self) -> f32,
pub fn roughness(&self) -> f32,
pub fn restitution(&self) -> f32,
pub fn density(&self) -> f32,
# ... other functions relevant for either graphics or physics
}
This tells you everything that you need to know about an entity's physical properties, given a shape and the UV map for its StrataMap. Its not quite the same as a regular UV map though, because it has to be able to also convert a "depth" to a point in 3D. The total depth of all layers is essentially a heightmap for the surface of a mesh, and generally only this and the outermost layer are relevant to the model generator. All of the layers are needed to compute physics properties, most of which are used at the client-layer for secondary physics, but the sum total of all mass from the StrataMap for all entities associated with a character can be relevant at the tactical-layer for combat equations or the strategic layer for calculating fatigue from traveling with encumbrance.
For the MVP, the function which generates the StrataMap for a character and their equipment is likely the responsibility of the programmer implementing the model layer since the information is most directly relevant to them. For the MVP, this all sounds fantastically complicated, so StrataMap can have a quick n' dirty temporary version that is just a wrapper around a single Material, forget the 2D map and layering, and combine a bunch of materials like Skin/Muscle/Fat/Bone into "Flesh" or different rock types into "Rock". The characters would basically look like 3D stick figures and the only weapons will be clubs and maces, but its fine for now.
The eventual purpose of StrataMap being so fantastically detailed is not only to be able to generate all of our models from it and have very accurate physics calculations, but it can also represent things like how the clothes and armor on a character (added on as layers) affect damage. An outer layer of steel followed by some padding would distribute force very effectively. Or a gap in the steel layer (for the slit of a visor) translates to a weak point that a character could attack with sufficiently high accuracy, with the animation system using inverse kinematics to place their weapon in the exact weakpoint. This enables the simulation at the tactical level to become extraordinarily detailed in the future after the bare minimum is implemented for the MVP.
In the MVP, there are only two shapes which a StrataMap is expected to support: the pseudo-capsule and a flat terrain mesh. A shape which can support StrataMap is one which unambiguously maps a U, V, and depth value to an X, Y, and Z in an entity's local Transform space. There can be no UVDs which map to multiple XYZs or vice-versa.
Adler: Bruno, what do you call this? Some geometric term that includes the word "transformation" I assume, like "functional transformation".
The following shapes should be able to facilitate this:
- Cylinder (U: longitude, V: length, D: radius)
- Capsule (U: longitude, V: length + top and bottom latitude, D: radius)
- Sphere (U: longitude, V: latitude, D: radius)
- Box (one face is chosen as the surface)
- Blade
- May need two separate shapes for single-edge and double-edge
- There are a couple ways to map it, but it needs to support both pointed and flat heads
Inventory
Inventory should preserve the physical and economic consequences of equipment without turning every expedition into manual spreadsheet work.
Personal inventory represents the contents of a character's pack. Party inventory is a shared chest with explicit value stakes; it is not equipment silently distributed among members. Hands, pockets, worn equipment, and other immediate tactical placement are separate from the backpack abstraction.
Item identity
Durable equipment is never stacked. Every weapon, shield, and armor piece is a distinct object whose condition follows it through use, repair, trade, loot, and personal or party custody.
Ordinary fungible goods may stack. Food, alcohol, and soft soap are quantity-one measured rows so partial use can leave a meaningful remainder. Food lots additionally preserve their own preparation, age, nutrition, value, provenance, and hidden contamination.
See Measured inventory for the durable amount model and Food and cooking for food lot and cooking behavior.
Equipment sets
Players should be able to define expected equipment sets containing weapons, armor, ammunition, medicine, tools, and travel supplies.
At a merchant, a resupply action can purchase missing items for the selected set. Surplus or inactive-set items can be moved to storage. The interface should preview what an automated action will buy, move, keep, loot, or sell before it commits anything.
Loot and party stakes
Battle loot first enters the shared party inventory. Each participant receives an equal stake by value, with an indivisible remainder retained by the party. A character may:
- deposit an item and increase their stake by its value;
- withdraw an item against their stake;
- cover an indivisible difference with personal coin;
- liquidate party-owned goods at a settlement without changing existing ownership shares.
Autoloot should respect a configured weight limit and prefer useful value-to-weight choices. Quest evidence, required trophies, configured equipment, and explicitly protected items should not be sold automatically.
Currency
Currency appears as one collapsed Coin row. Expanding it reveals the historical denominations, but search, sorting, transfers, and bulk operations treat currency as one category.
Coin is not ordinary merchandise and is excluded from automatic sale and liquidation.
Provisions
Travel planning should calculate expected food and water needs and let the party purchase provisions with a safety margin.
Food and water are consumed from shared supplies before personal reserves. Settlement departure fills owned water capacity. Ordinary settlement water is consumed automatically during settlement rest. Rest at an inn additionally includes food; field, private, and camp rest consume the party's own supplies, while temple rest still consumes the party's food.
Alcohol remains separate from ordinary water. Weak drinks may contribute some hydration, while strong alcohol is more valuable for disinfection. Desired quantity targets protect a reserve during settlement downtime but do not make carried supplies unavailable on the road.
Measured consumables
Partially consuming food, drink, or soap leaves the remaining fraction in the same row. Its displayed mass and value fall with the remaining amount.
Current interactions consume validated portions but do not yet support arbitrary pouring, mixed containers, or partial-row merchant trade. Those are container-model work rather than reasons to waste the remainder of a unit.
Soft soap supplies 25 cleansing points per full unit. Washing uses only the needed fraction. Shared soap is allocated deterministically when several characters need it, with disease and blood exposure taking priority.
Inventory browser
Two-sided inventory views use the same controls for trade, party transfers, loot, and cooking:
- independent search on each side;
- sortable columns and optional detail columns;
- expandable item rows;
- staged transfers before confirmation;
- quantity targets for bulk actions;
- keyboard-accessible item actions;
- URL-backed search, sorting, and column preferences.
Weaponsmith and armorer views can expose relevant combat statistics, while merchants whose goods do not use those fields retain a simpler table.
Inventory rows use stable monochrome item icons. Unknown or modded items receive a visible fallback rather than a broken asset.
Design principle
The underlying model may remain detailed, but repeated chores should be previewable and automatable. A player who enjoys optimization can inspect every item; another player should be able to define policy once and trust the interface to carry it out visibly.
Map
Weather overlays
The imported Plains, Forest, Hills, Wetlands, and Urban mixture remains an immutable normalized five-member description of the land. Strategic weather does not rewrite that source data. Rain derives a temporary effective mixture at journey departure: cells below 100/1000 Wetlands absorb rain without becoming wetlands, while susceptible cells gain Wetlands and deterministically renormalize the displaced underlying weights. A separate bounded saturation value slows travel through mud and waterlogging, including terrain already at 100% Wetlands.
Snow cover is a separate overlay, never a sixth biome weight. It blends Snow expertise with the ordinary terrain check. Cover splits the road-discounted training budget between Snow and the underlying biome rather than duplicating exposure.
The terrain profile currently affects strategic routing and can select possible scene context. Tactical resolution of hazardous terrain is intended but not yet implemented; #212 tracks the handoff and committed-result contract. The world map is a grid where each square has a height and an enum for the terrain type. Each of these affect both the speed of travel and the difficulty of climb/swim check to avoid injury. We should not try and create our own, we should be able to find both height and biome data from some open GIS dataset. At minimum, it should be easy to find modern data for these, but there may also be a historical dataset that we can use.
The strategic import records a bounded terrain profile on each road/ferry edge: elevation, ascent/descent, grade, slope/aspect, roughness, relief, landforms, water adjacency, and versioned seasonal/encounter tags. Because the historical road source has endpoint topology but no polyline, these facts explicitly use straight endpoint geometry. They select strategic travel and possible scenes; a tactical server still owns all live terrain interaction.
Height
Traveling from a lower height to a higher one may require the characters to make a climb check (based on upper body strength vs weight) based on the slope, and traveling from higher to lower may require the characters to make an agility check.
Terrain Type
As this is an enum, we can store extra information in each variant. For example, the "River" variant could include its depth and velocity, both of which would contribute to how hazardous it is to ford.
In addition to the costs imposed to traveling, it may also affect stealth and detection. Forest cover may largely prevent detection from flying enemies but also make it much easier for an enemy to ambush you, presenting a trade-off of risk which you might assess based on which enemies are known to exist in an area and which you are better able to defend against.
Pathfinding
As described in the travel page.
Weather
This probably isn't worth putting in the MVP, especially for such a temperate place like Italy, but eventually there should be a weather map that affects the way that you travel though terrain. Heavy snow would slow down land travel, frozen lakes and rivers become possible to cross without swimming (but also risky if the ice is thin), and rain makes climbing very difficult.
Points of Interest
For the MVP, there is no need for any point of interest other than enemy camps/lairs/nests relevant to active quests in an area as well as settlements. The former are simply placed randomly (though not too close to any other point of interest), the latter should be obtained from a GIS dataset. If we can't find historical GIS data on Italian settlements then we can just use modern data and rely on Cunningham's Law to fix it.
Underground
In our setting, there is ostensibly a vast underground network of caves, crypts, tunnels, Ratling under-cities, Dwarven strongholds, and even antediluvian ruins. But this sounds hard, therefore we shouldn't bother with it for the MVP. All of the quests will conveniently take you to overland locations, which don't even need to have structures.
Halbe: You are essentially being hired by local municipalities to clear out homeless encampments. If only we had this in the IRL modern setting...
Foraging
Personal foraging trains only the Plains, Forest, and Hills leaf skills in the normalized mixture of the character's current 1 km vicinity. The Terrain heading remains a presentation aggregate and is never awarded or stored. Cultivated ground affects legality, not the biome mixture. High Game, Low Game, Fish, Harmful Beasts, and Plants divide one selected search-time budget; source availability follows the local habitat rather than selecting another vicinity.
Bestiary authority
Canonical authoring records live in content/quests/bestiary.yaml. They are
sorted, validated, embedded and content-hashed at build time; see
Quest generation and investigation.
Combat weaknesses use the ordinary
physical resistance/padding model. Skeleton bone, for example, has edge
resistance and no innate padding rather than a flat cut/blunt multiplier.
Generated physical-evidence topics may carry an optional diagnostic clue kind. It is learned only when both the physical inspection and the relevant Bestiary check succeed. Foot morphology, grave residue, and unexplained missing blood currently feed the same forward candidate-ranking vocabulary used by witness descriptions. Candidate output is a deduction, never canonical identity: observer presentation uses qualitative strong, plausible, or weak support bands rather than exposing raw likelihood products or authored priors.
adventuresim-core::bestiary is the shared authority for strategic threat
identity. Persisted Quest.enemy_type and StrategicEncounter.archetype
strings are bounded, open ThreatId values such as skeleton and
grave_robber. Display names and aliases never select behavior, and IDs absent
from the startup catalog are rejected at strategic combat and loot boundaries
instead of becoming a generic enemy.
Profiles and current consumers
Every catalog entry has typed combat and investigation profiles. Combat covers reusable humanoid/quadruped rig, sustainable speed, perception-facing traits, attack/loadout, protection, innate material resistance/padding, fear/disease, temperament, encounter scaling, and loot. Investigation covers habitat/activity/victims, tracks, wounds, disturbances, sounds, silhouettes, odors, mistaken identities, distinguishing evidence, visibility, and preparation advice. Track, wound, disturbance, odor, and distinguishing-evidence IDs are bounded open catalog strings. New physical trace identities therefore do not require a Rust enum edit; code changes only when a new executable interpretation is needed.
The strategic autoresolver consumes identity, loadout, protection, speed, loot, perception/stealth, morale, encounter scaling, and innate protection. Skeleton bone contributes full-coverage resistance but no padding through the ordinary armor calculation, so cutting attacks are inefficient while blunt contact remains damaging. No species-level damage multiplier is applied; weapon force and penetration, worn armor, ranged loadouts, speed, and party size also produce mechanically testable preparation choices. Fire, silver, daylight, and ritual courage are stored separately as unimplemented investigation hypotheses. UI copy must not claim they modify autoresolve. Other fields are typed for the investigative generator and future tactical combat; they are not all simulated yet. Tactical servers do not yet receive bestiary identity, and tactical enemy instances, position, HP, and damage remain transient.
Innate protection and Protection::Armored cannot currently coexist in one
catalog profile because autoresolve has not modeled overlapping anatomical and
worn layers. Catalog validation rejects that combination rather than silently
overwriting either layer.
Creature categories and knowledge
Every threat profile carries one or more typed physical-knowledge facets: Beast, Undead, Human, Werekin, Elf, Dwarf, Fey, Spirit, Greenskin, Insectoid, Draconid, Construct, and Wildmen. One facet is authored as the creature's primary type; any others are secondary physical traits. These remain overlapping rather than exclusive: a werewolf is primarily Werekin with Human and Beast traits, while a spectral hound is primarily Spirit with a secondary Beast trait. Skeletons and ghouls are primarily Undead and only secondarily Human. Wildmen are their own primary category.
BestiaryHours stores only direct study by category. Effective knowledge is a
single, nonrecursive pass through a symmetric diagnostic-correlation matrix,
then capped at the Bestiary skill's 5,000-hour mastery calibration so several
related fields cannot add beyond the skill's authored mastery range. Correlation means transferable identification
knowledge, not merely that two tags can coexist. Wildmen transfer strongly
with Human (0.65) and more modestly with Fey (0.30).
Surgery is expressed through separate trained skills rather than a Surgery leaf. Projectile extraction averages Anatomy with Knife, stitching averages Anatomy with Tailoring, and bandaging and splinting use Anatomy alone. Each skill retains its own governing aptitude, mastery cap, and injury penalties.
Procedural physical-evidence topics may author atomic Bestiary implications. Each implication names exactly one category, a fixed support value from 0 to 10,000 basis points, a hidden category-specific lore threshold, and safe interpretation text of at most 1,024 UTF-8 bytes. Both the dependency-light raw catalog validator and the typed runtime validator enforce that byte limit. Support is a stable property of the observed clue; it is not computed from catalog population or hidden case truth. A transformed pawprint may support Beast and Werekin but never reveals whether the host is Human, Elf, or Dwarf.
Only the inspecting character's category-specific effective knowledge is used. Successful results are persisted on one canonical inspection record. Revisits keep the physical observation stable while current knowledge may add newly successful categories; existing results are never removed or duplicated. The same safe structured results persist in the investigation journal. Category lore separates enemies for which that category is the main type from enemies for which it is a secondary type. Enemy-specific hover details include only facts derived from fields consumed by current combat, such as a skeleton's edge resistance and lack of innate padding. Unimplemented folklore such as fire, silver, daylight, and ritual courage is not presented as gameplay knowledge.
Weighted context and inference
The catalog owns sparse forward likelihoods. Ecological base rate and curation
weight are separate; habitat, activity time, visibility, distance, and witness
capability provide context. rank_candidates computes inverse conclusions from
those forward likelihoods, priors, and evidence. No inverse table is authored.
Zero means impossible and is never raised by curation. A low positive weight
means rare. The public habitat-selection API validates improbable combinations
and requires a typed CausalBridge with evidence outputs. For
example, skeletons in an occupied house require a cellar crypt, graveyard
tunnel, or resident controller. Causal bridge identities and their event,
evidence, and action outputs are authored as bounded open IDs.
The MVP northern-Germany regional prior is a small typed authoring context,
separate from curation; it is not yet derived from imported world geography.
evidence_limited_preparation accepts only visible reports and evidence, never
a hidden threat ID. Direct bounty quests currently confirm opposition and may
show canonical preparation advice. Pure deterministic validation and ranking
APIs expose ambiguity cardinality, distinguishing clues, normalized
plausibility/curation marginals, dominance, bridge coverage, reachability, and
numeric/duplicate invariants. Evidence inputs are deduplicated and bounded.
Folklore provenance and adaptation
These are game adaptations, not claims that every motif was believed across northern Germany in 1544. Names, dates, regions, and motifs changed between tellings. The small current subset also fits reusable humanoid/quadruped rigs.
- Kobold: the Grimms' collected Der Kobold.
- Werewolf: the Grimms' collected Der Wärwolf; silver is an unimplemented investigative hypothesis and not asserted by that text.
- Nachzehrer/Wiedergänger: early-modern mortuary context in this academic overview; fire is an unimplemented investigative hypothesis.
- Wild man: early-modern German visual/cultural context in this art-historical study, not one uniform folk belief.
- Spectral hound: the later regional Der schwarze Hund (1839). Its later date makes it evidence of a collected tradition, not proof of the exact motif in 1544.
- Alp: included conservatively as a nocturnal identification challenge; its mechanics are an adaptation pending dedicated region-specific sourcing.
The Grimm collection context documents nineteenth-century collection of traditions; it does not prove every adapted motif in the MVP's exact place and year.
Historical equipment catalog
This document records the first equipment slice for issue #65: weapons,
shields, and armor intended for northern Germany in approximately 1544. The
definitions are canonical typed records in content/items/catalog.yaml,
compiled and embedded by adventuresim-core as described in
Item definition authoring. The strategic game and the
autoresolver consume the same persisted Item records; there are no special
NPC-only item rules.
Scope and historical basis
The core professional infantry set is pike-and-shot equipment with halberds, sidearms, and armor. The German History in Documents and Images description of an approximately 1532--42 Landsknecht procession identifies the pike as the typical weapon. The Musée Lorrain's description of imperial Landsknecht equipment identifies pikes and halberds as the main bodies, supplemented by crossbowmen and arquebusiers, and notes Nuremberg mass production of infantry armor from the 1540s. The Met describes the halberd as especially associated with German Landsknechts, while noting the pike's role in massed sixteenth century formations.
The catalog also deliberately includes usable older stock: the baselard, barbute, sallet, visored sallet, mail garments, heater shield, and longbow. These are not presented as the fashionable or preferred 1544 battlefield kit; they are plausible inherited, stored, civilian, militia, or second-hand goods. No later sixteenth-century weapons or armor types are included simply for variety.
Inventory
| Category | Intended entries |
|---|---|
| Civilian and militia weapons | club, walking staff, hand axe, flanged mace, war hammer, utility knife, baselard, Bauernwehr, hunting spear, self bow |
| Daggers and swords | rondel dagger, misericorde, Katzbalger, arming sword, longsword, messer, Kriegsmesser, rapier, Zweihänder |
| Formation and ranged weapons | military pike, halberd, longbow, light crossbow, heavy crossbow, matchlock arquebus, hooked arquebus |
| Shields | buckler, targe, heater shield, round shield, pavise |
| Helmets | arming cap, mail coif, kettle hat, barbute, sallet, visored sallet, burgonet, close helmet |
| Arm defenses | quilted sleeve, mail sleeve, vambrace |
| Leg defenses | padded chausses, mail chausses, greave |
| Torso defenses | arming doublet, jack of plates, brigandine, mail shirt, breastplate, cuirass |
| Waist and upper-leg defenses | padded skirt, mail skirt, fauld, tassets |
The catalog intentionally has more weapons than armor entries (26 weapons to 24 armor pieces). Helmets receive eight entries because they were independently useful, highly varied, and more likely than a complete suit to persist in an armory.
Shield statistics preserve an actual handling tradeoff rather than making one catalog entry a strict upgrade. The round shield weighs 3.0 kg and provides 3.0 block, while the heater shield weighs 3.5 kg and provides 3.5 block. A player therefore chooses between lower burden and greater protection.
Representation and gameplay inference
The current item schema has one armor item per existing body slot, with no
layering field. A mail_shirt, for example, is a chest-slot alternative to a
brigandine rather than a simultaneous underlayer. This is an explicit temporary
representation constraint, not a claim that period armor was worn in one
layer. Adding layering, ammunition-specific projectile behavior, or rust
belongs to later corrosion work.
Weapons carry an explicit distribution across Polearm, Axe, Bludgeon, Sword, Knife, Bow, Crossbow, Firearm, and Throw. Hybrid weapons split equally among their applicable tags: a halberd is Polearm/Axe/Bludgeon, a glaive is Polearm/Sword, and a hand axe is Axe/Knife. Combat averages the complete leaf checks by these weights. Knife is the short-weapon category, not a literal item name test.
Condition and repair
Weapons, shields, armor, and clothing are individual inventory instances. Their condition is a continuous five-bin bar: tier one is yellow-green, tier two yellow, tier three orange, tier four red, and tier five violet. Bins one and two are field-repairable, while bins three through five require a settlement craftsperson. Repairing bin n requires Smithing skill n for weapons, shields, and armor, or Tailoring skill n for clothing; field work is capped at bin two. Equipment quality is also its required maintenance skill, so a lesser smith can improve a masterwork item without restoring it completely. Damage can never occupy a tier above the item's quality: only quality-5 equipment can acquire violet tier-5 damage.
Clothing condition and Tailor repair are authoritative for damaged clothing instances, including seeded and imported damage. Ordinary clothing wear is not yet generated because clothing has no equipped/worn slot in the current item model; carried inventory is deliberately not worn down as if it were being worn. Routine wear should begin when clothing becomes equippable and can participate in the same contact and use paths as other equipment.
Quality uses the same 1--5 scale and is shown by the item name using the corresponding condition color, adjusted toward the fixed light interface text color for readability. Quality 3 is ordinary munition-grade work, quality 4 is the sort of commission a knight might order, and quality 5 is work for royalty or an esteemed hero. Munition grade is the neutral durability baseline. Quality 1--5 multiplies physical durability by 0.65, 0.80, 1.00, 1.25, and 1.60 respectively: the multiplier raises yield and fracture stress and inversely scales ordinary wear. Outside durability and its maintenance requirements, quality does not currently change combat statistics, coverage, handling, price, or any other item property.
The local catalog assigns several starter and demo items across all five qualities so the Wounded Demo fixture exercises each color and repair ceiling.
Equippable personal-inventory rows expose a checkbox backed by the item's catalog slot. Checking it equips that exact inventory instance, displacing any item already occupying the selected slot; unchecking it unequips the instance. Non-equipment rows keep a disabled checkbox.
Smithing uses the shared trained-skill curve: 5,000 invested hours is rank 2.5. Database upgrades split any legacy durable stack into quantity-one instances while retaining the original row ID for one piece, preserving equipped references; pooled party equipment is migrated the same way.
Rest resolves health first, then automatic field maintenance, then scheduled downtime. Field
maintenance uses Tailoring for clothing and Smithing for other durable equipment. Settlement rest
recommendations also include unfinished local repair orders. Services have independently seeded
Weaponsmith, Armourer, and Tailor ratings of 3--5. Repair orders escrow the exact item instance, have an ETA,
retain damage beyond the smith's skill, and never expire. A job's stable quote is
ceil(base_value * repairable_damage), with a minimum of one gold; only bins within that smith's
skill contribute. The quote is charged atomically from personal gold when the repaired item is
retrieved. Bulk collection is deterministic: orders are considered by submission time and ID, and
the affordable prefix is retrieved without skipping an earlier unaffordable job.
Impact damage uses each item's explicit yield, fracture, wear, and failure-share values. Ductile armor yields and dents readily but resists catastrophic fracture; stiff weapons resist ordinary wear but fail more sharply under a sufficiently large impact. Failure share models construction: one failed plate in segmented armor contributes less total damage than failure of a monolithic breastplate. Wear continuously reduces weapon precision and armor/shield handling, without reducing coverage for the sake of a single local hole.
Weights are kilograms. The documented object weights below anchor the scale;
the other weights are bounded gameplay estimates for ordinary serviceable
examples, rather than claimed measurements of a particular surviving object.
base_value is a relative gameplay value that represents material and skilled
labor. It is not a historical price series.
Combat-facing fields retain the meanings in Combat:
accuracyis the weapon precision multiplier; values use the documented 0.5 club/hammer, 1.0 axe, 1.5 sword/spear, and 2.0 purpose-built precision calibration.penetrationis its armor-resistance coefficient; blunt weapons use 0.1--0.5, ordinary edged weapons 1.0, spear and broadhead-like points 2.0, and narrow armor-seeking points 4.0.reachis metres. The present schema uses it for melee reach on melee items and autoresolve range on ranged items.resistanceandpaddingare joules;coverage,flexibility, andrange_of_motionare 0--1. Plate favors resistance and coverage, mail loses resistance under penetrating attacks through its high flexibility, and padded clothing favors padding and mobility.
These are deterministic gameplay inferences from the documented combat model
and physical construction, not claims that period sources supplied those exact
numbers. The catalog test rejects duplicate IDs, placeholder bot_ entries,
impossible slots, absent damage types, out-of-range armor values, and a
weapons-to-armor ratio contrary to the intended inventory.
Sources
- German History in Documents and Images: marching Landsknechts, c. 1532--42
- Musée Lorrain: equipment of two imperial Landsknechts
- The Met: German halberd
- The Met: German breastplate, dated 1540, with documented 3.515 kg weight
- The Met: German Augsburg burgonet, c. 1525--30, with documented 2.332 kg weight
- The Met: Augsburg breastplate with tassets, c. 1530
- The Met: German helmet, c. 1535
- Wallace Collection: German sallet, c. 1515, explicitly a declining form
Item definition authoring
When browser-local developer mode is enabled, expanding a concrete inventory row shows an Edit YAML button that opens the definition at its compiled file and line in GitHub. Source locations are generated during catalog compilation; authors should not maintain line numbers or source URLs.
Item metadata is authored in content/items/*.yaml. These files use YAML's
strict JSON-compatible subset: quote every mapping key and string, use JSON
arrays/objects, and do not use aliases, tags, implicit scalars, comments, or
duplicate keys. Every document declares "schema_version": 1.
Identity and runtime boundary
id is the stable, persisted identity. It is 1--64 lowercase ASCII letters,
digits, or underscores and must never be changed merely to rename an item.
Changing or removing an ID requires recreating/reseeding the disposable
development database. display_name and presentation.icon are presentation
metadata and may change without changing identity.
The adventuresim-core build script reads item files in normalized, sorted
path order, validates them, sorts definitions by stable ID, computes a SHA-256
revision over normalized paths and source bytes, and embeds the compiled JSON.
Production never reads loose YAML. Strategic startup projects typed definitions
into the existing flattened SpacetimeDB Item table; that table is a
persistence/client ABI, not an authoring schema. Mutable quantity, owner,
custody, condition, and market state do not belong in definitions.
Shape
Every item requires id, display_name, weight_kg, base_value, tags,
presentation.icon, and a tagged kind. Physical units are part of field
names. Kinds are simple, currency, ingredient, medication, clothing,
container, shield, armor, weapon, and food.
Kind payloads contain only compatible fields. Weapons require a slot, explicit
damage types, mode flags, and an explicit finite, non-negative skill
distribution summing to one. Armor and shields require their relevant
slot/stat payload. Repairable kinds require a durability capability with
quality 1--5 and explicit physical/handling inputs.
Capabilities compose independently of kind. Garlic remains an ingredient
while carrying capabilities.food; alcohol remains a simple serving while
carrying capabilities.alcohol. Food, alcohol, container, and durability are
the supported capabilities. Executable effects, physiology profile versions,
currency assignment, and other mechanics remain typed Rust and are
cross-validated against catalog membership.
There are no inferred quality, durability, damage types, weapon skills, or unit conversions. Optional capability sections are absent when inapplicable; fields within a present section are required unless documented otherwise. Recipes are outside this catalog.
Workflow
just content-check
just content-check items
The default all target runs the same build compilers used for item, quest,
organization, and dialogue content. The targeted items form runs the item
checker (the core build still validates its other compiled catalogs).
Both commands exercise the production build-time validator. Diagnostics aggregate independent semantic failures where possible and identify source file, line, column, item ID, and field path. JSON syntax and duplicate-key errors use parser coordinates. Validation rejects unsupported schemas, unknown fields, duplicate IDs, invalid stable IDs, non-finite/out-of-range values, incompatible slots/stats, malformed weapon skills, and invalid durability, quality, food, hydration, alcohol, medication, or container metadata.
Food and cooking
This page describes current production behavior. Food rows now use the initial
integer quantity-plus-measured-state rollout in
measured-inventory.md, while conserved food-lot
mass, nutrition, and value remain floating fields pending the stable
measured-object schema.
Food is authoritative strategic inventory. ItemKind::Food identifies ordinary
foods, while edible herbalist ingredients may retain Ingredient. Every
acquisition creates one independent quantity-one food_lot per purchased or
found unit; food lots never merge merely because item IDs match. The inventory
row identifies the batch, while mass, calories, value, quality, five flavor
potencies, and fractional ingredient provenance live on the lot. Partly eaten
lots retain quantity one and scale every extensive property (including flavor)
together with their fixed-point remaining amount; quality remains unchanged.
Transfers and sales therefore
move a complete remaining batch rather than manufacturing rounded sub-units.
Food item metadata, including dual-purpose ingredient capabilities, is
canonical in the embedded item definition catalog;
spoilage, cooking, and ingestion mechanics remain Rust rules.
Food definitions are validated before either personal or party inventory is
mutated, so an acquisition cannot leave an inedible inventory row without its
lot metadata. Inns sell a standard cooked meal with a fixed lot profile;
player-cooked meals reuse that item ID but retain their derived name, nutrition,
mass, value, contamination, and ingredient provenance on their own lot.
The standard meal provides 3,000 kcal, so two meals cover the ordinary
6,000-kcal daily demand.
The public lot records its inventory link, display name, preparation method,
ingredient provenance, quality, salty/spicy/sweet/sour/savory potency, mass,
useful calories, value, and creation minute. Quality uses the same name colors
as equipment and food tooltips use the plain label Quality N. Merchant
catalog quality is copied to every acquired lot. A
separate private row anchors microbial concentration and exponential growth.
Growth is evaluated lazily from strategic time and bounded; there is no spoilage
tick. Initial loads are deterministic server-random log-scale samples. Raw meat
grows fastest, cooked meat is heat-reduced and slower, and intact produce and
nuts are lower-risk. Temperature, storage, preservation, undercooking, and
burning are deferred.
Ingestion uses current concentration times consumed mass as a direct dose for
existing Dysentery (Bloody flux), whose vector is already food/water. The
exposure identity includes character, lot, and strategic minute. Immunity
applies and an unresolved Dysentery episode prevents duplicate infection.
Travel, camp rest, and non-inn settlement rest apply elapsed nutritional demand
once and then automatically consume the oldest pooled and personal food lots
toward a zero balance. Paid inn rest is full board: its elapsed calories and
ordinary drinking water are covered, any pre-existing food or water deficit is
cleared to zero, and personal and party provisions are preserved. Temple,
private, field, and camp rest provide no food or drinking water.
Cooking
The active character's Cooking skill icon is a raised menu button; flat skill icons remain informational. Activating Cooking opens a wide, responsive modal dialog over the unchanged character sheet. It uses the same two-sided inventory browser as trading and looting: the cooking pot is on the left, the character's full inventory is on the right, and transfer arrows stage bounded amounts of food between them. The amount controls use integer milliunits internally and quarter-unit steps in the current interface, including a final smaller remainder when necessary. The center shows a placeholder cooking scene, Cook and Cancel, and a horizontal icon row for pan-fry, stew, roast/skewer, and bake. Roast is always available. Pan-fry requires a pan, stew a pot plus water, and bake a portable oven. Stew draws pooled party water before carried water. Tools are retained. Inns sell the food ingredients and reusable implements needed by this interface. The preview reports estimated duration and flavor score and calls out roast calorie loss, a fatless pan, and stew disposal; the reducer remains authoritative.
Duration is method setup plus the slowest ingredient's safety/doneness time plus square-root batch scaling. The reducer preflights actor, state, selections, tools, water, and arithmetic before mutation. It advances neutral strategic time, and consumes inputs. Pan-fry, roast, and bake create a carried derived food lot; cooking no longer also eats those meals. Stew is the sole exception: soup is immediately eaten up to the one-day fullness cap because it cannot be carried, and any remainder is discarded. Consumed stew water contributes its milliliters divided by 1,000 to finished mass before flavor scoring and contamination dilution. Only the registered strategic gateway may invoke eating or cooking, and tactical actors are rejected. Cooking advances its safe time prefix before consuming supplies: a terminal interruption commits the elapsed time and terminal event, leaves ingredients and water untouched, and creates no meal. Remainders stay as independent lots, and their current lot mass and value drive encumbrance and merchant quotes. Completing a meal trains the mental, trained Cooking skill for the elapsed cooking time. A character can also apprentice as a cook through the inn's ordinary profession dialogue; apprenticeship and later independent practice follow the same progression and payment rules as the other non-religious settlement professions.
The authoritative Cooking check includes the documented one-pass Knife transfer after direct Cooking study. Each rank removes 6% of setup and batch overhead, to a maximum 30%; ingredient safety time is never shortened. Useful calorie retention rises from 95% at rank zero to 99% at rank five. Roasting then retains 85% of those calories to represent rendered fat dripping from the skewer. Baking has 30 minutes of setup, compared with 5 for pan-fry, 12 for stew, and 7 for roast.
Flavor potency is measured in mass-equivalent kilograms: one gram of salt
contributes enough salty potency for 100 grams of food. The shared objective
for an active flavor is potency equal to finished food mass. Flavors below
target score linearly (5 * ratio); excess is punished quadratically
(5 / ratio²). Each method has a fixed target mask, and an omitted required
flavor scores zero rather than disappearing from the equal-weight average.
Pan-fry and roast score salty, spicy, and savory flavors; stew also
scores sour. Baking deterministically scores the stronger of sweet and savory,
alongside any salt or spice, allowing both pies and savory bread. These shared
method rules are the future insertion point for character preference modifiers.
The continuous Cooking check first maps to a discrete chef tier:
floor(check), with checks below 1 occupying novice tier 1. Final meal quality
is the lower of that chef tier and floored aggregate flavor score, with tier 1
as the five-tier item-system floor. Pan-frying subtracts one tier, never below
1, unless actually staged ingredients tagged culinary_fat comprise at least
2% of selected ingredient mass; merely owning or adding a trace of butter or
lard does not count. Quality tiers multiply derived market
value by 0.80, 0.90, 1.00, 1.15, or 1.35. The catalog includes low-calorie
seasonings (salt, mustard, horseradish, vinegar, garlic, and sage), sweet and
sour ingredients (honey and sour cherries), naturally savory meat and
mushrooms, and calorie-dense butter and lard.
cooked_meal is a terminal preparation state and cannot be selected as an
ingredient. This prevents repeated cooking from compounding the value or
nutrition multiplier.
Foraged food
Raw wild foods gathered through current-vicinity foraging enter personal inventory through the same validated non-fungible food-lot path as other food. Watercress and seaweed extend Plants for wet-ground and coast foraging. Venison is High Game, fowl is Low Game, fish requires wet ground or coast, and minimal beast meat keeps Harmful Beasts functional. All use authoritative mass, calories, value, Raw Meat contamination, and cooking definitions. Foraging does not synthesize processed goods.
Foraging
Foraging is a personal strategic activity performed in the acting character's current authoritative vicinity. It never chooses a nearby biome or creates a travel excursion. Settlement, exact case-site, and en-route camp coordinates are supported; active movement, tactical encounters, unresolved strategic encounters, unlocated characters, and stale locations are rejected.
The browser gateway samples the immutable final terrain pack at that coordinate and attests the package digest, coordinate, location context, normalized Plains/Forest/Hills/Wetlands mixture, wet/coast access, and cultivation bit. The reducer accepts this only from the registered gateway, requires terrain routing schema 6 through gateway contract version 3, re-derives the character's location, and rejects stale digests or mismatched coordinates/context. A browser never supplies a trusted cultivated boolean.
Resolution
The dialog exposes exactly five stable source categories, in order: High Game,
Low Game, Fish, Harmful Beasts, and Plants. The year-round resource catalog is
in adventuresim_core::foraging::FORAGE_RESOURCES; every resource belongs to
one category. Seasonality is explicitly
deferred until a strategic season model exists. Processed charcoal, vinegar,
oatmeal, and rosewater and non-plant honey are not forage targets.
Searches last 1–24 whole hours. Rarity supplies a price-independent base discovery rate. The acting character's local weighted Terrain check can add at most 50% to discovery and yield. Multiple targets divide one time budget. Discovery frequency scales with the fraction of local terrain matching the resource's habitat; a trace of forest no longer grants full forest output, but a successful find retains the resource's ordinary per-discovery yield. Wet-ground and coast attestations count as complete matching microhabitats. Only the routing pack's authoritative wetland coverage contributes Wetlands skill weight. The broader river-or-wet-ground microhabitat boolean remains a resource-availability signal and is intentionally not converted into an invented wetland area fraction. Food alone receives a 1.75× subsistence calibration: an eight-hour low-skill search for the best food in an ideal habitat averages roughly the 2,000 kcal spent during that interval. Medicinal rarity is unchanged. Sources are canonicalized before resolution. One search budget is divided first among selected categories and then among the locally available resources inside each category. Adding more plant resources therefore cannot manufacture more search time. The reducer chooses and privately persists unpredictable entropy; once that seed is chosen, replay from the private authority is deterministic. A completed search may find nothing.
Food uses validated individual food-lot creation, retaining catalog mass, calories, value, and contamination. Herbalist ingredients use validated fungible stacks. Yields are never truncated for carrying capacity; ordinary encumbrance consequences apply afterward.
Actual elapsed time is clipped once at the existing injury/disease boundary. That elapsed time is conserved over concrete Plains, Forest, Hills, and Wetlands training according to the normalized local mixture. Terrain has no stored parent value, and settlement illegality does not turn training into Urban. Checks and training use the same aptitude-capped, injury-adjusted path as travel. Foraging never adds raw attributes to a check, never stores correlated hours, and routes rejected above-cap training into mastery enjoyment.
Legality
Foraging is illegal at a settlement or in a cultivated square. High Game, Low Game, Fish, and Plants also require a license granted by the character's currently presented, active, dues-current profession; Harmful Beasts requires none. The Lodge of the Hart King grants the common licenses at every rank, while High Game is reserved to Master rank. Licenses are global for now and deliberately ignore local recognition and political boundaries.
Unlicensed sources remain selectable as poaching. A completed illegal search
makes one deterministic Stealth check, even when several selected sources are
unlicensed or settlement/cultivated illegality also applies. Cultivated ground
and otherwise-legal wilderness poaching start
at DC 1.75; settlement exposure starts at DC 2.50; the worse base applies, plus
0.075 per hour after the first, capped at 4.50. Failure subtracts exactly 1.0
from CharacterVirtue; success avoids the loss. This never changes notoriety.
An interrupted search creates no partial yield. If illegal work consumed any
time, it still makes exactly one exposure check using the actual elapsed
duration and applies the same Virtue consequence on failure.
The reducer accepts stable category IDs rather than item IDs and retains those selected source IDs in one private replay authority row per character. The gateway-only projection omits seed, coordinates, context, DC, roll, and direct Virtue state, and exposes only the exact opaque request's player-safe result.
Measured inventory architecture
Status: accepted architecture for issue #150 with an initial production rollout for food, alcohol, and soft soap.
This document defines how fractional consumables will coexist with ordinary inventory stacks. Its authority is the strategic SpacetimeDB layer. Tactical code may receive a snapshot or request a strategic result, but must not create a second durable inventory authority.
Decision
quantity and measured amount describe different dimensions and both exist:
quantityis a count of independently ownable objects or unopened units.amountis the remaining magnitude of divisible material in one object or lot, stored as an unsigned integer in a definition-selected fixed unit.
An item definition opts into measurement through a profile/capability. The
inventory row continues to carry quantity; an optional, separate measured
state row carries remaining_amount. A row with measured state is always
quantity one. A definition without a measurement profile can never have
measured state.
Capabilities compose. Measurement, armor, weapon, food, alcohol, durability,
and similar profiles are authored as typed capability payloads in the embedded
item definition catalog, keyed by stable item_id,
not variants in one giant exclusive ItemKind union. A bottled medicine may
be both alcohol and food; clothing may also be armor. An exhaustive union would
make those legitimate combinations awkward or impossible.
The framework-independent arithmetic boundary is
adventuresim_core::inventory_measurement. It validates profiles and evaluates
full stacks and partial or empty singletons. The first rollout also publishes
inventory_item_amount and party_item_amount: each stores
remaining_milliunits on a quantity-one row, where 1_000_000 is one full
definition unit.
Alternatives considered
Discrete uses
One stack unit represents one serving, wash, charge, or meal.
- Advantages: works with the current schema and integer trade; stacking, transfers, and targets are simple.
- Costs: opening a bottle cannot preserve what remains; using soap wastes a hidden remainder; meals and drinks have arbitrary serving boundaries.
This remains appropriate for genuinely discrete things such as arrows. It is not the general model for divisible consumables.
Fixed-point bulk
Every row stores an integer magnitude such as millilitres or milligrams, with no physical container identity.
- Advantages: simple consumption, aggregation, and exact conservation; useful for fungible bulk stores.
- Costs: cannot account for tare mass/value, bottles, opened versus sealed goods, or reusable vessels without another model.
Bulk lots use the selected measured-state architecture with zero tare and a quantity-one lot. They are not a replacement for containers.
Per-container contents
Each opened container is a quantity-one row whose measured state records its remaining contents.
- Advantages: preserves bottle identity, tare mass/value, empty containers, and exact transfer/loot ownership.
- Costs: opened containers no longer stack automatically; pouring and mixing require explicit rules.
This is the selected representation for containerized goods. Unopened identical containers still stack and split only when one is opened.
Rejected: count-or-amount
A sum such as Count(u32) | Amount(u64) makes the two values mutually
exclusive. A half-full bottle is simultaneously one bottle and some amount of
liquid, so the model loses information or needs container-specific exceptions.
Payload sum types also make relational filtering and arithmetic less direct
than a small profile table plus state table.
Rejected: one unified numeric field
Treating everything as amount permits nonsensical durable states such as
0.5 swords. Enforcing integrality from the item definition recreates the
selected capability model with a weaker schema. Floating point is additionally
unsuitable for conserved durable inventory, nutrition, mass, and value because
split/recombine results would depend on operation order.
Target clean schema
Names are illustrative, but the relationships and invariants are normative. The production implementation should use the repository's clean reset/reseed workflow. The project is pre-launch: there will be no migration, legacy columns, dual reads, compatibility shim, or preservation of current characters.
#![allow(unused)] fn main() { MeasurementProfile { item_id: String, // primary key; FK-equivalent to Item kind: MeasurementKind, // Depletable | Containerized | BulkLot unit: MeasurementUnit, // Milligram | Millilitre | ... standard_basis: MeasurementBasis, } MeasuredObject { id: u64, // stable across every custody transfer item_id: String, kind: MeasurementKind, // immutable snapshot, not a live definition lookup unit: MeasurementUnit, // immutable interpretation of remaining_amount capacity: u64, // immutable initial amount, greater than zero full_contents_mass_mg: u64, // immutable per-object basis full_contents_value_subunits: u64, tare_mass_mg: u64, tare_value_subunits: u64, } FoodMeasuredObject { measured_object_id: u64, // primary key; optional family capability initial_nutrition: IntegerNutrition, ingredient_provenance: Vec<IntegerIngredientShare>, preparation: Preparation, created_at: u64, contamination_anchor: u64, } PersonalMeasuredState { inventory_item_id: u64, // primary key, current custody row measured_object_id: u64, // unique stable object remaining_amount: u64, // mutable, inclusive 0..=object.capacity } PartyMeasuredState { party_inventory_item_id: u64, // primary key, current custody row measured_object_id: u64, // unique stable object remaining_amount: u64, // mutable, inclusive 0..=object.capacity } }
MeasurementKind is a payload-free discriminator if persisted. Profile
columns remain ordinary scalar columns so reducers and operational queries can
inspect them directly. Food, alcohol, and soap profiles remain separate tables
linked by item_id; their existence is independently validated against the
measurement profile.
The definition profile supplies the standard immutable basis for ordinary
goods. Opening a sealed unit creates a stable MeasuredObject and copies its
kind, unit, and numeric basis. A recipe or other transformation instead creates
a derived object whose kind, unit, integer capacity, contents mass, contents
value, and family metadata are calculated from its actual inputs. The evaluator
uses the object's snapshots, never the current item definition, whenever
measured state exists. Thus two
cooked_meal objects may share an item ID while retaining different masses,
values, nutrition, provenance, preparation, age, and contamination identity.
Definition edits cannot retroactively alter existing lots.
remaining_amount is current mutable state. Object capacity and its
full/initial conserved fields are the immutable denominator and basis. A
consumption computes the new total from the new amount, then records the
difference between the old and new totals; it must not repeatedly scale the
already-rounded remainder. The transition to zero assigns every final integer
residual to that last consumption. Integer nutrition and provenance use the
same initial-basis/difference rule, so rounding dust cannot be duplicated or
stranded.
Definitions use one canonical fixed unit per profile. Initial units are milligrams for mass-like solids, millilitres for volume-like liquids, milligrams for effective mass, nutrition subunits selected by the food schema, ABV basis points, and the smallest supported currency subunit. Conversions for display occur only at the API/UI boundary.
Definition invariants
capacity > 0.- Full contents mass/value and tare mass/value use integers.
- Depletable and bulk-lot definitions have zero tare. Containerized definitions may have nonzero tare; zero is legal for a disposable wrapper.
- All checked full-unit additions fit
u64. - Food, alcohol, and soap capabilities must reference a compatible measurement profile and unit.
- A discrete definition has no measurement profile and cannot acquire measured state.
- A newly created measured object validates its snapshotted kind/unit against the creating definition or recipe. Later evaluation uses those snapshots; its immutable basis may differ from the standard basis for an authorized derived lot.
Seed validation rejects the whole definition before inventory mutation. These are authoring errors, not values to clamp.
Personal and party row invariants
The same rules apply to InventoryItem and PartyInventoryItem:
quantityis positive. Zero-quantity rows do not persist.- For Depletable and Containerized definitions, no measured-state row means a full, unopened row. It may have any positive quantity allowed by the inventory limit. BulkLot never uses this representation: even a full bulk lot is quantity one with measured state and an object basis.
- A measured-state row requires a measurement profile and
quantity == 1. - It references exactly one stable measured object of the same item ID and
0 <= remaining_amount <= object.capacity; reducers reject mismatches. - A partial row is
0 < remaining_amount < capacity. - An opened, still-full row may be represented by
remaining_amount == capacitywhen opened identity matters. It does not merge with sealed stock. - A depletable/bulk row reaching zero is deleted atomically with its measured state. A containerized row reaching zero remains as the empty container with amount zero. Discarding or consuming that container explicitly deletes both.
- Exactly one personal or party custody-state row references a measured object. The referenced inventory row is the object's current owner; duplicate custody, cross-owner references, missing objects, and object/state orphans are invalid.
- Deleting a parent and its state also deletes its measured object and family metadata unless the same atomic reducer rekeys custody during transfer. No reducer may publish an intermediate orphan.
The absence of measured state is the final schema's representation of unopened full stacks, not a transitional fallback for legacy data.
Opening, splitting, transferring, and combining
- Opening one unit from an unopened stack decrements that stack. If it was the
last unit, the row may be reused. The opened unit becomes a quantity-one row,
receives a stable measured-object ID with a copy of the standard basis, and
gets state at
capacity. This happens before the first consumption. BulkLot acquisition creates this object/state immediately, including at full capacity; homogeneous sealed goods use Depletable or Containerized semantics, not BulkLot, until opened. - Reducers preflight definition, ownership, bounds, row limits, and all checked arithmetic before mutation.
- A complete partial personal row transfers as one indivisible custody unit to a new party row, or vice versa. The reducer atomically deletes the old custody-state link and creates the new link to the same measured-object ID; amount, immutable basis, family metadata, and container/lot identity do not change.
- Quantity transfer of unopened stock retains current stack behavior.
- "Transfer part of this partial row" means pouring and is deferred. The first rollout transfers the complete row only.
- Unopened rows merge by the existing stack compatibility rules.
- Partial depletable/bulk rows may combine only through an explicit reducer, with the same item definition, unit, relevant food/preparation/provenance and contamination profile and compatible immutable bases, and total amount at most the surviving object's capacity. The reducer keeps the lowest measured object ID, adds exact conserved integer fields, and deletes the other parent, state, object, and metadata atomically.
- Containerized rows do not combine merely because their contents match. Pouring between vessels is deferred.
- If multiple eligible rows exist, selection is stable: requested owner scope, then family-specific priority, then oldest/open partial first, then item ID, then inventory row ID. Reducers must sort explicitly rather than rely on table iteration order.
Effective mass and value
For unopened stock these values come from the definition's standard basis. For every measured singleton they come from its immutable measured-object basis. Let:
Cbe positive full capacity;Abe remaining amount,0 <= A <= C;McandVcbe full contents mass and intrinsic value;MtandVtbe container tare mass and recoverable intrinsic value.
For a measured singleton:
contents_mass(A) = floor(Mc * A / C)
contents_value(A) = floor(Vc * A / C)
effective_mass = Mt + contents_mass(A)
effective_value = Vt + contents_value(A)
For Q unopened units:
effective_mass = Q * (Mt + Mc)
effective_value = Q * (Vt + Vc)
Depletable and bulk profiles set Mt = Vt = 0. Containerized amount zero
therefore retains its tare totals. Products are evaluated in u128, divided
once, converted to u64, then added/multiplied with checked operations. Any
invalid bound or overflow rejects the command before mutation; authoritative
code must not saturate, wrap, or use floats. Aggregators use checked integer
addition and fail the whole operation on overflow.
Flooring is canonical and monotonic. Reducers compute the consumed delta as
effective(old_amount) - effective(new_amount), so each transition uses the
immutable basis and the final transition receives every residual. For any
partition of one amount, the sum
of independently floored contents values cannot exceed the unsplit value.
Combining restores at most the canonical value of the combined amount.
Intrinsic conserved value is distinct from a merchant quote.
Currency and trade
Before arbitrary amount sales, currency gains an integer subunit fine enough for ordinary fractional goods (for example 100 subunits per displayed base coin). Denominations remain presentation/exchange records over that conserved integer.
The safe initial policy is:
- merchants buy or sell only complete inventory rows;
- unopened stacks may still transact an integer quantity of full units;
- a measured singleton is quoted once from its canonical effective contents value plus recoverable tare value;
- all rows/quantities in one authoritative line are first summed into one checked integer intrinsic line value;
- every authoritative modifier, including merchant margin, tax, language,
reputation, and local-problem effects, is a positive integer rational
(
numerator / denominator) or a basis-point value converted to that form; - reducers cross-cancel factors with greatest-common-divisor reduction, compose
the remaining numerators and denominators with checked
u128arithmetic, apply that one composed ratio to the aggregate intrinsic line value, and round exactly once; - player-to-merchant proceeds use one floor after the complete-line aggregate;
- merchant-to-player prices use one ceil after the complete-line aggregate;
- party stake credit, liquidation, surrender valuation, and trade all call the same effective-value function;
- no quote is computed for artificial child portions, and splitting is never an accepted way to obtain multiple independently rounded payouts.
A zero factor numerator or denominator, a zero configured basis-point
denominator, an intrinsic-line sum overflow, a composition/product overflow,
or a rounded result outside u64 rejects the entire command before mutation.
An intrinsically zero line may quote zero only when every factor is valid.
There are no saturating or floating fallbacks.
The reusable prototype API checked_aggregate_price implements this
aggregate-then-compose rule independently of production merchant policy. The
production pricing migration is explicitly out of scope here.
Later arbitrary pouring/sales must carry a deterministic residual/remainder ledger or allocate the line's remainder to one stable child. Such a child is never quoted independently merely because it was split. Repeated split, sale, and recombine sequences must never increase total currency plus intrinsic inventory value.
Item-family semantics
Alcohol
Alcohol definitions retain serving volume, ABV basis points, emergency hydration, and disinfectant effectiveness, but serving volume becomes measured capacity rather than a consumed stack unit.
- Opening a bottle splits one sealed unit. Drinking subtracts the exact integer volume requested or the bounded volume needed by the action.
- Alcohol dose, hydration, and disinfectant use are prorated from consumed volume with widened integer arithmetic. Hydration remains capped by physical non-alcohol water volume.
- Surgery previews the exact container, volume, soap amount, resulting remainder, and effectiveness before confirmation.
- Reserve targets are expressed in total potable volume or servings converted to volume, not row count. Protected strong disinfectant stock remains a separate policy constraint.
- Automatic selection preserves current intent: shared before personal where allowed, ordinary drinks before strong disinfectant, then open partial before sealed, effectiveness/ABV policy, item ID, row ID.
- Drinking from and pouring between arbitrary reusable cups, bottles, and waterskins is deferred. The first implementation consumes directly from the owned source row and transfers only complete partial containers.
Food
The existing food_lot is a useful behavioral prototype but currently stores
conserved mass, nutrition, and value with floating-point fields. The clean
rollout converts durable mass, useful calories/nutrition, ingredient
provenance shares, and value to integer units.
- Every heterogeneous or prepared lot remains quantity one with measured state. Homogeneous sealed units may stack until opened.
- Eating subtracts integer mass and proportional nutrition/value from the same lot. The last operation consumes all residual conserved fields so rounding dust cannot strand or multiply nutrition.
- Recipes consume complete selected portions and create one derived quantity-one output lot whose integer mass, nutrition, value, provenance, and contamination are computed once. No terminal meal may be repeatedly cooked to compound value.
- Partial meals retain preparation, age, provenance, nutrition, and private contamination anchors. Compatible combine requires all relevant metadata to match; heterogeneous leftovers otherwise remain independent.
- Spoilage continues to be evaluated lazily from lot identity and time. No floating conserved inventory field is justified by continuous microbial calculations; derived concentration may use numeric simulation internally while persisted conserved dose/mass inputs remain integers.
- Automatic eating selects oldest/most perishable eligible open lots before sealed lots, then item ID and row ID, subject to shared-before-personal rules.
Soap
Soap becomes a depletable mass/capacity profile rather than a whole-use stack.
- A wash or surgery subtracts the bounded amount actually used. Cleansing and infection-control effectiveness are prorated from that amount and may retain diminishing-return logic outside the inventory arithmetic.
- Remaining soap mass and value fall with remaining amount; zero deletes the row because there is no container tare.
- Rest and surgery previews show exact mass/percent remaining, planned consumption, source owner, expected cleansing/effectiveness, and remainder.
- Automatic selection uses open personal soap before sealed personal soap, then open shared soap before sealed shared soap, with the existing health-risk ordering for assignment and item ID/row ID ties.
Impact map
| Surface | Required behavior |
|---|---|
| Inventory targets | Definition-aware targets use full units for discrete/unopened goods and total amount for measured goods. Reserve calculations include partial rows without pretending they are fractional object counts. |
| Personal/party transfer | Full stack quantities retain current behavior; partial rows move whole with state. Both owner schemas enforce identical invariants. |
| Loot | Tactical results describe full units or explicit strategic measured lots. Finalization creates validated profile/state rows; autoloot uses effective value/mass. |
| Trade | Complete-row interim policy and aggregate directional rounding apply. Quotes, affordability, and settlement stock use currency subunits. |
| Encumbrance/travel | Replace every direct item.weight * quantity path with one shared checked effective-mass evaluator for personal plus party inventory. |
| Party stakes | Deposits, withdrawals, loot shares, reserve value, and member settlement use the same effective value. Partial row custody never loses its stake valuation. |
| Liquidation/surrender | Deterministically value and select complete rows with effective mass/value; delete parent and measured state atomically. Currency remains excluded from ordinary sale. |
| Automation | Resupply, ration planning, autoloot, autosell, washing, eating, drinking, and surgery compare amount totals and effective value/mass, and use stable selection. |
| Tactical handoff | Include remaining amount and units only for relevant carried/equipped snapshots. Tactical state never becomes durable authority. |
| UI | See below; staged controls operate on row IDs and amounts, never display fractional swords. |
Every inventory consumer must go through central helpers. Direct multiplication
of base weight or base value by quantity is invalid once profiles ship.
UI and API contract
The collapsed inventory row continues to show # for discrete or sealed
quantity. Measured singletons show a localized amount and unit, optionally
remaining / capacity and a progress bar. Container rows show contents and
container separately in expanded details; empty containers say "Empty" rather
than "0 bottles." Search and sort use effective total mass/value.
Opening is normally implicit on first use but can be explicit where the player must choose a bottle. Transfer, discard, drink/eat/wash, cooking, surgery, trade, and liquidation dialogs preview the exact row, amount, effective mass/value change, and post-action remainder. Controls use integer step sizes declared by the family profile and clamp only in the UI; reducers independently reject invalid input. The initial UI offers complete-row transfer/sale and bounded consumption, not pouring/mixing.
Public personal and party projections add the standard profile plus stable
measured-object ID, immutable object basis/family display fields, and current
measured state where applicable. Mutation APIs accept inventory row IDs plus
integer amount where consumption supports it; object IDs are returned for
identity but clients cannot reassign custody directly. Generated
adventuresim-stdb-client bindings, strategic-web view models, tactical
snapshots, simulator actions, and reducer call sites must update in the same
schema commit. The initial rollout includes regenerated
adventuresim-stdb-client bindings for both public amount tables, and
strategic-web subscribes to them for live invalidation.
Initial rollout implemented here
- Food, alcoholic drinks, and soft soap are created as quantity-one measured
rows with a full amount of
1_000_000. - Eating and travel eating reduce amount together with the existing lot mass, nutrition, value, provenance, and contamination inputs.
- Cooking accepts integer milliunit portions; the UI stages quarter-unit increments and the reducer checks each amount against the current lot.
- Evening drinking and emergency hydration consume only the fraction needed for the requested effect. Quantity targets reserve the equivalent number of full measured units.
- Washing consumes one twenty-fifth of a soap unit per cleansing point. Surgery consumes that bounded soap amount plus 25 ml of disinfectant.
- Complete measured rows retain their state across personal/party custody changes. Encumbrance, party stake/liquidation, and merchant sale value use the remaining fraction.
- Zero consumption, discard, liquidation, and sale remove companion state with the parent row.
Stable measured-object/profile tables, container tare/recovery, integer replacement of the existing floating food-lot conserved fields, arbitrary pouring, and arbitrary partial-row trade remain target-schema work. This first rollout intentionally uses definition-relative milliunits so gameplay stops wasting whole consumable units without claiming the full container model is complete.
Rollout plan
- Implemented initially: checked mass/value/pricing helpers and definition-relative milliunit state.
- Add the profile, stable object/basis, family metadata, and personal/party custody-state schemas behind per-family creation gates. Merely publishing these tables must not make a reducer able to create partial rows.
- Cut over one family at a time. Before enabling that family's gate, the same clean-schema deployment must atomically update every effective mass/value consumer, read projection/API, mutation reducer, generated binding, strategic simulator action, tactical handoff, and relevant UI/automation. This includes loot, targets, party stakes, liquidation, surrender, encumbrance, trade, and transfers wherever that family can appear. Consumer parity is a prerequisite, not later cleanup.
- Implemented initially: reset/reseed schema support and alcohol/soap reducer cutover while retaining complete-row transfer/trade.
- Implemented as an interim: food consumption and cooking portions are
authoritative, while floating
food_lotconserved fields remain until stable measured objects and integer derived bases land. - Partially implemented: ship each family's measured UI display, amount controls, previews, accessibility labels, and stable live-refresh behavior with its gate.
There is intentionally no live-data migration. Deployment of the schema change
requires an isolated database reset/reseed and regenerated client bindings.
No production state may contain a measured or partial row while any reachable
consumer for that family still assumes base weight/value * quantity.
Verification requirements
- Profile validation: zero capacity, incompatible tare, invalid family/unit, and full-unit overflow.
- Row validation for both owner types: zero quantity, state on discrete item, non-singleton measured state, unmeasured/multi-quantity BulkLot, amount over object capacity, missing/duplicate object custody, and orphan state.
- Full, partial, opened-full, and empty mass/value examples for depletable, bulk, and containerized profiles.
- Derived recipe lots with the same item ID but different immutable bases, nutrition/provenance, and effective totals.
- Monotonicity over the complete amount range and checked aggregation overflow.
- Opening a stack, complete partial transfer in both directions, deterministic selection, compatible combine, zero deletion, and empty-container retention.
- Property tests that partition/recombine preserves conserved amount and cannot increase intrinsic value; trade sequence tests that cannot create currency.
- Pricing tests for factor cancellation/composition, aggregate floor/ceil, aggregate-versus-split quotes, invalid zero factors, and every overflow boundary.
- Family tests for alcohol dose/hydration/disinfection/reserves, food nutrition/recipe/leftover/spoilage identity, and soap cleansing/surgery previews.
- Integration tests for loot, trade, encumbrance, targets, autoloot/autosell, stakes, liquidation, surrender, and generated API round trips.
- UI tests for units, empty labels, amount bounds, complete-row-only controls, previews, sorting, keyboard operation, and SSE refresh deferral while a draft is active.
Deliberately deferred
- Arbitrary pouring, partial-row transfer, and player-selected splitting.
- Mixing liquids or heterogeneous food lots with different profiles, provenance, age, contamination, or preparation.
- Refilling reusable bottles, cups, waterskins, and arbitrary vessels.
- Merchant purchase/sale of arbitrary sub-row amounts.
- Container damage, leakage, evaporation, and nested containers.
These omissions constrain the first rollout; they do not change the durable model.
Issue #150 acceptance map
- Discrete-use, fixed-point bulk, and per-container approaches and tradeoffs: Alternatives considered.
- Integer durable state and personal/party invariants: Decision, Target clean schema.
- Contents plus container mass/value: Effective mass and value.
- Currency precision and split/recombine rounding: Currency and trade.
- Alcohol, food, and soap partial semantics: Item-family semantics.
- Targets, transfers, loot, trade, encumbrance, and automation: Impact map.
- UI and reset/migration: UI and API contract, Rollout plan.
- Gameplay and architecture documentation:
this document plus links from
architecture.md,food-and-cooking.md, andwiki/shared/inventory.md.
Physiology system
Physiology is the skill for observing health, administering prepared interventions, and improving wound recovery. It produces a fallible differential of possible diseases, but never reveals the authoritative disease identity or recommends a treatment. Preparation crafting and chemistry remain deliberately out of scope for issues #214 and #215.
Private authority
The authoritative strategic simulation derives ten bounded functional-loss meters:
- oxygenation
- perfusion
- hydration
- temperature
- inflammation
- coagulation
- nutrition
- neurologic function
- renal clearance
- tissue integrity
Disease curves are deterministic and piecewise linear. A versioned, server secret keyed phenotype changes the relative involvement of meters for each episode while keeping the population mean neutral. Baselines, phenotype values, raw meters, infection identifiers, and disease identity stay in private SpacetimeDB state.
Interventions use generic, versioned profiles with meter deltas over time. Administration records the patient, concrete preparation and profile version, route, amount, optional body region, start and stop minutes, and private sensitivity/adverse variation. No effect lookup accepts a disease key.
All authoritative personal-time paths evaluate disease and intervention effects together. The earliest integer-minute terminal crossing wins, with a stable meter-order tie-break, so splitting an interval into smaller calls cannot change the result.
The authoritative database initializes private key material from runtime randomness and persists one private key for the lifetime of the database. Changing its version requires recreating the disposable pre-launch database; older causal rows fail closed instead of being re-derived with new material. Infection and administration causal rows pin the ruleset and key versions used to interpret them. No secret is compiled into the module, placed in an environment-backed WASM constant, or exposed through a public view.
Observer notebooks
Physiology exposes four period-facing Humours: Sanguine, Phlegmatic, Choleric, and Melancholic. Each is a documented weighted sum of several private meters. The map is intentionally many-to-one, so a humour reading cannot reconstruct a private meter or disease identity.
Notebook authorization is based on persisted pair-presence spans. Joining or rejoining a party opens a fresh span at the lesser of the two personal clocks; departure and death close it by the same rule. Each direction stores the observer's Physiology capability band at that boundary, preventing later training from sharpening historical observations. Every notebook records at most one examination per day. Higher bands produce more finely quantized readings and a better-calibrated differential, but do not add examinations.
The trusted strategic gateway derives a bounded, pre-quantized chart on demand from causal infection, administration, presence, capability, ruleset, and key boundaries. Periodic meter or Humour snapshots are never persisted. A chart contains:
- timestamped, signed humour deviations for each body region
- a fallible differential of possible diseases
- interventions the observer could know about
- explicit gaps for absence
Externally visible findings are never listed directly. Coughing, fever, rash, and every other finding are forced through the deliberately unhelpful Humour lens and contribute to the relevant regions. Healthy is the zero baseline; both positive and negative deviations are shown, and the absolute deviation contributes to the Humour-colored impairment in the corresponding regional health bar.
The physician notebook presents seven tall graphs side by side, one for each body region. Time runs vertically. Hovering, focusing, or selecting an observation opens its quantized snapshot. Medication starts and stops are horizontal event markers, while party absence is drawn as a hatched interval that also breaks the Humour lines. Stable keyed observer noise varies the readings from day to day without changing the authoritative body state, making early treatment response deliberately ambiguous.
Possible diseases are ordered and colored from red through yellow to green. Physiology skill and time spent observing make those colors better calibrated. The differential also compares the expected action of known treatment with the patient's subsequent regional burden, so improvement or deterioration can strengthen or weaken a candidate without exposing private disease identity.
It contains no authoritative diagnosis, recommendation, infection row, phenotype, or raw private meter. Browser subscriptions never include the private tables.
Presentation contract
This document and the Health wiki page are the canonical explanation of meters, Humour weights, chart limitations, and intervention scope. The game does not expose a standalone authoritative reference page: players currently encounter the system through the physician notebook. Future NPC dialogue should keep period claims separate from authoritative explanations so presentation does not blur in-world belief with system truth. Humour information remains textual as well as colored, keyboard focusable, and available through pinned mouse/touch tooltips.
Character
Characters are created by investing some amount of favor into them. The more powerful the character, as determined by their stats, the more favor you need to invest. The exact kind of favor you need also depends on what character you want. If you want an elf character, you need to go do some quests with the elves.
You aren't exactly spawning a character into the world; ostensibly, you are obtaining control over a character who already exists! This means you don't always have to start "fresh" with a young, untrained character with no background. You can create a wealthy, skilled character simply by spending a lot of favor on him.
Personality
Characters have an immutable sparse personality drawn from discrete axes. Generated NPCs receive two to four randomly selected non-neutral axes; first-character candidates preview and persist the same exact generated axes. Personality changes raw morale reactions rather than replacing Will, Social skills, or Religion knowledge. The hygiene axis is Slovenly/Cleanly: Slovenly characters ignore filth morale, while Cleanly characters strongly dislike filth and appreciate being completely clean. Other characters never see these authoritative tags directly.
Authoritative personality is private. Other characters instead keep durable, observer-specific beliefs with confidence and observation time. Beliefs may be wrong and can later be corrected. Insight forms beliefs about other people and is opposed by involuntary Deception modified by Transparency; Insight also governs reflection about oneself. Public morale labels never reveal the true trait that changed a reaction.
Relationships
Affinity is directional: how the subject currently regards a particular actor. It is anchored to the subject's personal strategic clock and exponentially decays toward neutral with a 30-day half-life without crossing neutral. Familiarity is symmetric shared-party time stored once for the canonical character pair. Its displayed effective hours divide shared time by current party size while both characters remain together, and use the undivided total after they separate.
The Social family is Insight, Charm, Command, and Deception. Social outcomes combine the action skill, current Affinity and Familiarity, the actor's diagnosis, the target's true personality and topic sensitivity, and a server roll. Listening is low-risk exploration; more presumptuous actions have greater upside and downside. Only recognized negative morale concerns are actionable. Their topic is derived by the server, and repeating the same approach to the same topic has a cooldown even if the source row is refreshed. Characters use Insight to Reflect on their own concern; reflection can revise a self-belief but never changes Affinity or Familiarity. The interface shows only a qualitative, familiarity-weighted affinity estimate rather than the authoritative value. Observed traits and morale-source interpretations use greyer text when confidence is lower; their exact confidence is available on hover together with a hint about which approaches that trait may favor or resent.
The same social menu also supports ordinary conversation with another living, co-located party member or a currently present settlement NPC. The player chooses 15 minutes to eight hours in 15-minute increments, with 30 minutes as the default. A conversation with a settlement NPC must fit wholly inside that NPC's current presence window. Each quarter hour uses the speaker's Charm and Insight together with mutual personality fit and the existing relationship. Familiarity always records the shared time, but morale and directional affinity can rise or fall; even a skilled, compatible pair can have an awkward conversation, and a poor match can occasionally connect. The observer-facing result remains qualitative and never exposes checks, personality fit, rolls, or numeric deltas.
The available social approaches are filtered by the concern rather than showing every Social skill for every problem. Commiseration is always available as one action: it uses Insight when the actor currently shares that kind of concern and Deception when they do not, so sincere and feigned variants never appear at the same time. Action labels remain grounded in facts the simulation actually knows. The character sheet groups the four skills beneath an expandable Social row, whose displayed value is their average.
First-time players first choose a life stage, then choose a generated whole character. Young candidates are age 16, professionless, and offered as a varied roster of five. Adult candidates are age 22 and freshly journeyman-equivalent; old candidates are age 40 and master-equivalent. Adult and old rosters each offer one candidate for merchant, weaponsmith, armourer, tailor, herbalist, cook, learned religious practitioner, witch hunter, knight, and forester. The specific eligible organization is deterministic but random from the player's perspective when a family has multiple options. Witch hunters join The Hunt of the Pale Lantern, knights join The Order of St. George, and foresters join The Lodge of the Hart King; these three organizations have no profession-of-faith requirement.
Professional candidates preview and receive their complete plausible package: organization and mapped rank, current dues and presentation, required profession of faith, qualifying skills, equipment, ammunition, and currency. Packages are authored per profession and life stage rather than layered over a generic combat archetype: a newly qualified adult has a modest working kit and purse, while an old master has veteran equipment, more supplies, a larger purse, and profession-sensitive experience and presentation. Only deliberately equipped items contribute to the previewed combat capability. Players cannot customize individual fields. The roster is reproduced from a private random seed stored for the browser tab, but nothing is stored on the server until a candidate is confirmed. Age is intended to carry further tradeoffs later; those tradeoffs are deliberately not specified yet.
Players may create multiple characters in the same browser. The strategic header's portrait menu lists the browser's remembered non-temporary characters, marks the current one, and switches between them. Character select returns to the life-stage step so another character can be created. This prototype roster is browser-scoped and is not an account or authentication boundary.
Mortal
Character personality includes an immutable Temperance axis. Temperate and Drunkard are visible non-neutral tags; the neutral state is omitted like other neutral axes. Random mortal/NPC profiles still activate exactly two to four distinct axes across the expanded thirteen-axis behavioral catalog. Mirth, Courtship, Transparency, and Self-knowledge join the existing axes. Presentation and Inclination are always assigned outside that sparse count; private Sex supplies demographic truth and never participates in attraction. The displayed inclination traits are Attracted to men, Attracted to men and women, Attracted to women, and Attracted to neither. Apparent gender identity is Man/Ambiguous/Woman. Man and Woman are normally learned on contact; Ambiguous identity requires an Insight discovery check. Deliberately presenting traits that differ from a character's true traits is reserved for a broader Deception-based feature.
Each actual personality discovery check conserves 0.25 real training hours. An Open subject awards all of it to the observer's Insight; a Neutral subject splits it evenly between observer Insight and subject Deception; a Guarded subject awards it all to subject Deception. Unsupported contexts produce neither a check nor training. Mortal characters age normally and eventually die. They cannot have their physical features customized; when rolling them, players must choose from a limited selection of randomly generated characters. They are cheap and efficient, ideal for players who want a roguelike/extraction-esque experience of frequently rolling new characters, quickly obtaining power, dying, and starting over.
Humans
Dwarves
Inspired by their Tolkien/Warhammer depiction. A proud, stubborn, greedy, short sturdy, and strong race. Dwarves dwell in underground mountain cities. In their days of glory, the Dwarves built extensive tunnel networks between these cities; these tunnels have since been infested by foul creatures.
Dwarves who shame their kin by dishonoring the ancestors, breaking oaths, or engaging with prissy Elven nonsense like magic may be exiled at best, at worst compelled to redeem their honor by undertaking various suicide missions to retake an ancestral realm.
As the race needs a lot of Dwarf-specific assets, Dwarves will not be included in the MVP or tentatively even the next phase.
Halflings
Inspired by their Tolkien/Warhammer depiction. A small, jovial, provincial people generally unconcerned with the matters of the "big people." Would be found in small idyllic villages here and there. Not important enough for the MVP.
Orcs/Goblins
Inspired by Warhammer greenskins, though less comedic and specifically only grown from nasty underground funky pools. An Orc is just a Goblin who had lots of fresh meat thrown into its spawning pool (and must maintain this diet).
If you aren't familiar with Warhammer, the idea is that they are a fungus-based lifeform with genetic memories. The point is for them not to need a complex civilization to be threatening (they already know how to fight and speak) and for you to not feel bad for slaughtering them (no women or children, they emerge REDY 2 FITE). Quest fodder for the MVP.
Ratlings
Inspired by Warhammer Fantasy Skaven, though with the technology level toned down somewhat. These are wretched, craven humanoid rats who dwell underground, both in stolen Dwarven cities and in their own subterranean creations beneath prosperous human cities. They don't need to be in the MVP.
Immortal
Immortal characters do not age, will respawn if killed, and can be customized in detail. Their purpose is to give players the option of a more conventional RPG playstyle than the punishing roguelike experience of mortal characters.1
Respawning an immortal character requires a favor cost equivalent to the death cost of a similarly valuable mortal character. The cost may even be higher for immortal characters, so players would be ill-advised to use them for suicide missions. However, what immortal characters lack in cost efficiency, they compensate for with a higher effective skill ceiling, having unlimited time to train their skills.2
Immortality is based on race, not an abstract per-character flag. All immortal races are said to have "fey blood." In the current roadmap, Elves are the only immortal race planned.
Elves
Inspired by their Tolkien/Warhammer depiction. Tall, beautiful, and haughty, Elves live in either deep forests or fictitious islands. They are generally morally good. Exceptions include the evil "Dark Elves" and the somewhat more neutral, ecoterroristic "Wood Elves."
Dragons
Intelligent dragons who can take on a human form. It would be extraordinarily expensive to actually create a full-blooded dragon character. Some dragons may be unable or unwilling to take a human form. Absolutely not in the MVP and almost certainly not in the polished product unless one of the devs is very insistent.
Halbe: I am certainly not going to try and animate dragon flight.
Beastmen
(rename "Beastlings"? "Shifters"?)
Beastmen have both a beast form and human form that they may shift between. The exact type of beast depends on whatever would be local to them. Can be felines, canines, serpentines, equestrians, lizards, and more.
Probably not in the MVP. Might be in the polished product at least for wolves.
Half/quarter/etc.-blooded
These generally look like normal humans, except they can be immortal and customized. They are for players who want the mechanics of Elves/Dragons/Beastmen but don't want the pointy ears or shapeshifting. These come from breeding between mundane people and the fey-blooded.
In the case of feybloods who can shift between forms, the half-bloods may be unable to shift. Instead, they might take on some intermediate characteristics of the two forms.
Halbe: Yes, half-blooded beastmen are the "designated furry race." And I think gnomes are just elf-halfling hybrids.
Bruno: And I was expecting something tasteful and classy, like the half-bloods are our way of capturing the aesthetic of ancient Egyptian deities in a post-Christian world. Alas.
Vilebloods
When a mundane character consumes fey blood, he can become fey-blooded. However, this is evil, so it also curses him. The exact nature of the curse depends on the kind of fey blood.
- Werewolves/bears/etc are beastmen-blooded.
- True vampires are elf-blooded.
- Mostly analogous to Warhammer Dark Elves. They do not burn in the sun, and they are not actually undead.
- The aesthetic of Freaky Devil-Looking Thing (e.g. imps) is captured by mongrel vilebloods. They may take features from a mix of reptilian, mammalian, Draconic, and/or Elven blood.
Undead
Mortals risen from the dead through unnatural magic.
- A vampire is created when another vampire offers a mortal his blood and buries him alive. After the mortal suffocates to death, he becomes a vampire.
- Zombies and skeletons are not elf-blooded; they are risen via necromancy. They are mindless and must be consciously puppeted by a necromancer.
- Ghouls/wights are zombies/skeletons who have a soul bound to them (the ritual requires elf blood). They are not mindless, and though bound to their necromantic masters, they can act autonomously.
- A lich is a necromancer who has turned himself into a wight. As his soul is bound to himself, a lich is the only type of wight with free will. (This implies the possibility of ghoul-liches, who retain their flesh.)
Death
Characters begin alive. Death is an authoritative strategic transition: Character.alive is the fast life-state flag and one immutable CharacterDeath row retains the first typed cause, source, optional committed-outcome identifier, and the character's personal strategic minute. Repeating the transition is idempotent and cannot replace the original context. Tactical combat may submit a final death outcome, but tactical positions, hit points, enemies, and tick state never enter strategic persistence.
Dead characters remain visible for history and party context, but cannot train, rest, travel, trade, manage equipment or inventory, enter combat, use party actions, recruit, change membership, or chat. Party readiness, forecasts, provisioning, movement, needs, condition updates, and combat construction consider living members only; a corpse's personal minute and location remain fixed while survivors continue. Dead members are not recorded as battle participants, receive no victory morale, mission experience, loot stake, or quest reward, and participant life state is checked again when loot is stored. A disposable simulation capability provides the deterministic death path used for integration testing; ordinary production identities cannot invoke it.
Characters initialize with the German vernacular selected deterministically from their final settlement profile. NPC Yiddish incidence is also deterministic; every selected Yiddish speaker retains a decent local German dialect at the documented 0.8 effective shared-language coefficient. Quest-company leaders atomically replace both Oral and Written language identity after being moved to their authoritative settlement, so a random creation origin cannot leak into their language record.
-
However, everyone's first character (and probably the next several) will be mortal; mortal characters are playable on free accounts, and players can obtain their first with zero favor. ↩
-
Albeit with drastically diminishing returns. ↩
Quests
Quests begin as problems in the world, not tasks waiting for a player to activate them. A settlement may already be suffering thefts, disappearances, dangerous creatures, disease, or disrupted trade before any character hears about the cause.
Investigation skills answer different questions. Bestiary narrows which creatures fit received descriptions and successfully interpreted physical clues. Terrain follows and recovers tracks. Hearing testimony passively uses Insight to form a fallible impression of each specific claim, while Charm, Command, and Deception offer increasingly risky and forceful ways to question that claim. Insight reads demeanor, not hidden facts: a sincere mistake can look sincere, and evasive or partly truthful accounts are inherently ambiguous. Witnesses do not always have quest information withheld, and the wording of an initial or released account does not identify its reliability. A character may question any highlighted claim, even one that looks truthful. A failed challenge strains affinity; Command always imposes some strain. Time together builds familiarity, while the NPC's private personality and morale also matter.
Tracks are followed as a short sequence rather than one all-or-nothing check. Each successful section contributes a physical finding before the final section locates a site; unseen later sections and their destination remain private.
Discovery
Players learn about problems through tavern rumors, local conversations, witnesses, and physical evidence. The journal records what a character has actually heard or observed. It does not expose hidden truth, calculate a single confidence score, or mark every useful person and place in advance.
Accounts can be incomplete, mistaken, evasive, or contradictory. Different characters may therefore know different things about the same case.
Exact map pins require exact believed or visited location knowledge. Textual directions and approximate areas remain text until investigation produces a specific destination.
Cases and contracts
A case is the underlying world problem. A contract is an agreement to pay for some result concerning that problem.
Accepting a contract does not create the case, and abandoning one does not erase it. Some generated problems have no contract at all and are encountered solely through rumors and investigation. Direct bounties are the simpler exception: their issuer may disclose a known site and promise payment for a specific result.
Contracts that require reporting are paid only after the party returns to the issuer and reports the completed work.
Investigation
Investigations can combine social and physical routes. A witness may point to another person, a place, or a suspicious event. Tracks, wounds, objects, and other evidence may support several interpretations until a character has enough relevant knowledge.
Bestiary combines reports a character has actually received with diagnostic clues that character successfully learned. The evidence view and journal list possible monster kinds with qualitative support and provenance. Failure, numeric scores, and the canonical enemy remain private.
Different routes may reach the same finale. A failed lead does not delete an independent route, and a later correction does not rewrite what the character previously believed.
Planning and travel
Before leaving, a party should consider:
- the likely opposition and plausible mistaken identities;
- terrain, distance, provisions, daylight, and opportunities to camp;
- relevant social, investigative, medical, and combat skills;
- equipment suited to armor, large enemies, groups, or ranged threats;
- whether capture, rescue, retrieval, proof, or negotiation may matter more than killing.
Knowing an exact case site makes it available to strategic route planning. It does not accept a contract, resolve an objective, or grant a reward.
Confrontation and outcomes
At a hostile site, the party may enter tactical combat or use strategic autoresolve. Both consume the party's durable condition, equipment, skills, injuries, fatigue, and ammunition. Tactical positions and damage exchanges remain transient; strategic wounds, spent supplies, loot, morale, custody, and case outcomes are committed only at the validated result boundary.
Victory is not synonymous with killing everything. Depending on what the party has learned and prepared, a confrontation may support defeat, driving enemies off, capture, rescue, retrieval, exposure, or another case-specific result. Strategic authority chooses only among outcomes that are still valid for that party, site, and case.
Defeat can wound the party and leave the problem unresolved. An incapacitated party may withdraw to recover but cannot continue ordinary combat or travel until it is ready.
Continuing problems
Unresolved problems may worsen over time. New incidents can add witnesses, victims, evidence, settlement consequences, public notoriety, and stronger hostile groups without rewriting earlier events. NPC adventuring companies still recruit, but they no longer investigate or resolve quests automatically. Conspicuous hostile cases eventually become public and can be referred through nearby innkeepers or eligible organization representatives. The referral creates a durable journal case and exact destination pin containing only the threat type, safe site label, and approximate count.
This keeps cases part of a shared world: ignored threats become a growing combat problem and their public rumor radius expands.
Rewards
Rewards should compensate expected danger, consumed resources, travel, and the chance of serious injury or death. Strong parties are safer but more expensive; weak parties may have almost no chance against a difficult threat. The intended planning problem is to find an appropriately prepared party, not simply the largest available one.
Battle loot enters the shared party inventory and follows the party's stake rules. Contract payment remains separate and is awarded only through the contract's reporting flow.
Technical references
The implementation deliberately separates player-visible knowledge from hidden world authority:
- Quest generation and investigation covers authored content, deterministic generation, testimony, evidence, and observer knowledge.
- Quest authority covers cases, contracts, objectives, missions, outcomes, local problems, recruitment, and incidents.
- Bestiary authority covers stable threat identity, physical knowledge, and preparation information.
- Strategic simulation covers automated balance and regression evaluation.
Time between players is kept somewhat in-sync. The idea is that generally, time advances at 56x speed, so that one week in the real world is one year in-game. The main purpose of this is to account for the realistic travel distances and healing rate that we use, as otherwise the game would be extraordinarily boring. However, its not exactly in-sync, because at minimum there is also a real-time simulation for things like combat or navigating difficult terrain. Thus, each party is permitted to be a little-bit out-of-sync with each other, and can use accelerated downtime to catch up.
Current implementation
Disease is evaluated in the patient's personal character time. Travel, camp rest, settlement rest, and lazy catch-up check every disease boundary crossed by the interval. If terminal physiological failure occurs, the clock and all work stop at that exact minute. This prevents a long skip from jumping over a fatal peak into apparent recovery.
The server stores official time as an absolute number of game minutes rather than a wrapping calendar value. A newly initialized world begins on August 20 at 00:00. A 365-day year is 525,600 minutes, and one game minute takes exactly 84/73 real seconds, making one game year one real week. Calendar displays wrap this absolute number into a day-of-year and time-of-day, but comparisons never wrap.
The server stores an epoch rather than updating the clock table continuously. When a browser opens a page, it requests one snapshot of the character and official clocks and renders that snapshot without a wall-clock timer. The character snapshot also determines the interpolated location sky, the edge-to-edge sun or moon position, and building illumination until an explicit action returns a newer time. Authoritative reducers derive the current official minute from the epoch when gameplay needs it.
Each character has their own absolute minute. Character time advances lazily when their strategic page is accessed or their daily schedule is saved. If they are more than a year behind official time, the server advances them in one transaction to exactly one year behind and does not apply the triggering schedule change; the player can try again after the catch-up. Characters may remain out of sync while idle. At departure every living party member advances forward to the latest compatible party minute; nobody is rewound, dead members are unchanged, and excessive skew is rejected. Journeys persist that absolute departure minute plus separate movement and elapsed coordinates, so camp rest advances progress without changing distance.
Every character saves one 24-hour settlement-downtime plan with integer-minute allocations for activities; individual skill-study allocations are replaced by activities. Its unallocated Leisure remainder includes sleep. Walking advances personal time and travel condition but never trains skills or performs scheduled activities. Camp time first clears each member's fatigue; the remainder applies only activities safe and meaningful in camp, including Prayer and Leisure, while masking settlement work and crime. The pure training and settlement-activity calculations are shared with the native strategic simulation harness; the harness uses repeated one-day actions as its canonical cadence. A live bulk rest evaluates one aggregate outcome and at most one incident interruption, so rounded activity income and incidents can differ from an otherwise equivalent sequence of one-day rests; bulk-rest strategy parity remains follow-up work.
The Social dialog can persist automatic chats for an exact actor/companion pair. When that actor receives positive discretionary downtime after convalescence, maintenance, and fatigue recovery, the server considers enabled companions in stable character-ID order and selects an available approach for the first stable unaddressed source by combining the actor's effective relevant skills with their immutable personality. A Sanguine, Gregarious actor may lean toward humorous Charm, for example, while an Ambitious, Brave actor may favor Command; the strongest effective skill can outweigh that disposition. Exact-score ties favor the riskier fitting action instead of collapsing to Listen. The ordinary authoritative social action path still decides life, party, co-location, topic, cooldown, skill, and outcome rules. At most three companion attempts occur per downtime interval, and at most one attempt is made for each pair; an approach currently on cooldown is excluded from selection. Disabling the option blocks future automatic attempts without affecting manual actions or erasing history. Travel, generic waits, and intervals consumed entirely by required recovery or maintenance do not trigger automatic chats.
At a settlement, every explicit activity in that plan can also be performed immediately by selecting its icon. The activity dialog chooses one to 24 whole hours, beginning at the character's current personal minute and showing the resulting end time. This advances personal time and applies that activity's training, economy, Morale, Virtue, Fatigue, and incident risk without changing the saved plan. Immediate activity is not rest: it does not heal, wash, repair equipment, provide inn service, or apply the plan's Leisure remainder. Prayer/Meditation and Carousing use their saturating morale curves over the selected interval rather than pretending their effects are linear.
Activities combine reduced-rate training with another strategic result:
- Apprenticeship is available after accepting a service NPC's offer to teach their profession. It costs Gold and divides conserved training time among that profession's associated skills. At profession rank 2, Practice replaces paid instruction and earns a small wage; at rank 4 it earns a substantially better master's income. Religious variants are called novice, cleric, and teacher rather than apprentice, journeyman, and master, and their independent practice earns Virtue instead of Gold.
- Combat Training includes sparring and target practice. It trains equipment-relevant Melee, Ranged, Dodge, and Block along with Will and Balance.
- Carousing trains Charm, grants saturating Morale, and imposes a small Virtue penalty.
- Prayer recites and practices prayers rather than studying doctrine. For a professed character it trains their own Religion tradition at 25% speed, and its saturating morale is scaled by the party's knowledge of that tradition. A character with no professed religion instead sees Meditate, receives one quarter of the ordinary saturating morale independently of party Religion, and gains no Religion hours, Fervor, or neglect.
Religion stores only direct hours in each tradition. Correlated knowledge is derived from those direct hours and never fed back into storage. Religious apprenticeship and practice train the tradition represented by the service NPC rather than an aggregate Religion skill.
Within Combat Training, current equipped hands determine the relevant Melee, Ranged, Dodge, and Block weights described in Stats. Training deterministically catches the lowest normalized trained hours up before maintaining their weighted balance, while also practicing Will and Balance. Changing equipment redirects future training without rewriting the saved schedule.
- Labor earns personal gold from effective Strength and Endurance checks during settlement downtime (
hours × (Strength + Endurance) / 4, rounded) and trains Will at 25% speed. - Thievery earns more gold in more populous settlements during downtime and trains Stealth at 25% speed. Stealth improves the take while reducing both notoriety and the continuous chance of discovery.
- Raiding earns gold during downtime and feeds the same equipment-derived leaf-skill distribution as Combat Training at 25% speed. It does not prefer Ranged over Melee or derive Block and Dodge practice from armor. Raiding produces high notoriety and a high retaliation chance.
The schedule previews each activity's daily Gold, Virtue, Morale, and Fatigue at the currently assigned time. Notoriety is presented as negative Virtue so future honorable activities can use positive values on the same scale. Positive preview values are green, negative values are red, and zero is neutral.
Notoriety is persisted per character and displayed as strategic state, but it has no downstream consequences yet.
Thievery and Raiding discovery is resolved whenever settlement downtime advances, including explicit rest and off-screen catch-up. The continuous exposure formulas are:
thievery_discovery = 1 - exp(-0.12 * hours * population_scale / (1 + stealth));
raiding_retaliation = 1 - exp(-0.35 * hours);
Raiding is checked first because an organized retaliation supersedes a watch patrol. On discovery, the activity creates a typed strategic incident independent from quests and contracts. Caught Red-Handed pits the party against the town watch; Retaliation at Dawn pits it against armed retainers. Both offer tactical combat, autoresolve, or retreat through the encounter map. The party's active quest is never replaced or mutated.
At a settlement, a player may rest at an inn or temple, moving that
character's personal time forward even if it passes official time. Rest may be
entered as whole days, or as an HH:MM duration keyed to a selected wake time.
The wake-time slider moves in whole-hour intervals, uses the travel planner's
solid night, sunrise, day, and sunset colors, and defaults to 08:00; the
duration's minus and plus controls change it by one hour while direct entry
accepts exact minutes. Settlement rest always schedules at least 24 hours
before the next selected clock time, preserving the character clock's exact
minute. The same control is available from the Map for free, party-wide rest
at the party's current settlement, en-route camps, and quest destinations.
Field rest permits a sub-day interval, defaults to the time needed to clear the
most-fatigued living member, and lets the leader wait for a chosen departure or
combat time; selecting the current clock time means its next-day occurrence.
Scheduled downtime uses the shared Leisure calculation documented in Stats: six hours offsets baseline fatigue, tiring activities such as Labor must then be offset, and only recovery left after the fatigue carried into that interval reaches zero earns diminishing-return morale. That earned result updates one capped recent-morale source at the interval's end, so refreshing state cannot award prospective morale or stack repeated syncs. The automatic "until healed" recommendation includes health, field-repairable yellow equipment condition, and the remaining ETA of items left with a craftsperson at the current settlement.
Inn rest costs 2 gold per started day and includes full board: elapsed calories and ordinary drinking water are covered, existing food and water deficits are cleared, and personal and party provisions are preserved. Ordinary settlement water is also consumed automatically during non-inn settlement rest, clearing thirst without spending carried water. Temple rest is free sanctuary intended for characters down on their luck, but it does not provide food. A future karma system will account for taking undue advantage of it.
Convalescence, blood recovery, and automatic field maintenance use only the interval's unallocated Leisure minutes. A fully allocated 24-hour schedule therefore grants no passive healing, while an empty schedule grants the full interval; the absolute-minute calculation is invariant under splitting the same interval into several rest calls. Bleeding and infection exposure still advance through every elapsed minute, and scheduled activities apply over the same calendar interval rather than being delayed until recovery finishes. Immediate activities remain non-rest actions.
Inn affordability is checked against the requested stay before any rest effect is applied. If disease or another physiological boundary clips an affordable stay early, only the started days in the actual elapsed prefix are charged.
The rest summary itemizes the selected inn full-board charge separately from other net spending during the interval, such as alcohol or apprenticeship, without attributing that additional spending to a single activity.
Strategic travel adds calories to the fatigue reservoir at the current marching calibration of 6,000 calories per full day. It also consumes food and water proportionally through the persistent strategic-needs state. The fatigue reservoir remains a separate representation of exertion and future sleep pressure: eating does not erase the fatigue caused by marching. Travel, camp, and private rest use carried provisions. Temple rest uses carried food and ordinary settlement water; paid inn rest feeds and hydrates its guest as part of full board. Recent morale events decay against each character's absolute strategic minute, so resting and travel both move them toward expiry.
The calendar treats Day 7 and every seventh day thereafter as Sunday. A religious character who is in a settlement on Sunday receives an explicit call to keep a full day of worship and rest. Traveling during any part of Sunday counts as refusing that call. The server applies the same continuous Fervor- and party-Command-based morale penalty once for that Sunday, including when a journey begins Saturday night and ends Monday morning. A pending Sunday demand is automatically resolved as refused when the party departs; already resolved Sundays cannot be penalized twice.
Throughout this wiki, the term "official time" refers to the most current time according to the server. Your character can be exactly one year behind official time, beyond that they will have to catch up with downtime (resting or training) before you can do anything else. Characters can move ahead of official time through settlement downtime; party time synchronization and its UI will be refined later.
For example, in the example scenario, at the time that they venture forth from a settlement they are in sync with the official time, but their four ~20-minute simulated encounters incurs a time-debt of 80 real-world minutes. 80 minutes in the real world is about 3 days of official time, thus when their characters return to a settlement they will ostensibly be recovering, training, relaxing, traveling between settlements, or working some non-adventurous job for at least 3 days before they set out again.
Why bother keeping players within a year of each other?
Its not necessary for the game to work, true, but it would contribute to a sense in which the world feels like a real simulation rather than an abstract game as many MMOs do. When you end up at the mercy of the simulation and sustain a very serious injury that kills your weekly playtime, there is always the last resort of just creating an additional character. In fact, it is expected that players will maintain multiple alts for this purpose.
Ok, then why allow players to desync in the first place?
Because the world is to-scale with realistic healing rates. It would be really boring even if it were constantly at 56x speed. Plus, there couldn't be a real-time tactical layer.
Implications
The most odd implication of this is that ostensibly, characters that are further from official time are sort of prescient about certain things in the future, and characters closer to official time do not know the outcome of events which have ostensibly happened. As an example:
A griffon has nested near a town. Geoffrey, who is 3 weeks behind official time, decides that he wants to try and slay it. However, while he is forming his party, Jack, who is 5 months behind official time, also takes the quest and slays the griffin since he put his party together very quickly. Geoffrey then learns that actually, the griffon was apparently slain several months ago.
When Jack put his party together, he was joined by Derthert, who was actually an entire year behind official time when he saw the open party. This means that Derthert must have consulted a diviner or seen this opportunity in a dream then decided to spend 7 months training/working in this settlement before the prophecy of this quest comes true. Bizarre, but it somehow works out.
We are not actually simulating an economy (at least for the MVP) nor do quests originate from circumstances of the simulation (at least for the MVP, they're just totally arbitrary fetch/bounty quests), so the implications of this shouldn't be a big deal, but it is weird to think about.
Language exposure
Actual elapsed settlement time grants conserved ambient Oral exposure in the local distribution. Travel grants each party member at most one elapsed interval of conversation exposure, chosen from a sorted pre-gain snapshot, so companions do not multiply time. Profession work grants Written exposure from centralized literacy profiles: merchants write substantially more than smiths; medical work uses Latin; Catholic work uses Latin; Jewish work uses Hebrew and Yiddish; other current religious work uses German. A distinct physician service remains follow-up work; medical Latin currently uses the existing herbalist/medical profession seam. Foraging advances the acting character's discrete personal strategic clock by the actual injury/disease-safe prefix, exactly once. It is not a wall-clock job.
Travel
Travel is a strategic activity. The party plans a route, chooses a daily schedule, prepares supplies, and advances time until it reaches the destination, stops at camp, or is interrupted.
Departure weather
Strategic weather is evaluated from absolute minute, coarse geographic cell, and elevation using a versioned deterministic authority. Conditions are clear, rain, or snow with bounded intensity. Recent intervals deterministically produce ground moisture and snow cover; there are no per-tile weather rows.
Routing snapshots the weather rules version, interval, precipitation, intensity, moisture, and snow cover at departure. The snapshot participates in route cache identity and is persisted with route authority. Rain may increase effective Wetlands before path search and add mud duration. Snow cover blends the party's Snow expertise into route checks and cost before path search, then splits (without duplicating) the road-discounted training budget. An active journey is never rerouted as later intervals pass.
Route planning
The settlement map shows historical roads, ferries, settlements, known exact case sites, and the party's current route. Selecting a destination previews the fastest route available to that party.
Routes account for roads, open ground, woods, hills, water, crossings, and directional elevation. A party with stronger Terrain skills may prefer a different path because its members move more efficiently through the terrain they understand.
The five Terrain leaves are Plains, Forest, Hills, Wetlands, and Urban. Native wetland coverage is retained beneath roads, so marsh crossings use and train Wetlands even when the road itself supplies the faster travel surface.
Players should make strategic choices about resources, danger, and timing rather than manually approximate the shortest geometric line.
The source and artifact contracts are documented separately:
- Viabundus covers the historical road and settlement source.
- Strategic route terrain covers compiled elevation, water, terrain, and routing facts.
Speed
Road travel is fastest. Open ground, sparse woods, deep woods, wetlands, steep grades, and off-road case sites impose progressively greater costs.
Off-road wetland movement is 0.5 km/h. A road over wetland trains the underlying Wetlands skill at 10% exposure, the ratio between 0.5 km/h wetland movement and the road's 5 km/h.
The party moves at the pace of its limiting members after attributes, encumbrance, fatigue, terrain skill, and route surface are considered. Overloading one character can therefore slow everyone.
Daily schedule
The leader chooses how many hours per day the party walks and whether that window is centered on daytime or nighttime. The remaining hours become camp and downtime.
A longer walking day reaches the destination sooner but leaves less time to recover fatigue, treat injuries, cook, train, or perform other camp activity. A member who cannot recover during camp carries fatigue into the next day.
The route preview shows five synchronized rails:
- food;
- water;
- fatigue;
- terrain;
- day and night.
These rails forecast both movement and camp time. Actual camps already reached remain part of the journey history even if a later rest changes the remaining forecast.
Camps and redirection
A journey longer than its current walking window stops at a persisted camp. While camped, the party can rest, treat injuries, manage supplies, or redirect the remaining journey.
Choosing a new endpoint changes the plan from the party's current physical location; it does not teleport the party or reuse an obsolete straight-line duration. The leader may also turn back.
If the leader becomes unable to act during an expedition, a ready party member may direct field rest and an evacuation journey on the party's behalf. This is a narrow rescue authority: it applies only while the leader is publicly unready and does not transfer leadership or permit the companion to accept quests, initiate ordinary travel, or command combat objectives. Incapacitated members remain with the party, consume ordinary time and supplies, and are carried along the existing journey rather than abandoned or teleported. Their body and inventory remain part of the party's burden, while staggered members contribute reduced carrying capacity and incapacitated members contribute none.
If no living member is actionable, ordinary action authority remains unavailable. At a coherent persisted off-settlement journey camp only, an alive authoritative leader may nevertheless attempt supplied passive camp rest when all condition statuses are known and nobody is critical. Publicly symptomatic members may convalesce this way. An unresolved encounter or incoherent party/journey/itinerary forecast holds before rest. This permission models the party remaining at rest; it cannot be used to continue the journey, choose an encounter response, pursue a case, manage a contract, or exercise leadership.
Food and water
Every living traveler consumes food and water over elapsed strategic time. Shared provisions are used before personal supplies. Settlement departure fills available water capacity, while field camps rely on what the party carried or can safely obtain.
Insufficient food or water creates durable need and condition penalties. Emergency alcohol may provide limited hydration, but its alcohol content caps the useful water and creates its ordinary drinking effects.
See Inventory for provision storage and Food and cooking for authoritative food behavior.
Stealth, detection, and interdiction
Strategic stealth determines whether nearby groups detect one another early enough to choose a response. Party size, member skill, terrain, light, speed, and the opposing group's perception all matter.
A detected party may be intercepted or forced into a slower, disorganized state. This prevents the map from becoming a consequence-free race in which a player can always pass through a hostile group.
Random encounters
Encounter checks occur at deterministic movement boundaries. Terrain, daylight, route position, and relevant nearby threats influence what can occur. Retries and different travel chunk sizes do not reroll the same boundary.
An interruption records its strategic time and route position and offers only the choices justified by the encounter. If combat begins, the tactical or autoresolve system owns the immediate fight; the journey retains only its validated interruption and result.
Case sites
Travel to an exact known case site may leave the road network and follow native terrain routing. Knowing or tracking the site is navigation state only: it does not accept a contract, progress an objective, reveal hidden enemies, or grant a reward.
After resolving or abandoning the situation, the party plans onward travel from its actual case-site location.
Training and exposure
Walking trains the Terrain skills corresponding to the ground crossed. Road travel still provides discounted exposure to the underlying terrain; camp time does not.
The active route stores its validated geometry and terrain mixture so a character's later training cannot retroactively change a journey already in progress.
Services
Unresolved local problems can impose capped trade and disease consequences. The inn is the discovery funnel; a settlement without an available inn uses overview.
People and locations
Every seeded or imported settlement has persistent local NPC identities in addition to its service providers. The overview/public area and service locations contain multiple people; towns and larger settlements also populate a keep. A horizontal, keyboard- navigable circular portrait strip selects whom the active character addresses. It is attached just above the resizable chat panel, opposite the party portrait strip, and moves with the chat's top edge. The selected NPC's description is centered in the remaining stage between the two strips. Service pages initially select the service provider, while other locations select a deterministic local. Selecting someone else keeps the party in place and updates the visible physical description and greeting. Dialogue subjects appear as highlighted phrases in what NPCs actually say rather than in a separate topic list. Names are not globally or settlement-locally unique. When one NPC refers the player to a different local who has the same displayed name, the dialogue explicitly says that it means "the other" person and repeats the contact's profession, appearance, and usual location. When the speaker is the contact, they identify themselves in the first person and make their testimony subject clickable in that line.
The book button immediately to the right of the settlement identity and official time toggles the character's journal without leaving the current location. Journal mode replaces the settlement's left rail with a recency-sorted quest list and its right rail with the selected quest's dry log; toggling it again or pressing Escape restores the location rails.
Public squares, residential areas, and, for towns and larger settlements, the keep appear as selectable building tabs alongside services in the settlement header. Until bespoke building art is available, non-service tabs reuse neutral building art and house/castle icons. These places use the same authoritative portrait, description, and chat surface; villages and hamlets cannot enter a keep that their population does not have.
NPC presences and daily time windows are strategic database state, not tactical positions or tick state. The player view exposes physical presentation, occupation, household, observable presentation, and public local role, but never private sex, personality, motives, beliefs, quest truth, generation weights, required causal bridges, or the private explanation for an unusual presence. Generation uses one stable typed weighted evaluator for production and tests; zero weights are impossible, while any rare choice marked as bridge-dependent is rejected unless the settlement context supplies that bridge.
Settlements may have public disease outbreak facts: disease, start and end character-minute bounds, and intensity. Acquisition uses deterministic continuous overlap plus innate and acquired immunity, so dividing the same stay into smaller rest actions does not reroll exposure. Imported settlements carry a bounded strategic industry profile derived from land use, hydrology, soil, geology, historical woodland, population, and route accessibility. It describes plausible production rather than stock on hand; accessibility can reduce scale but cannot invent resources.
The map camera follows the rendered map element's measured aspect ratio and observes later layout changes. Tile selection therefore covers the world area actually visible in desktop, narrow, and resized layouts without distorting or letterboxing the map.
Raster terrain uses a visible light-brown area for open hills, green for forest, and dark green for their overlap. It does not add symbolic hill or mountain stamps to the paper map.
Settlement and quest destinations share the same strategic location shell. A settlement's base location view shows population statistics and historical alternative names on the left and a short population-based description on the right. Imported settlements may also expose a plain-text Viabundus settlement/city description in a collapsed section labeled with its source language. Its Map tab contains an accessible SVG interaction layer over detailed Paper AVIF world tiles with roads, water, generalized GLO-30 height bands and contours, and explicitly partial Copernicus forest coverage. The browser loads only cached tiles covering the current pan and zoom; current and selected settlements, locally issued available quests, the party's active quest at its issuing settlement, connectivity, settlement names, links, and the straight line from the current location to the selected destination stay in the dynamic inline SVG overlay. This keeps each response bounded rather than exposing every quest in the world. Settlements use population-class village, town, and city pictograms, while quests use diamond markers. Labels retain a consistent screen size, appear progressively by settlement importance and zoom, and avoid one another in screen space. Every canonical settlement or visible geographic quest symbol can be inspected through the same ?destination= URL used by the ordinary destination list. Selecting a destination initially fits the map to the corridor between it and the party's current location, with a close local view when both locations coincide; map symbols retain a consistent on-screen size throughout zooming, and the closest view is backed by a high-quality level-6 tile set. A directly connected settlement or active quest destination shows distance, journey time, and the existing travel action; any other settlement or available quest shows useful detail without a travel form. Dragging pans, the mouse wheel or a two-finger pinch zooms, and keyboard arrows, +, -, and Home provide accessible navigation without adding map controls, a legend, or source information to the screen. Links and server-rendered selection remain usable without JavaScript. Settlement services remain separate tabs and are available only at settlement locations.
Geographic map pins and settlement travel actions require an imported
source_node_id on the current settlement. Demo or otherwise source-less
settlements show an explicit map-data-not-initialized state, and source-less
destinations are omitted from the geographic SVG rather than interpreting
their nongeographic coordinates as longitude and latitude.
The backend classifies every materialized settlement by population as Hamlet (under 2,000), Village (2,000-3,999), Town (4,000-7,999), City (8,000-12,999), or Capital (13,000 and above), with population level as the fallback when no estimate exists. These regional bands ensure the imported 1544 playable area represents all five settlement scales. The strategic map progressively hides lower-level settlement pins as the camera zooms out while always retaining the current and selected settlements. Each service tab layers its existing service SVG over a grayscale, CSS-tinted building background. Unknown settlements, hamlets, and villages use the village set; towns use town overrides when present; and cities or capitals use city overrides when present. A missing higher-tier image falls back to that service's village building. The location header is twice the height of compact application headers so the low silhouettes and large service marks remain legible. The tabs meet the lower edge of the sky and the active building is shown by an underline. The settlement name and saved character time share one carved or engraved rectangular sign island. The active tint carries through both interface rails, whose recessed interiors, edge beams, square corner blocks, and darker interactive rows are all derived from that tint, as well as through character inspection via the validated building URL parameter. Quest destinations use green environmental framing. The location header renders a continuously interpolated sky from the active character's saved time snapshot: daylight is bright blue, dawn and dusk are warm, and nighttime plus the building surfaces are darker still. The sun or moon follows an edge-to-edge arc that peaks over the center at noon or midnight, while other header text uses protected dark labels for reliable contrast.
Weaponsmith chimneys add a subtle decorative SVG smoke layer without changing the raster building or service semantics. Quest destinations use two physical tabs: the default Map view shares the unlit tent scene used by travel camps, while the Enemy view places a skull mark over the encounter ground. The Enemy view presents combat before resolution and recovered loot afterward, so loot is not a separate tab. All effect and prop layers are noninteractive.
The wilderness props follow the ornament anatomy at
styles/timber-framed/ornament/<variant>/ornament.png. Each uses a 512-by-512
transparent canvas, a bottom-center anchor on source row 487, and the shared
top-bar scene scale range of 0.8473–1. The raster may overlap only the standard
service-tab art bleed (0.65 rem inline, 0.45 rem above, and 0.5 rem below);
animated effects may rise through its transparent upper field. These
front-facing compositions must not be mirrored.
Camp and quest-location headers also carry a distant grayscale wilderness horizon behind their tabs. Forest, grassland, and hills variants use the same 2880-by-240 transparent panorama contract as settlement horizons and inherit the live sky tint and brightness. Until imported terrain selects the actual biome, a deterministic hash of the camp party or quest-location ID keeps the assigned scenery stable between visits without adding persisted state.
Each settlement offers a number of services as tabs of a unified trade page. Each service corresponds to a profession, represented by a single NPC. A greeting links the NPC's profession; asking about it explains the work and offers a second linked topic through which the active character may become an apprentice. The left side lists what the NPC offers, including training or other services where appropriate, and the right side lists what the party offers.
The general merchant teaches Command; cooks at inns teach Cooking; weaponsmiths and armourers teach Smithing; tailors teach Tailoring; herbalists divide their instruction between Physiology (one half) and Anatomy, Knife, and Tailoring (one sixth each); and the church teaches its own religious tradition through prayer and practice activities. Inns also sell cooking implements and food ingredients. Apprentices pay for instruction. At rank 2 in every associated profession skill they become journeymen who may practice independently in cities for a small wage, and at rank 4 they become masters whose practice earns a good income. Religious progression uses novice, cleric, and teacher as neutral interface terms, and religious practice earns Virtue rather than money.
This is a profession and activity system, not a guild-membership simulation. There are no persisted guild affiliations, rivalries, dues, membership limits, exclusive apprenticeships, or restrictions on where a qualified character may practice beyond the activity requiring a city. Several professions may teach the same skill without making the character a member of all of them.
The location header reflects settlement scale through architecture and its distant horizon. Unknown settlements, hamlets, and villages use the low village set; towns use moderately prosperous two- and three-story guildhouses; cities and capitals use taller masonry civic and merchant buildings. Each tier has inland, Baltic coastal, and river horizons. Until the world import supplies hydrology, a deterministic hash of the settlement ID assigns one of those three variants, so the view is stable between visits without creating persistent geographic data. The imported selector is intended to replace that temporary hash directly.
Across each architectural tier, churches and watchtowers rise above ordinary service buildings while retaining the shared ground line. Every facade keeps a common central light wall field for the separately layered white service mark; the mark stays at one height across the tier. Tall facades generally place their entrance beneath that field, with centered doors favored for churches and watchtowers and used selectively for other city buildings.
Horizon art preserves its aspect ratio and stays centered on the bottom edge. Ordinary wider screens crop its sides instead of stretching fields, buildings, bridges, ships, or towers; ultrawide-specific composition is deferred.
Services show up at the top, above the list of items in their own list. Each service has a button to expand it, which shows a custom per-service form. To rest at an inn, for example, there is a slider for how many nights you would like to pay for. Doctors may have a healing menu for different treatments (bandaging, physiology for diseases, surgery). Smiths may have a menu where you can search for a custom piece of equipment designed by another player which you can pay them to produce. Mount & Blade Bannerlord has a good reference for this menu, including the ability to trade intangibles (the barter menu with other lords kind of has this).
Weaponsmith and Armourer trade rows also offer Repair, plus a Repair all action. An action is disabled only when the item is undamaged or every damaged condition bin exceeds that smith's skill; mixed damage is accepted and repaired as far as the smith can manage. Submitted items leave the owner's inventory and appear in a bottom-anchored custody panel below the independently scrolling wares list. The panel grows only as needed up to half the list height and then scrolls internally. It shows the ETA and the residual condition the smith cannot repair. Finished items can be retrieved after leaving and returning to the settlement, with no expiry.
Halbe: Brothels may provide... other services... for a morale bonus. ||But also a risk of being afflicted by a disease||.
In the center of the screen, clients may render a 3D window of the NPC representing the service. They can have dialogue that plays, like a greeting when you open their menu, a goodbye when you leave it, and comments as you interact with their menu. But this should never interfere with the gameplay. You don't have to click through dialogue in order to buy something, it just plays in the background as you use their service. This is not important for the MVP, and later down the line this would also be a great opportunity to add voice acting and mocap to give the world some personality.
The shared chat panel floats just above the bottom of the center view with a translucent, fixed environmental background and can be resized vertically from its top edge. Its height is shared across settlement and quest-location pages and remembered by the client. Local, Party, Settlement, direct-message, Guild, and Info messages share one chronological stream rather than separate tabs. Messages are distinguished by color without a repeated channel-name prefix. A compact row of colored square toggles filters each channel: Local is white, Party blue, Settlement yellow, direct messages purple, Guild green, and Info grey. The channel name is available as the toggle's hover tooltip and accessible label rather than persistent text. Enabled channels show their full color, while disabled channels become translucent; there is no inner enablement mark. Info is reserved for game notices such as inventory and currency changes.
Halbe: The inspiration for this is the Maiden in Black from Demon's Souls, who recites an incantation while you are in the level-up menu.
Social
Each settlement has at least an inn, which serves as a social hub for chatting with other players and forming parties. Quests originate from the NPCs who run settlement services rather than from a separate notice board. A service tab uses a gold exclamation when its NPC has an available quest and red when the active completed quest is ready to report, and the quest is offered through linked lines in the shared party chat. Map destination lists mark settlements with available quests in gold and the party's active quest route in red; the current settlement is included as a non-traveling row, and completing the active objective leaves its marker red until turn-in. Parties are independent of quests, and every character is always in one: a new or departing character leads a party of one.
The crown on a portrait identifies the party leader, which is always the leftmost member. Current aggregate Physiology, Command, and Religion checks are stacked vertically as the leftmost element of the floating party strip, immediately before the party-inventory chest. Surgical capability is never aggregated: recruitment displays the character's Anatomy/Knife/Tailoring composite, while each procedure uses the applicable individual combination, so backup practitioners matter when the primary practitioner is wounded or several patients need simultaneous triage. The role-add button sits immediately to the right of the rightmost filled party member and opens the centered recruitment popup. The popup separates current roles, saved templates, role details, individual recommendation groups, and its final action into distinct sections. A leader creates a named Role with individual minimum recommendations and a quantity; each position created by that quantity is a visually grouped Slot. Current roles can be reopened in the same builder to change their name, recommendations, or total slot count. Slot count cannot fall below the number already filled. Deleting a role clears its pending applications and role association without removing party members who filled it. Saved role specifications live in a toolbar at the top of the popup. They can be loaded by selecting them, renamed or deleted through hover actions, or created immediately from the current recommendations through a separate naming prompt. Saving a template is independent of adding an active recruitment role. The Combat group contains adjacent melee, ranged, and heavy checkboxes plus weapon precision and armor tier sliders; the Mobility group contains Athletics and Endurance sliders. They are recommendations rather than hard gates, so applicants and applications that fall short receive a warning but remain actionable.
Applicant inspection uses the same icon-based character summary as a full character sheet. Its equipped-hand profile is loaded from the applicant's actual equipment, so dual-wielded duplicates collapse and hybrid weapons expose all of their weapon leaves. This presentation does not alter recruitment recommendation matching or capability checks.
Party authority is collaborative without becoming ambiguous: the leader executes party-level actions directly, while any other living member may attempt the same controls to send a persistent suggestion. The leader receives Approve/Deny notices beneath that member's portrait. Suggestions cover travel, quest acceptance/abandonment, tactical combat and autoresolve, recruitment roles and applications, member removal, party skill targets, shared inventory targets, mission cancellation, and disbanding. Turning in a completed quest is the exception: any living party member at the quest giver's settlement may report its completion and immediately claim the reward for the party. A member's newest travel suggestion replaces their older destination, and their shared-inventory edits coalesce into one notification. NPC leaders approve suggestions automatically after two seconds.
Leadership uses standing votes. At any time, a living member may use the crown control above any living member's portrait, including their own, to assign or reassign their one vote. The selected crown remains visible; a vote for the current leader is shown as a non-interactive gold crown. New solo parties receive a self-vote, and every living member joining through a party merge automatically votes for the destination's current leader. Votes survive leadership changes. A living leader is replaced only when a challenger has at least 66% of living members' votes. If the current leader is dead, the inclusive threshold is 50%; when only one member survives, normalization supplies that survivor's self-ballot so succession cannot deadlock. Dead or departed voters and invalid candidates are removed. When multiple candidates qualify, the greatest tally wins, then the lowest character ID. Re-evaluation occurs after votes, membership changes, and death, without requiring a newly elected leader to immediately retain 66%.
The leader separately configures party-level targets for Physiology, Command, and Religion from the aggregate bars above the party portraits. Each notched bar shows the current aggregate as its fill and its whole-number target from 0–5 as a movable marker; clicking anywhere on the bar moves the marker to the nearest notch, and the marker may also be dragged. Hovering reveals the aggregate to one decimal place and its target. Physiology uses the bounded geometric-support equation described in Stats: member values are sorted highest-first, then receive weights of one, one half, one quarter, and so on before being combined against the unfilled portion of the five-point scale. Religion uses each character's maximum effective tradition check as a UI-only coverage summary, while Command uses the strongest speaker, a saturating coordination allowance, and supporting members' deviations from a 2.5 baseline. These checks never filter an individual role. The same bars preview a prospective member from either side of recruitment: green shows an increase and red shows a Command decrease caused by a below-baseline recruit. Checks receiving no contribution are omitted from a projection. Candidate cards otherwise retain only their capability summary rather than repeating exact statistics.
A character applies to the role as a whole rather than an individual slot. Open slots in the same role appear as overlapping portraits, without a connector; filled positions become ordinary party-member portraits and lose their role presentation. Hovering an open slot reveals its recommended tags and applicant names. Selecting it replaces the sidebars with exact role requirements on the left and detailed applicant capability values plus Accept/Reject controls on the right. Every applicant in that right rail has a selectable portrait; selecting it shows the applicant's stats on the left and their placeholder portrait in the center without replacing the request list. Other applications stay pending while the role has capacity and are rejected only when its last slot fills. Notification badges live on the corresponding open-slot stack, while the browser window title retains the total pending count. Local development creates two randomly named bot applicants per slot: one that meets the recommendations and one that deliberately misses a requirement.
The default chat channel is Local. NPC conversations belong to the active party and service NPC; player conversations belong to both complete parties, so party members share the same history even if they open the subject later. Player conversations require both parties to occupy the same strategic location. Selecting any local player opens their stats, bio, party information, and the shared conversation; a stranger may submit a general application, while an existing member sees the relevant leave or remove control. General applications share a zero-capacity, requirement-free Unassigned role. Zero capacity stops a role from being advertised but does not discard its pending applications. Incoming players who speak to the party appear as selectable portrait shortcuts at the lower-right of the center view. Quest-giver recruitment is advertised only while the recruiting party remains in that settlement; if every role has zero open slots, the NPC mentions the helping party without inviting applications. The leader's linked name in that dialogue selects their local character profile and conversation.
Clicking any filled character portrait selects that character. The character sheet shows its automatic icon summary before biography and skills, while the opposite rail shows attributes and health. Neutral personality axes are omitted, and each shown personality tag has a tooltip stating its exact morale multiplier and any event-duration multiplier. On one's own biography, hovering or focusing the Religion entry reveals a Renounce action when the character currently professes a faith. Summary ranks deliberately use healthy aptitude-capped values: current injuries do not alter recruitment or character summary icons. The summary's tooltip still exposes the exact healthy rank, and the full skill rail separately shows injury-adjusted current performance. Recruitment recommendation matching continues to use the existing capability projection; the icon summary is presentation only.
Portrait social affordances open the normal social popup for party members and present settlement NPCs. The selected person's morale/relationship area opens the same popup. Its ordinary Chat activity has a 30-minute default and a 15-minute duration slider up to eight hours. Quest-specific confrontation approaches are not duplicated there: they appear only while the corresponding quest dialogue is active. The fallible Insight impression happens passively when the witness's quest testimony is heard.
A living active character with Physiology 2 or better sees the selected character's Physiology surface as a passive, durable notebook derived from actual shared-presence spans. It shows quantized Humour readings, recognizable symptoms, known interventions, localization appropriate to historical skill, and explicit gaps, but never diagnoses or recommends. The active character's Cooking skill icon continues to open cooking.
Herbalists sell concrete prepared interventions into personal inventory. The patient or an authorized, co-located party member may administer one with an explicit route, amount, and optional body region, and may stop an active administration. Physiology does not craft preparations; Herbalism #214 owns ingredients, lots, composition, recipes, and crafting, while Chemistry #215 owns chemical behavior. Foraging remains deferred.
Every settlement currently has one church with a fixed faith. Its priest offers conversion only to that faith through the dialogue system; changing the settlement's stored religion changes the priest's topic, profession, and the faith-specific church icon in the settlement navigation. Multiple churches in one large city are deferred until the settlement service model supports distinct church instances.
Hovering near a non-leader portrait reveals a removal action beside its inventory action. The leader may remove another member, while a non-leader sees the action only on their own portrait and may use it to leave. A player-controlled character must settle any party-inventory stake before removal; dismissing a generated companion automatically withdraws its liquid gold stake through the normal party-inventory system before it leaves. The leader disbands the party instead of removing themselves. Generated companions automatically use the normal settlement-rest system until their injuries heal whenever the party reaches a settlement; their personal strategic clocks advance independently.
Personal and shared inventories retain a desired quantity per item. Personal targets belong to the character; shared targets belong to the character who leads the party, so their preferences return whenever they lead a later party. A positive target keeps an empty row visible for restocking. Merchant trading exposes Player and Party tabs on the player's rail and uses the selected inventory and its gold.
Merchant, party-member, party-chest, and loot panels share staged transfer controls. One chevron stages one item, two stage only the quantity needed to reach or preserve the destination target, and three stage everything movable. Footer controls apply the same action to every row from top to bottom. Equipped items are excluded until unequipped.
Moving near a filled portrait reveals its backpack action. A party member's backpack opens the two-character item offer view. The active character's backpack opens a discard view: items are first staged into a left-side Discard list, can be removed from the draft, and are deleted only after confirming Discard. Equipped items cannot be staged.
Armor is summarized from the equipped pieces covering each body region, not item names or an average armor score. 1/4 armor means either a shield or a helmet worn with a cuirass; 1/2 armor requires a helmet and cuirass plus either tasset/leg protection or a shield; 3/4 armor adds both arms and thigh/knee protection; full armor requires high coverage on every modeled region. This follows the broad historical silhouettes: a pikeman's half armor consisted of helmet, cuirass, gorget, and tassets (Art Institute of Chicago), while cuirassier three-quarter armor ended around the knee and omitted lower-leg defenses (Metropolitan Museum of Art). The current body model has one slot per arm and leg, so high regional coverage is temporarily used to distinguish full harness from three-quarter armor until upper/lower limb slots exist.
All of this is done in hypertext, but what's ostensibly physically going on in the world is that NPCs explain their own problems and rumors spread once a party takes the work: "a couple of adventurers are planning on slaying those goblins that have been ambushing the merchant caravans, I hear they're looking for an archer. You should seek them out at Grub's Tavern if you're interested". When you show up in their group chat, you're approaching them at their table.
Off-Topic
Not all socialization in settlements is relevant to quests. The tavern is also effectively a public chatroom. Later, after the MVP, we can also give players the ability to purchase a building and make factions which may serve as faction-exclusive chatrooms.
Halbe: Or they could freely discriminate in other ways. In-world racism between Elves, Dwarves, and Humans would be very appropriate. Even the kind of discrimination that would be considered objectionable in the modern day would be fine, like sexism or intra-human racism, due to the system described in the next section
Moderation
We don't want to be the speech police, but there is inevitably going to be spam or links to pornography and other objectionable content that we will have to deal with. However, it would be great if we could give players the tools to enforce speech themselves and opt-in to more strict moderation than the bare minimum needed to stop spam and illegal content. Essentially, a player-run faction could own a place like a tavern and would be responsible for handling the moderation. There can be multiple taverns in a settlement, so if one of them has an overzealous moderator then you could simply go to one of the other ones. If they are all overzealous, especially if its that a particularly obstinate group of players are trying to establish a monopoly to enforce their annoying speech rules, then we can leverage the fact that this is a game not just a forum. Steal their stuff, assassinate their characters, burn their building down. Normally, these things would be hard to get away with. But players might have the ability to rate the moderators, and if enough people complain then we can increase the likelihood of success when attempting these "faction warfare" actions (when you do not have the will of the people, player establishments are protected by favor.
None of these player-run social hub features should be in the MVP, we will just be very selective about who we invite for testing until we add support for this stuff.
Languages
Each imported settlement has an inferred East-central, West-central, and Low vernacular distribution totaling 100%. Low rises northward; longitude divides the central dialects, with southern Thuringia favoring East-central. Yiddish is a small per-person incidence, never a town-exclusive language. Deterministically selected Yiddish NPCs are fluent in Yiddish and have a 0.8 best-shared-language coefficient with a fluent local German; direct German hours account for the Yiddish/German correlation. Demo settlements use explicit fallback profiles. Books, libraries, and tavern/priest translators require future item and service systems.
Each settlement also has a deterministic, versioned economy profile. Population, prosperity, road access, and nearby production jointly decide which services exist and which stock categories are common. A tiny settlement may expose only an inn and general store; a village uses a general blacksmith; prosperous towns split weaponsmith and armorer services. Generalists carry broader categories where specialists are absent. The server enforces service availability for trade, herbalist care, and repairs. The overview exposes prosperity, specializations, and every religion represented by the canonical legal status, not merely the faith selected for the single church/priest presentation.
Authored organization chapters add distinct Place Facades alongside ordinary settlement services. Each opens a real organization building with one deterministic persistent representative. Visitors may enter and speak to the representative; nonmembers may ask to join, while members conduct dues, reactivation, promotion, and presentation business through the compiled dialogue. A service-linked guild keeps its ordinary merchant or craft service and dialogue in addition to its separate chapter building.
Trade
Local-problem pressure uses the same checked basis-point adjustment after base and language pricing for displayed quotes and reducer transfers, including food.
The compiler supplies each imported settlement with canonical local production outputs and a marginal, local, or regional scale. Future trade simulation may consume these signals, but rules v6 does not create prices, inventory, or shipping flows.
For the current strategic prototype, each available storefront exposes an unlimited catalogue selected from the settlement's economy profile. Items have a base coin value; merchant buy and sell prices are derived from it with shared hidden profit-margin and sales-tax multipliers. Both the merchant and player inventory tables display each item's per-unit weight and relevant coin value. Coin is authoritative inventory rather than a separate character resource. Every settlement reproducibly selects one denomination from a fixed 1544-flavoured set: Rhenish gulden, Lübeck mark, Hamburg mark, Saxon thaler, Brandenburg groschen, and Danish mark. Starter funds and newly issued payments use the issuing settlement's denomination; quest and battle rewards use the quest's issuing settlement. All denominations currently have equal value and are accepted everywhere.
Inventory presents every character or party's currency as one ordinary, collapsed Coin row. Its quantity, value, and weight aggregate the underlying stacks. An accessible disclosure reveals read-only denomination rows; expansion is transient and has no effect on sorting, filtering, or bulk actions.
The General Market, Weaponsmith, Armourer, Tailor, and Inn use the same live trade interface and transaction reducer. The specialist storefronts filter their unlimited displayed stock by item category, while all use the same pricing and buy/sell behavior. Drafting a trade immediately displays the item and coin quantity changes on both sides; inventory changes only persist after choosing Offer. Each offer is bound to its exact storefront and unique persistent service provider. The reducer revalidates the settlement economy, provider service, provider location and schedule, and every purchased catalogue item before pricing or mutating inventory. Inn cooking supplies therefore remain Inn stock rather than being reclassified as General Market goods. Trades are also bound to the settlement where the character is currently located; visiting another settlement's URL does not allow remote trading. Party-scoped purchases spend pooled coin first, then the active character's personal coin for any shortfall. The Party tab's collapsed Coin row therefore shows both sources while drafting a purchase. Personal coin contributed to a party purchase grants that buyer an equal amount of additional party stake; spending already-pooled coin does not create new stake.
Multi-page workflows may pass a local absolute-path URL in return_to. A
successful merchant offer returns to that exact path, query, and fragment;
external and malformed destinations are rejected. Travel provisioning uses the
query to preserve the selected destination and target surplus while visiting
the market.
Herbalists use a narrower authoritative purchase path. They offer unlimited ingredients plus all eight pre-prepared medication courses, but prepared medication remains rejected by the generic merchant reducer. Each course costs more than the normal merchant cost of its recipe ingredients, using shared pricing helpers on both the server and storefront. Mixed and multiple purchases are allowed; every medication course enters personal inventory as its own quantity-one row. The herbalist page deliberately omits party-inventory buying and explains that restriction so courses cannot become unusable shared stacks.
Weaponsmith and Armourer storefronts also accept individual equipment instances for repair through separate actions that never enter the sale draft. A smith repairs only condition bins at or below their independently seeded skill (minimum 3), but may accept an item with additional harder damage and leave that residual condition untouched. Custody and the quoted ETA persist across travel and have no collection deadline. The smith quotes the full job when accepting it: the item's base value multiplied by the share of damage that smith can repair, rounded up to at least one coin. The quote is stable while the item is in custody and is paid from personal coin when completed work is retrieved. The custody table shows durability, ETA, and this full-job cost. A row arrow retrieves that exact quoted order by default; Shift changes it to retrieve up to two matching ready orders, and Control changes it to retrieve all matching ready orders. The header arrow defaults to two and Control changes it to all ready work in that shop. Bulk retrieval stops before the first order the character cannot afford rather than failing already-affordable retrievals. Removing a staged purchase before offering it simply cancels that purchase; it does not create a sale or apply a merchant fee. The confirmation popup appears in the center of the view only while an exchange is pending and includes Offer and Cancel controls; Cancel discards the entire draft. Loot, discard, character trade, merchant trade, and party-inventory transfers all use this same centered confirmation pattern.
Every inventory action exposes one inward-pointing arrow control. Row controls default to one arrow, become two while Shift is held, and become three while Control is held. Header controls default to two arrows and become three while Control is held. These modifiers apply consistently to merchant, character, party, loot, discard, and smith-custody inventory views.
Equipped inventory stacks remain separate from unequipped stacks. Merchant purchases are always added to an unequipped stack (or a new stack), and the UI does not offer transfer or sale controls for an equipped stack.
The backpack action beneath the active character portrait opens the inventory discard view. Discarding follows the same draft-first interaction as trading: the player stages quantities into the left-side Discard list, may cancel the draft, and must press Discard before the server removes anything. Equipped items are never eligible for deletion.
The best shared Oral-language coefficient is authoritative for merchant quotes. Lower mutual proficiency raises purchase prices and lowers sale proceeds; the web quote and reducer use the same core rounding helpers. Prepared medicines remain on their existing specialized purchase path.
Dialogue architecture
Settlement dialogue is the markerless discovery boundary for local problems. Inns surface unknown unresolved symptoms; overview is fallback only when no inn NPC is available. Locals repeat referrals. Hidden causes and destinations stay private.
Publicly notorious recurring hostile cases are the exception to investigation secrecy. An eligible innkeeper, or an explicitly capable organization chapter representative speaking to a dues-current member, may state the canonical threat, exact site, and approximate count band. This is one shared authoritative dialogue disclosure; it carries no testimony, evidence, preparation advice, or client-selected case ID. The disclosure upserts a durable observer journal case and exact public-alias pin; later referrals refresh the count without adding a second entry.
Witness questioning uses proposition-granular authority. Hearing each atomic
claim automatically creates a private, fallible Insight assessment for that
observer. The gateway exposes only an opaque challenge token, the exact
displayed claim boundary, and a bounded unknown, likely_false, or
likely_true presentation signal. It never exposes reliability, canonical
truth, proposition identity, rolls, thresholds, or correctness.
The testimony draft separately authors the exact challengeable substring and
may author any subset of Charm, Command, and Bluff lines. Present lines must be
nonempty and unique; the client has no generic fallback. Surrounding narration
and punctuation remain ordinary text, and speaker attribution replaces
redundant phrases such as “The witness says.”
Insight reads demeanor rather than acting as supernatural fact detection:
sincere mistakes lean the same way as sincere accurate testimony, deliberate
deception leans the other way, and evasive or partly truthful accounts have no
private directional signal. Noise can still make every assessment wrong.
Each fresh NPC encounter may issue a private, dialogue-session-scoped witness social capability for that participant, but the gateway does not project it until the observer actually selects and hears that witness's quest testimony in the current session. Generic greetings and ordinary conversation therefore show no claim controls. Once engaged, each highlighted claim is a real accessible control, including green and uncertain claims. Activating one opens its local Charm, Command, and Bluff responses immediately below the utterance. Relationship, familiarity, and demeanor remain in the normal social popup. Hidden concern binding, diagnostic correctness, personality fit, checks, rolls, and chances remain private. Hearing testimony spends no additional time for Insight. Charm is the lowest-risk and lowest-leverage response, Command is medium and always strains affinity, and Bluff is highest-risk and highest-leverage. Each response spends five strategic minutes. A claim can receive at most one response; other claims remain actionable. Action receipts and session revisions make retries idempotent and stale requests fail closed.
Ordinary conversation is deliberately outside the quest dialogue controls. The normal social menu offers a duration-selectable chat with a present local; claim responses appear only in the relevant active dialogue session after that witness's quest testimony is heard. Casual chat can still change that NPC's private morale, directional affinity, and familiarity, so time spent getting to know someone can affect a later confrontation without revealing whether they have quest information.
A challenge succeeds only when that particular claim is factually inaccurate and its social check succeeds. Accurate claims and insufficient checks share the same safe failure wording. Success may release only the canonical withheld testimony already authored for the exact witness; released testimony follows the same structured claim and passive-assessment path. The response shows only the realized clamped affinity change, never the exact relationship value.
Persistent settlement actors
Settlement dialogue is authorized against persistent settlement_npc identities and
their authoritative strategic settlement_npc_presence, rather than a client-created
<settlement>:<service> name. A location may contain several NPCs; changing the
addressed portrait changes the actor while the character remains at that location.
Service providers retain their service conversation, while ordinary residents use the
compiled local-resident conversation and cannot receive service-only topics.
The public NPC row contains only visible identity and presentation: name, age band, presentation, height, build, hair/facial hair, complexion, visible features, clothing, profession, household, and local role. Private demographic sex, the internal projection key, population seed explanations, and relation weights remain private. Dialogue facts include typed age, profession, status, clothing presence, prior interaction, language compatibility, observable location role, and time period. Hidden causal circumstances remain private until a future discovery system deliberately reveals them. Greeting response priority remains deterministic.
Population choices use contextual weighted relations. Zero plausibility is a hard
exclusion; low positive values remain rare. Curation weight stays separate from world
plausibility, and unusual demographic/location combinations require a causal bridge.
One relation owns each conditional weight; inverse tables are not duplicated. Production
population creation calls the canonical typed evaluator in adventuresim-core, and its
private serialized explanation records the input context and every selected relation,
factor, decision, and required bridge.
Scripted dialogue is a compiled, server-authoritative strategic system. It is
separate from free-form local chat. Authors edit the JSON-compatible subset of
YAML in content/dialogue/*.yaml; builds validate and embed a deterministic
catalog, its SHA-256 revision, and compiler-derived source locations. Runtime
servers do not read loose content files.
Authoring model
The distinction between generic and quest dialogue is authority, not a
separate rendering system. A direct topic response may contain the typed
runtime: testimony binding when—and only when—the same response applies
receive_referred_testimony. For the exact generated witness, the server
expands that slot through the normal turn pipeline into persisted text,
claim, and text fragments. A claim fragment transports only its displayed
value and event-local order. Proposition identity, reliability, factual
accuracy, demeanor, checks, and rolls never enter event JSON. The private
assessment row must match the exact session event, claim order, and displayed
text before the gateway makes the fragment interactive; a missing or
mismatched row leaves ordinary inert text. period_claim remains display-only
and literal YAML cannot manufacture claim authority.
Conversation-start responses and prompt result turns cannot contain
authoritative testimony because those execution paths do not carry the exact
emitted claim-event sequence into the receiving effect.
Each conversation has a stable ID and named participant roles. A role declares
player or npc plus minimum/maximum cardinality, so one authored exchange can
require a shopkeeper and assistant or address several players. Optional
on_start responses use the same conditions, priority rules, attributed turns,
effects, and automatic source mapping as topic responses. The server evaluates
one start response exactly once when it creates a session; use it for greetings
instead of making the browser select a topic implicitly. Topics have
stable IDs, labels, knowledge/eligibility conditions, and explicitly prioritized
responses. A response contains attributed turns composed of text and inline
topic fragments. It may also contain an allowlisted typed runtime slot for a
speaker's visible identity, place, symptom, claim, uncertainty, referral,
evidence, testimony, or contract terms. Authored literals and runtime slots
remain distinct in the compiled catalog and source map. The server resolves
slots from authoritative strategic rows and persists only bounded inert text;
generated values are never scripts, conditions, effects, or canonical truth.
Runtime testimony is the one structured binding: each authoritative draft
becomes a claim boundary with surrounding punctuation retained as text, and
multiple drafts retain deterministic event-local order. The compiler rejects
a testimony slot without its receive effect, the effect without exactly one
slot, and attempts to place more than one testimony slot in a response.
Prompts support yes_no, single, and multi choices and
first_response, unanimous, majority, or all_respondents resolution.
Choices may contain result_turns; these are appended to the durable transcript
only after the prompt resolves and its effects succeed.
Conditions are a typed tree: always, all, any, not, and fact. Fact keys
are allowlisted in FactKey; participant profession, organization, religion,
familiarity, clothing,
service role, location, time period, quest state, and flags are supported. New
world facts require a Rust resolver change. Never put executable code, SQL, or
client-trusted effects in content. Effects are likewise a closed enum. A client
sends catalog revision and stable topic/choice IDs; the authoritative reducer
resolves turns and effects from the embedded catalog.
Investigation dialogue uses generic facts and effects rather than per-case content IDs. A local-problem referral records the character-owned safe rumor receipt immediately when the tavern/overview conversation starts, without accepting a contract. Its observer-safe presentation is persisted once in the dialogue transcript immediately after the authored greeting. Referral turns name a known contact or describe them, give their occupation/relationship and expected location, and retain explicit uncertainty. Truthfulness, private motives, hidden causes, and undiscovered evidence never participate in topic eligibility. When the addressed NPC is the named contact, the referral switches to first-person wording and presents the testimony subject as an inline clickable phrase. A different same-named NPC is still explicitly disambiguated.
Generated return and exposure finales reuse compiled generic topics in both service and resident conversations. A topic is projected only when the server can pre-issue exactly one generated case/objective binding for the addressed NPC. Execution revalidates the public/canonical mapping, recipient, evidence or custody, session revision, and one-use binding before emitting a typed fact.
Run just dialogue-check before review. Use
cargo run -p adventuresim-dialogue --bin dialogue-check -- explain <id> to
inspect response priorities. Equal highest priorities at runtime are rejected
as ambiguous instead of depending on file order.
Persistence and multiplayer
SpacetimeDB stores dialogue sessions, named participants, attributed events,
open prompts, idempotent action receipts, per-character answers, and
per-character topic knowledge. All raw dialogue rows are private; fail-closed
gateway views are the only subscription surface, and the trusted web process
additionally filters them to the selected character. Each player participant
receives a projection row; nonparticipants receive none. The authored
condition/effect catalog is never sent to browsers. Player organization facts
come only from a current, locally recognized presented organization. The player
profession is that organization's canonical starting_role.profession, rather
than its organization ID; religion comes from authoritative profession-of-faith
state. Reducers verify gateway
authority, same-party membership, shared settlement, role cardinality, catalog
and session revisions, topic eligibility, and stable choice IDs. Every NPC role
is bound to a real persistent NPC, and every mutation revalidates each NPC's
exact session location and current schedule. Selecting an NPC creates a fresh
encounter so contextual and prior-interaction facts are reevaluated; old
sessions remain history rather than an indefinitely reusable active view.
Free-form local_chat_message remains an independent stream.
Organization business uses the dedicated compiled
organization-representative conversation. Join, dues/reactivation,
promotion, and presentation are closed effects with no authored or
client-submitted organization ID. Strategic authority derives the institution
only from the organization-bound representative NPC, verifies that the NPC
occupies that institution's exact authored chapter location, then applies the
existing membership authority. Membership state, promotion availability,
dues, and current presentation are server-built dialogue facts. Before asking
for confirmation, the representative names the organization and states the
joining fee and admission requirements, the dues amount, interval, and current
standing, or the current rank and next-rank requirements as applicable.
Committed prompt answers are retry-safe when a response is lost; a new answer
or an action receipt from a different prompt cannot mutate a closed prompt.
The representative's greeting anchors a highlighted organization-business
topic. Its follow-up links are selected from authoritative membership facts:
nonmembers see joining, suspended members see dues only where dues exist, and
current members can follow a gated chain through dues, promotion, and
presentation without seeing actions unavailable in their current state.
The web conversation surface exposes topics only as highlighted phrases in NPC dialogue. Clicking one asks about that subject; there is no separate list of generic or undiscovered topics. While a prompt is open, the shared composer matches its choices, shows a unique prefix as grey inline completion, and lets Tab accept it. Multi-select answers use comma-separated choice labels. Other text continues through the independent free-form chat stream.
Developer mode and source editing
The hammer button immediately left of the character portrait toggles developer
mode. It is off by default and persisted locally in the browser. Its
content-editing consumers include dialogue and item definitions: authored NPC
lines receive keyboard-accessible GitHub editor links, and expanding a concrete
inventory row reveals an Edit YAML button. Repository and ref are centralized by
the server (ADVENTURESIM_SOURCE_REF, default main); source paths and spans
come from compilation, so writers never maintain line numbers. Unsupported or
unsafe paths do not produce links. Extend developer mode only by querying the
root data-developer-mode attribute; do not create independent toggles.
Schema changes are pre-launch and intentionally have no migration or legacy compatibility path. Recreate/reseed the development database and regenerate the SpacetimeDB client when deploying this schema.
Organizations
Organizations replace the former universal profession record. A character may
hold any number of memberships, while organization_presentation records the
single organization (or none) whose identity and privileges the character is
currently asserting.
Content
content/organizations/*.yaml is compiled into adventuresim-core. Definitions
declare stable IDs, names, chapters, recognition, admission requirements and
fees, arbitrary ordered ranks, recurring dues, activity training and rewards,
and privileges such as bearing arms, wearing armor, or licensed foraging.
Organization-level privileges are inherited at every rank; rank-level
privileges are additive.
Chapters are explicit authored records, not settlement-ID flags. Every record
names its settlement, a bounded stable organization-* location ID, building
name and kind, and the title and profession of its representative. Each
chapter therefore becomes a distinct navigable building even when several
organizations share a settlement or an organization is also linked to an
ordinary settlement service.
An organization may also declare explicit starting_role metadata: one of the
ten authored start profession families plus distinct adult and old rank IDs.
Presence of this block makes an organization eligible for deterministic
first-character sampling. The catalog validator rejects unknown families,
missing ranks, identical adult/old mappings, chapterless eligibility, and a
full catalog that leaves any profession family uncovered. This metadata is
never inferred from an organization's name, service, requirements, or skills;
the catalog-only test organization is therefore not eligible.
For settlement-scoped recognition, at least one playable chapter must also be
recognized. Admission and selected starting-rank professions of faith must
agree; conflicting authored faith requirements are rejected.
Requirements and training targets are tagged data. No code path decides that an organization is a guild because it teaches Smithing, or a church because it requires a religion. Mixed requirements are intentional and supported.
content/settlement-policies.yaml declares settlement arms and armor
restrictions. Only the currently presented, recognized, active, dues-current
membership supplies an exemption. Ownership is unaffected: when a character
loses an exemption, prohibited equipment is unequipped.
Foraging licenses use a separate global presented-privilege evaluation. They still require persisted presentation plus a matching active, dues-current membership and its current rank, but do not require the current settlement to recognize the organization. A valid persisted presentation therefore survives travel and entry into an unrecognizing settlement. This does not weaken the locally recognized equipment privilege rules. The Lodge of the Hart King grants Low Game, Fish, and Plants throughout its ranks; its Forest-4.0 Master rank adds High Game.
The first catalog includes migrated trade and religious bodies plus three universal, denomination-neutral adventurer organizations: The Hunt of the Pale Lantern for witch hunters, The Order of St. George for knights, and The Lodge of the Hart King for foresters. Their chapters combine the playable footprints of the former denominational and regional variants. The catalog also retains a deliberately eccentric Catholic cooks test organization. These are historically informed fictional institutions rather than claims that each exact organization existed in every listed settlement.
The character sheet exposes the presented organization as a compact profession
picker; it is only a self-presentation control. Joining, dues, reactivation,
and promotion are conducted by speaking to the representative in the
organization's chapter building. Its large label combines the member's rank with the service profession
where one exists (for example, Apprentice Weaponsmith), while the smaller
label names the organization. Crests are stable heraldic marks derived from the
organization ID and service using the locally vendored Game Icons charges, so
every catalog organization has a stable heraldic identity without
presentation-only persistence fields.
Persistence and authority
SpacetimeDB owns membership, rank, dues, presentation, payment, promotion, and
equipment-law enforcement. Startup seeds exactly one deterministic persistent
representative per authored chapter. The NPC carries an explicit organization
binding, has an all-day authoritative presence at the exact chapter location,
and uses the compiled organization-representative conversation. Dialogue
effects carry no organization ID: authority resolves it from that live NPC and
revalidates the actor, session settlement, and exact chapter location before
reusing membership reducers. Joining is idempotent; the joining fee is charged once.
Crossing a paid-through boundary suspends membership and clears its
presentation. Paying at a chapter reactivates it without retroactive arrears.
Forester (ranger), witch-hunter, and knightly organizations explicitly author
public_threat_referrals; the capability is never inferred from names, skills,
or services. At any authored chapter, its representative may disclose public
hostile cases within that settlement's bounded rumor reach to an active,
dues-current member. Organization presentation is not required. The dialogue
uses the same canonical, observer-scoped disclosure path as an eligible
innkeeper.
Organization training and activity require a current membership and local chapter. Their skill mix is read from the catalog, including fixed skills, Bestiary and Terrain leaves, equipped weapon skills, and Religion only where an organization explicitly teaches a particular tradition. The Hunt of the Pale Lantern instead divides its training evenly between Spirit Bestiary and equipped weapon skills.
MVP privacy and uniqueness limitation
organization_membership rows remain public in the MVP because strategic-web
subscribes to them to render membership and schedule management. This is not an
authorization boundary: only the effective presented organization governs
privileges or public dialogue identity. An owner-scoped projection can replace
the raw subscription later. The join reducer enforces one procedural membership
per character and organization by checking the pair before insertion; the
pre-launch schema does not yet add a composite unique index.
Validation
Build validation rejects unknown fields and invalid or duplicate IDs, requirements, ranks, weights, organization- and rank-level privileges, religions, skill leaves, malformed chapter locations, duplicate chapter settlements, and settlement policies. A canonical check against a compiled Viabundus world is also required:
python scripts/validate_organization_world.py --world path\to\compiled-world.json
The cross-world check is separate because the catalog can be compiled without the large Viabundus dataset.
Quest generation and investigation
This page is the canonical technical reference for authored quest content, deterministic case generation, observer-specific investigation knowledge, evidence, and discovery.
Source-aware ambiguity
Generated testimony presentation is not a reliability oracle. Account wording families are selected independently from hidden reliability, generated location claims share the same route-segment grant shape, and journal confidence describes provenance rather than sincerity. The same visible wording, source shape, confidence band, and destination class must remain compatible with truthful, mistaken, partial, evasive, and deceptive private states.
Hearing quest testimony automatically produces a fallible passive Insight assessment for each authoritative proposition boundary. The observer-safe signal may be uncertain, lean untrue, or lean true; all three can be wrong and all three remain actionable. It never declares intent, changes a proposition, or reveals factual accuracy. Private accuracy and demeanor are separate: mistaken witnesses can look sincere, while evasive and partly truthful accounts have no clean directional signal. or grants navigation. Every primary witness volunteers the same reliability-neutral public pattern account. A separate private pattern detail may or may not exist, sampled independently from reliability; its presence never changes the initial dialogue's text, cardinality, order, or source. Atomic claim boundaries come from private generated testimony records, never browser sentence splitting.
Persistent NPC personality and starting morale are sampled once from server-private entropy and stored; public NPC IDs do not determine them. Named morale-event rows preserve why morale changes. Current settled morale, relationship affinity, familiarity, personality, and the chosen approach all affect resolution, while browser projections remain qualitative. Ordinary timed chat is available from the normal social menu rather than the quest transcript. It may improve or strain morale and affinity and always builds familiarity, but it never diagnoses pressure or releases testimony. Claim-specific Charm, Command, and Bluff responses remain available only in the active dialogue session where that observer heard the claim.
Field action skills are deliberately narrow. FollowTracks and
ReacquireTracks use the matching Terrain skill; other investigation actions
use observation, with ambushes combining observation and Stealth. Until mixed
terrain exists, Forest maps to Forest, Hills and Underground to Hills,
Settlement and Ruins to Urban, Plains and Road to Plains, and Marsh to
Wetlands.
Generated physical trails are immutable private manifest authority. A
TrackTrail owns an ordered chain of observer-scoped TrackSegment IDs; every
segment records its ordinal, explicit terrain, safe finding, and adjacent
predecessor/next links. Exactly one physical tracking action owns each segment.
Completing an early segment records only its safe finding and route-segment
knowledge, then activates the adjacent segment. Only the final segment may
produce an exact true-site destination. Inactive segment capabilities are not
projected, so the browser cannot infer the remaining segment count, destination,
fixed difficulty, or canonical cause. Attempt progress remains local to the
opaque segment capability.
Developer quest editor
Settlement pages expose a complete quest-authoring dialog when the existing browser-local developer mode is enabled. The top-right book button loads the startup-compiled catalog, closed engine mechanics, and the current settlement's persistent navigable NPCs. Its typed repeaters cover the template and canonical cause, consequences and incident cadence, sites and areas, witnesses and testimony/referrals, physical evidence and deterministic inspection topics, routes and action outputs, DNF objectives/custody/hostiles, finales, dialogue producers, canonical events, and causal bridges.
Submission derives the settlement from the session-selected character's authoritative current location; the browser never supplies a settlement ID. Structural diagnostics (references, bounds, navigable NPCs, and generated-case invariants) always block creation. Catalog compatibility/curation diagnostics block by default, but the explicitly labeled developer override can suppress only that second tier. Invalid input is transactional and writes nothing.
Debug and automatic generation call the same strategic materializer.
DeveloperGenerationContext persists the complete definition, compatibility
override, private observer entropy, ordinal, current witness candidates, and
catalog revision in private authority. Authority validation compiles that
context again and requires an exact manifest match.
Author-local IDs are validated before compilation, then every internal case, site, area, event, proposition, witness, evidence, action, objective, custody object, hostile group, finale, and bridge reference is deterministically rewritten beneath the newly minted case scope. Persistent settlement NPC IDs and catalog IDs are never rewritten. This permits the same definition to be spawned repeatedly without colliding in globally keyed authority tables. Starting custody is authored as exact object/site tuples; its asset-or-subject kind is derived unambiguously from objective leaves rather than from the finale.
Creating a debug quest inserts only latent open world authority. It deliberately does not grant a rumor receipt, referral, journal entry, destination pin, or any observer knowledge. Players discover it through the same tavern/NPC rumor path as automatically generated trouble.
Security limitation: developer mode is UI hiding only. It is off by default and stored in the browser, but there is currently no developer credential or reducer authorization. A caller able to reach strategic-web or invoke
spawn_developer_questdirectly can use the tool. Add server-side authorization before exposing it to an untrusted deployment.
The editor is complete for the declarative surfaces represented by
GeneratedCase. Closed engine mechanics remain Rust-owned and are exposed as
schema options. Tactical encounter execution and tactical enemy authoring are
outside this editor; hostile identity, count, and site are configurable while
the existing tactical/autoresolve systems consume them.
Authoring catalog
Modular quest and bestiary content is authored in the strict JSON-compatible
subset of YAML under content/quests/. Files are read in sorted path order,
validated during the adventuresim-core build, embedded in the executable,
and parsed into an immutable catalog once at process startup. Production does
not read loose YAML. The SHA-256 digest of filenames and exact source bytes is
the generated-case catalog revision, so changing authored content creates a
new deterministic replay boundary.
bestiary.yaml owns current monster names, aliases, combat values,
loot/loadout identifier, innate resistance and padding, behavior, habitats,
descriptions, clues, ambiguity links, and preparation text.
investigation.yaml owns evidence portraits, inspection topics and
creation-time DC ranges, witness demographics and circumstances, descriptions,
sites, and rare bridges. generation.yaml owns templates and separate
plausibility/curation weights, including explicit hard zeros.
Quest files may also declare dialogue_variants: bounded inert templates for
generated referral prose. Variants reuse dialogue's typed
condition tree and highest-priority selection semantics. They are presentation
only: no variant can change canonical facts, reliability, recipient, route,
or eligibility. Templates are rendered only from server-supplied values. The
quest compiler records the exact template scalar source span, so generated
referrals use the existing developer-mode edit link to the
selected quest YAML. Contract and finale exchanges remain ordinary compiled
dialogue content; use content/dialogue/ for those surfaces.
Generated testimony does not use a parallel quest prose wrapper. Its authored
spoken_text and exact challenge_text are expanded by a generic dialogue
response's typed testimony binding into structured resolved fragments. This
preserves punctuation and duplicate surrounding text without browser
substring reconstruction. Volunteered and socially released withheld drafts
use the same emitter, and private claim authority is attached to the exact
emitted event sequence and event-local claim order before the gateway can
project an opaque challenge token.
Run cargo run -p adventuresim-core --bin questgen-check -- validate after an
edit. Build-time embedding, process startup, and the authoring checker use the
same exhaustive validator. It rejects unknown fields, duplicate or overlong
IDs, dangling references, incomplete relation coverage, ambiguous witness
rules, invalid closed-mechanic names, malformed evidence DCs and weights, and
unsupported template graphs. Diagnostics include the source file and
structural path. Open catalog IDs are limited to 63 ASCII identifier bytes.
Current typed adapter boundary
Threat, site, witness-demographic, circumstance, ambiguous-description,
physical-evidence, bestiary-trace, and causal-bridge identities are bounded
open string IDs. A new value using existing mechanics therefore requires YAML
only. Closed Rust enums remain only
where the engine must execute a finite mechanic: rig topology, attack style,
protection, temperament, activity period, terrain, evidence check attribute,
reliability behavior, route/action and objective operation, settlement symptom,
and encounter archetype. Adding a new value to one of those finite mechanic
vocabularies requires Rust.
shattering_blow is a preparation hypothesis backed by the existing physical
resistance/padding model; it does not introduce a damage multiplier.
Typed investigation lists, aliases, regional priors and loadout IDs are embedded. Tactical materialization currently consumes only one loot item ID; it does not support multi-item authored loadouts, ability scripts, bespoke AI state machines, or new rig/animation topologies. Behavior currently reaches combat through temperament, perception, stealth, morale, movement, attack, ranged precision, encounter scale, and physically based innate resistance and padding. These are the factual incomplete bestiary surfaces for this change.
The first catalog boundary does not yet interpret arbitrary authored graph programs. Reliability semantics (truthful, mistaken, evasive, deceptive), account-style behavior, route/action execution, finale/objective execution, symptom-to-settlement-effect calculation, and incident construction remain closed engine mechanics. Their available IDs and primary selection weights are declared in YAML, but adding a new executable semantic requires Rust. Likewise, template declarations select the route/objective set, cause-to-finale mapping, consequence profile, incident interval, and incident ceiling persisted in each generated manifest. The two supported route/objective graph shapes still have typed Rust assemblers, and startup rejects any other shape. This prevents content from becoming executable server code and is the principal incomplete quest surface of this PR.
Witness demographic selection is also authored. A match rule may constrain
the NPC facts age_band, sex, profession, and local_role; empty lists are
wildcards. Age bands are child, adolescent, adult, and elder, and sex is
female or male. Profession and local-role selectors are lowercase
identifiers matched against either the complete generated NPC fact or one
whole alphanumeric token in that fact; arbitrary substrings never match.
Selectors must match the finite NPC fact vocabulary known at startup. Higher
priority wins. Exactly one selector-free fallback is required, and
equal-priority rules belonging to different demographics may not overlap under
the same matching function used at runtime. Fallback priority is ignored: the
fallback is consulted only when no non-fallback rule matches.
Generated quests are deterministic typed case manifests assembled from shared modules rather than scripts with substituted nouns. The initial catalog has two families:
RecurringDepredationinvestigates repeated attacks, locates one bound hostile group, and resolves it through the strategic mission outcome seam.DisappearanceOrLossuses physical and social routes to locate the same person, asset, or false claim, then rescues, retrieves and returns, or exposes it according to the canonical cause.
The family does not determine the answer. Threat, site, witness demographic, circumstance, report description, reliability, evidence, and route are separate variables constrained by typed relations. Descriptions reuse the shared bestiary and deliberately fit several threats.
Weighted constraint model
Every candidate relation has separate positive integer plausibility and curation weights. Authoritative selection uses bounded integer arithmetic, stable candidate IDs, and deterministic domain-separated entropy. Zero means impossible. Low positive weights remain possible, but sufficiently unusual combinations must name a typed causal bridge. A selected bridge materializes a canonical event, discoverable evidence, and a playable lead. Each bridge authors the existing action that emits its evidence separately for both supported template families; startup rejects missing families or action names that their typed assembler does not emit. A selected bridge from any generation relation is carried through to this materialization step. Evidence relations are the one explicit exception: startup rejects bridges there because the follow-up evidence selector has no case-graph materialization context.
The solver uses deterministic weighted candidate order, forward rejection, and backtracking under a hard node budget. Its private trace records factors, hard-zero reasons, required bridges, forward rejections, accepted choices, and backtracks. Static typed catalogs are intentional: do not add runtime closures, floating-point authoritative draws, duplicated inverse tables, or unbounded dynamic rules.
Manifest invariants
adventuresim-core::quest_generation::GeneratedCase contains canonical and
public identities, cause and events, consequences, sites and areas, persistent
NPC witness bindings, proposition testimony, evidence, typed investigation
actions and outputs, DNF objectives, custody, hostile groups, finales, dialogue
producers, bridges, and the private replay trace.
Validation fails unless:
- there is exactly one true finale site;
- recurring cases begin with one exact referred-contact action, which unlocks inactive approach and watch routes only after that contact succeeds;
- disappearance/loss cases retain independent physical and witness roots;
- every route can disclose the same site with a typed exact output;
- targets name materialized sites, areas, or persistent NPC authority;
- unusual selected relations have their event/evidence/lead bridge;
- every selected objective leaf has a concrete owning producer;
- disappearance objectives match the canonical cause and both terminal routes converge on the same person or asset;
- recurring combat alternatives name the same hostile group;
- every rare bridge names the exact reachable action that emits its exact evidence authority.
Unsupported cause/finale combinations are not selectable. Voluntary disappearance is excluded until locate/testimony/report producers exist, and generated templates do not select negotiation or capture without an owner.
Persistence and privacy
Settlement generation writes the case, local problem, investigation authority,
evidence, action graph, custody, hostiles, finales, and private
quest_generation_authority in one reducer transaction. The latter stores the
seed, catalog revision, input snapshot, full manifest, and trace for replay. It
has no public table accessor. Gateway projections expose only symptoms and
observer-owned knowledge; browsers never receive canonical causes, traces,
undiscovered evidence, true/decoy status, or hidden coordinates.
Persistent settlement NPCs also cross that boundary through a dedicated
BackendSettlementNpc projection. The projected row contains the NPC's stable
identity, home settlement, player-visible appearance, profession, household,
local role, service, and conversation identity. It deliberately excludes the
authoritative NPC's private sex and internal projection_id. Gateway-side
developer quest previews use visible age, presentation, profession, and role
for witness discovery. Preview, authoritative developer compilation, and later
victim-profile target validation share one visible-field candidate and
presence-commitment scheme. The scheme records presentation in that commitment
but leaves the candidate's sex selector empty for every presentation; it never
infers private sex from Man, Woman, or Ambiguous. Automatic procedural
generation remains a separate authoritative path and may use private
demographic truth.
The initial manifest remains immutable, but an unresolved generated problem
may acquire append-only follow-up incidents as authoritative world time
advances. At the template's authored interval, settlement activity materializes
the next due incident with a stable (case, ordinal) identity, occurrence time,
persistent NPC witness and victim binding, circumstance, existing case site,
canonical event, and new physical-evidence authority. Delayed refreshes
deterministically catch up every missed incident, with at most sixteen incidents
materialized by one reducer transaction. Fresh evidence is selected through the
same cause-and-site likelihood table and hard zeros as initial evidence.
The authored incident ceiling and thirty-day lifetime still govern non-hostile
families. A hostile RecurringDepredation instead remains active and continues
scheduled incidents until resolved. NPC adventuring companies and recruitment
offers remain, but companies no longer investigate, mitigate, or resolve
neglected cases automatically.
Every bestiary threat authors an escalation mode, growth rate, normalized
baseline_combat_power, and investigability. 10000 power is one unscaled
baseline orc. Higher investigability makes route actions,
physical inspection, and Bestiary interpretation easier. Mob escalation adds
bodies; single-entity escalation increases difficulty without multiplying
bodies or drops. Both derive progress from the incident ordinal using the
integer asymptotic recurrence. Count and per-enemy combat scale are monotonic;
normalized group power is
ceil(count * baseline_combat_power * combat_scale_bps / 10000) and is capped
globally at 300000, thirty baseline-orc equivalents. This makes a strong solo
and a weak mob comparable instead of granting every species its own thirtyfold
ceiling. Valid authored baselines are 1000..=300000; therefore per-enemy
combat scale is independently bounded at 3000000 basis points, the scale a
single minimum-baseline threat needs to reach the global power ceiling.
Tactical physical attributes and limb health consume
sqrt(combat_scale_bps / 10000) once, while damage-relevant training consumes
combat_scale_bps / 10000 once; autoresolve uses the same physical multiplier
and has no low rating plateau. Mob loot remains per body; solo growth retains
baseline drop quantity. A hostile group persists its immutable base count,
difficulty, and per-enemy normalized power. A mission snapshots group version,
count, base difficulty, combat scale, normalized power, and loot when it binds,
so later incidents affect future missions without rewriting a pending or active
one.
Incident authority is private. A character who already knows the problem can receive a dry local report when rumor circulation next reaches them; an uninformed character receives no incident history, witness identity, evidence, or location. Follow-up evidence remains undiscovered merely because its hidden authority exists.
Each follow-up also advances private public notoriety toward the threat's
investigability * 100 cap:
next = current + ceil((cap - current) * 3500 / 10000). At 6500 the case
becomes publicly known. A cap below 6500 can never cross, so elusive threats
remain investigation cases while conspicuous threats such as orcs and
skeletons eventually become combat-only problems. Catch-up records the
scheduled occurrence minute of the first crossing incident, never the later
refresh minute.
Once public, an authorized conversation grants only canonical threat type,
exact true site, and the current approximate count band (one, a few,
several, a warband, or a horde). It grants no evidence, testimony,
manifest, traces, or preparation advice and supersedes incorrect location
leads. The first referral also upserts one observer-safe journal entry and an
exact pin under the public case alias; repeat inquiries refresh its count band
without duplicating the case or journal row. Innkeepers at the afflicted and
adjacent settlements always qualify. Beyond adjacency, one listener-centric
shortest-road traversal is bounded to 4096 visited states, 16384 inspected
edges, and the maximum 18-by-25-km hearing distance. Cheap local graph scope
filtering happens before a deterministic 32-case manifest-validation cap, so
remote old cases cannot starve nearby cases. Missing or disconnected map nodes
fail locally and safely. Dues-current members may also ask the exact persistent
representative of an authored chapter whose organization explicitly enables
public threat referrals; membership uses the observer's authoritative
character clock and presentation is irrelevant. Both sources share the same
observer-scoped disclosure function.
Generated physical evidence has its own observer-facing presentation rather than speaking through an NPC. At an exact, occupied case site, each visible object is presented as a portrait with an italic initial observation and clickable inspection topics for particular parts of the object. A topic may be irrelevant, or it may test eyesight, intelligence, or instinct and reveal the object's clue.
Inspection is an authoritative fixed-threshold comparison. The generator assigns each checked topic a hidden, deterministic difficulty when it creates the evidence; inspection never makes a random roll. A character therefore gets the same result on every retry unless their relevant attribute changes. The browser receives topic IDs, labels, and observed narration, but never the difficulty or the character's compared value. Repeated attempts are allowed, and only the first successful discovery records the clue in the journal.
Each accumulated incident increases the unresolved problem's trade, encounter, and disease consequences by 25 percent of their initial values, before the existing global safety caps and mitigation are applied. At the currently authored five-incident ceiling, consequences are twice their initial severity for non-hostile problems. Recurring hostile consequences continue to use the existing global safety caps. Resolving or fully mitigating the linked problem still suppresses all of those effects.
The private authority also stores a domain-separated SHA-256 commitment to the exact serialized generation context, including observer-ID entropy. Every authoritative consumer validates that commitment, row identities, seed, catalog revision, factor trace, settlement scope, and the core manifest invariants, then deterministically regenerates the complete manifest from the stored context and requires exact equality. Observer IDs and generated state are derived only after this validation; malformed or stale authority fails closed instead of becoming manual behavior.
Canonical case IDs are used by objective authority. Journals use a separate public case ID. The reducer samples a separate 128-bit observer-ID secret and persists it only in generation authority. SHA-256 domain-separated IDs for actions, witnesses, propositions, capabilities, leads, and outcomes are minted from that secret; none embeds or is reproducible from canonical or other browser-visible identifiers. Action resolution seeds are independently sampled and persisted when capability authority is issued or revised. Exact retries are idempotent, while a new version receives fresh entropy and a version-separated roll.
Every private investigation capability also persists immutable provenance: manual, or generated with its canonical generated-case identity. Generated capabilities never fall back to manual behavior when authority or output rows are missing. Projection, recovery, and execution reconstruct the opaque capability ID from the private generation context and require its method, target, terrain, prerequisite, alternate, summary, consequence, and typed outputs to match the immutable manifest. Canonical and public case aliases are resolved only through private indexed authority, with collisions rejected. Private case/objective authority carries the same explicit manual-or-generated provenance. Dialogue eligibility and execution share one validator: only an explicitly manual case may use its current-NPC fallback, while a generated case requires its immutable manifest, context, objective expression, and authored dialogue producer to remain intact.
Discovery and resolution
Entering an inn guarantees symptom discovery when an available unknown
validated generated quest problem exists. This records the rumor in the
player's dry journal and grants a private referral authority; it does not
accept a contract or disclose testimony. Legacy/manual
seeded LocalProblemAuthority rows currently drive settlement simulation
modifiers and effects only and are intentionally non-discoverable; retiring
that producer is separate work. Any local can repeat a validated generated
rumor and name the referred persistent NPC by visible description, profession,
and expected location tab. Testimony is issued only when the addressed NPC is the bound
witness. Corrections reuse the proposition they revise, preserving an earlier
false believed pin until the correction is learned.
Witness discovery is an explicit authored graph, not permission inferred from private manifest membership. The initial rumor grants an observer-bound referral to the primary witness. Individual testimony drafts may refer to exact subsequent witnesses; only processing that account grants the next private referral. The journal intentionally does not project that referral, its location, or any suggested next action; those details remain in the dialogue the player actually heard. Referral execution revalidates observer, canonical/public case authority, NPC, settlement, location, catalog revision, dialogue session, and live presence. Private referral authority records whether it came from the exact initial rumor receipt or an exact source witness and testimony draft; every use regenerates the manifest and revalidates that provenance and its one authored edge. Missing, cyclic, duplicate, or unreachable route-required witness edges fail generation validation. Secondary testimony with no authored contact action changes no route, while the primary contact still uniquely unlocks its successors.
Only an authoritative typed exact destination output can create an exact pin.
The raw site ID remains navigation authority; the matching map pin uses the
site's generated safe name. Witness-described sites use a neutral attribution
such as Place Anna Weber described; labels never comment on whether the
account is plausible or confirmed. The journal does not interpret or restate
the pin as a destination.
Discovery actions execute at a known contact, settlement, area, or predecessor
route and may reveal travel-capable knowledge. They never also resolve custody.
After travel establishes authoritative occupancy, a separate InspectSite or
LayAmbush action may resolve custody or prepare the finale. Retrieve and
rescue outputs use the versioned custody producer. Return and
expose use compiled generic dialogue responses, pre-issued bindings, the exact
generated recipient, and one-use consumption. Cases and linked local problems
may resolve without a contract.
Every generated investigation route makes bounded persistent progress without replacing its native skill check. This includes physical searches and tracking, contact-finding and approach actions, observation, patrol, and ambush routes. Each attempt retains its ordinary skill-based chance, time, supplies, fatigue, and risk, while contiguous failures on the exact same observer capability raise the deterministic threshold until attempt six is guaranteed. Capability, observer, method, or version gaps reset that progress; manual investigation actions remain unbounded. Attempt authority and journal wording snapshot the threshold, uncertainty, and six-attempt bound at resolution time. When exact destination knowledge is corrected, dependent progress is matched through validated private canonical/public case aliases. Unsupported routes receive a fresh version and seed; an exact replacement that still supports the same route preserves its contiguous work.
Attack patterns are playable modules rather than private flavor. Initial
surveillance remains pattern-neutral. Success earns one exact corroborated
pattern proposition; only then does the dependent action disclose and enforce
its nighttime window, roadside route, authored victim profile, or broad
schedule-free search. Unreliable testimony may contradict that proposition
until the evidence is learned, and the dependent capability requires knowledge
of the exact evidence ID and successful completion of its authored predecessor,
rather than inferring from the canonical event. Projection, failed-route
recovery, and execution all apply those same live-support requirements.
For a corroborated nighttime condition, the player-facing action projection
applies the same day/night gate as reducer execution. During daytime it exposes
the stable night_window reason code and an exact bounded wait_minutes until
minute 1200 of the current day, derived from public world time and the
furthest-advanced living party clock. It does not project the private event,
cause, or unlearned pattern authority.
Victim-specific patterns bind an opaque cohort reference to one persistent
settlement NPC in private authority, including their authored demographic and a
versioned presence fingerprint. The learned clue exposes only legitimate
demographic, physical, and referral details; patrol and ambush execution
revalidate the NPC's identity, profile, location, and availability immediately
before resolving the action.
The player-facing action projection applies the same current cohort-authority,
case-binding, NPC, presence, expected-place, profile, demographic, presence
version, and schedule predicates. If any no longer holds, the action remains
visible but unavailable with the single observer-safe target_changed reason;
the projection never identifies the NPC or reveals which private predicate
failed. This keeps alternate public actions usable while preventing a
permanently invalid cohort capability from remaining actionable.
Generated cases create no Contract rows. Tavern discovery and NPC referrals
are their entry points. Settlement activity counts open generated cases
directly and immediately replenishes resolved generated problems independently
of contract acceptance, tracking, reporting, or payment.
Recurring finales use the existing strategic mission authority from #217.
Pending Defeat and DriveOff leaves for the generated hostile group become
weighted MissionOutcomeCandidate rows after exact observer-authorized site
entry. Investigation never fabricates a battle result and no tactical tick
state is persisted.
An observer can open a generated case site without a contract only after the observer-owned exact pin and authoritative party occupancy agree on that site. The location page uses the validated public problem summary and site label; it does not synthesize a contract or expose the private manifest. Available site-bound investigation actions continue through the normal authorized investigation reducer. Strategic autoresolve is offered only when the validated generated finale, a pending objective path, and an active hostile group all bind the exact occupied site. Battle results are joined back to the page by typed case-site authority rather than canonical/public case aliases. The observer-safe case-site projection also exposes whether the generated case has resolved, including noncombat finales that produce no battle result. A resolved site shows an explicit completion notice and no longer offers pre-finale rest controls. Manual bounty pages retain their accepted-contract and active-contract gates.
Developer tools
cargo run -p adventuresim-core --bin questgen-check -- validate
cargo run -p adventuresim-core --bin questgen-check -- explain 42 0
cargo run -p adventuresim-core --bin questgen-check -- audit 1000
cargo run -p adventuresim-core --bin questgen-check -- counterfactual 42 43
validate exercises both initial template ordinals, explain prints the
private manifest for development, audit reports family marginals, and
counterfactual compares stable high-level selections. Candidate domains,
persistent-witness input bytes, visited nodes, trace records, and trace bytes
all have production bounds before ordering, cloning, or serialization.
Investigations
Investigations are observer-specific knowledge, not a projection of canonical
quest truth. adventuresim-core::investigation models atomic propositions
through distinct perception, recollection, disclosure, transmission, belief,
revision, evidence, and lead stages. A witness can be sincerely mistaken,
omit one proposition, distort another, or later correct an account; there is no
witness-wide is_lying flag.
Privacy boundary
The SpacetimeDB module keeps case truth, canonical events, stage records,
evidence authority, generation explanations, beliefs, revisions, leads, action
receipts, and sharing receipts in private tables. Only
backend_investigation_journal, backend_investigation_leads, and
backend_case_site_pins are public views. They fail closed for identities
other than the registered strategic gateway and omit hidden threats, causes,
sincerity, undiscovered evidence, private NPC identifiers, weights, bridges,
and hidden coordinates. The trusted SSR gateway additionally filters every
view to the selected session character. A case-site pin is joined to private
physical authority only when that observer has an unrevised exact-believed or
visited lead; textual and approximate leads cannot reveal coordinates.
The local-problem integration consumes a character-owned private rumor receipt
and derives an observer-facing case ID from the public problem ID. It never
projects opaque_case_ref, consults the problem cause, or silently grants a
contact's current location.
Generated cases privately map canonical, investigation, and public journal identities. Dialogue eligibility accepts only those exact aliases plus session-relevant rumor, testimony, belief, evidence, or custody provenance; it never searches arbitrary cases the character happens to know.
Runtime testimony generation is a private production pipeline. Server-authored perception, memory, disclosure, and transmission stages are persisted as a private bundle, then issue a private, one-use, character-owned safe receipt. Receiving that receipt records exact observer-owned provenance for the claim and witness. The browser never authors or receives the hidden pipeline payload. Player actions can only consume an existing matching receipt; they cannot submit statement text, confidence, sources, or coordinates.
Compiled testimony patterns operate per proposition: an account may be truthful, mistaken, evasive, deceptive, or partly truthful, including an accurate event description paired with an omitted reason for being present. Follow-up eligibility uses only observer-visible claims, contradictions, familiarity, language/social checks, prior questioning, and possessed evidence. The reliability pattern, motive, and canonical event remain private.
Evidence authority explicitly classifies presentation as physical or
informational. Physical evidence requires a custody row currently held by the
presenting party or character; a missing row fails closed. Informational
evidence requires a private, source-attributed
investigation_evidence_knowledge receipt. The mere existence of hidden
evidence authority—or a belief concerning the same proposition—does not grant
knowledge or physical possession of proof.
Physical inspection and Bestiary knowledge
Physical evidence exposes only safe topic IDs and labels. Its base attribute thresholds, Bestiary thresholds, hidden case truth, and failed Bestiary checks remain private. Once the physical inspection succeeds, the reducer evaluates each authored atomic category implication against the inspecting character's effective Bestiary knowledge. It never substitutes the party's best expert and never consults the hidden threat.
Successful checks persist only observer-owned diagnostic-kind receipts and safe interpretations. A failed physical observation creates no diagnostic receipt. Generated testimony similarly creates an observer-owned report receipt only after the private manifest is revalidated and the testimony is actually received. Neither receipt can be supplied by the browser.
Whenever either input changes, the server calls the shared infer_threats and
qualitative_deductions functions using only that observer's received reports,
learned diagnostics, and the public regional context. It persists a deduplicated
set of possible monster kinds with strong, plausible, or weak support and
safe provenance. The journal and evidence conversation show those qualitative
candidates and their sources. Raw scores, basis points, percentages, hidden
thresholds, failed checks, canonical cause, and hidden evidence IDs never cross
the gateway. Identical visible report and diagnostic inputs therefore produce
identical deductions regardless of the hidden cause.
The SpacetimeDB module currently sets test = false, so ordinary Cargo tests
cannot instantiate a reducer database harness. Narrow pure tests cover action
receipt scope, canonical-row augmentation, duplicate suppression, stable
physical failure, success-only category filtering, and observer-safe
serialization. End-to-end reducer transaction behavior remains a live
database verification responsibility.
Sharing and navigation
Knowledge belongs to a character. Sharing a selected lead or belief is an explicit, idempotent action. The recipient must be living, in the same party, and at the same strategic location at that moment. Joining or rejoining grants no historical knowledge.
Destination knowledge advances from unknown through textual directions or a landmark, an approximate area or route segment, exact believed location, and observer-specific visited location. Only the last two stages may carry a pin. An incorrect exact account remains the observer's destination until a sourced correction revises it.
Generated corrections reuse the proposition ID they revise. A later witness or evidence receipt creates the revision and marks the earlier lead corrected; private materialization alone does not remove the observer's false pin.
Strategic destinations have stable case-site IDs independent of quests and
contracts. Character and party location, journey endpoints, camp continuation,
terrain planning, map links, and tactical scene selection all use the case-site
ID. A site's case_id may currently point to a legacy direct bounty, but
Quest contains no location, scene, coordinate, distance, or tracking fields.
Tracking is a private per-party presentation choice over an exact site already known by the leader. It does not disclose the site, accept or abandon a contract, move the party, satisfy an objective, start combat, or grant a reward. Travel independently revalidates the leader's observer-safe exact knowledge on every attempt and retry. Direct bounties seed a private site and explicitly disclose it when the issuer accepts the contract, preserving the prototype flow without making quest state the location authority.
Available-quest, quest-giver/service, issuer-route, and turn-in exclamation markers are absent. Exact case-site pins are knowledge projections, not quest markers. Reported exact locations and visited sites are labeled separately; an active-contract badge appears only when the party's active contract explicitly matches that case. Recruitment indicators are a separate social feature.
Threat inference
Player-facing ranking accepts only received descriptions, discovered evidence,
and visible regional priors. It delegates forward likelihoods and zero/rare
semantics to bestiary::rank_candidates_in_region; it has no inverse table and
cannot accept hidden ThreatId or case truth. Provenance names only inputs
already known to the observer.
Strategic investigation actions
Gateway projections correlate every investigation action and public outcome with the observer's public case ID. Generated canonical case IDs remain private. A public case summary carries an immutable subject established by its first observer journal entry; later leads and journal headlines update recency without replacing it in summaries or evaluator events. Dialogue topic options that advance a case carry that same public case ID (presentation-only topics carry an empty value), so clients can never apply a valid topic from one open case while attributing it to another. Public case battle rows similarly contain the observer character and public case ID rather than the canonical generated case ID; consumers must match observer, public case, party, battle, mission, and exact site.
Investigation opportunities are private, versioned capabilities issued by the strategic authority. The browser receives only an opaque action ID, method, version, safe description, prerequisites, costs, uncertainty, and contribution labels. Hidden case truth, target IDs, exact coordinates, deterministic seeds, success thresholds, and weights never enter browser state.
The initial action vocabulary is inspect site, search area, follow or reacquire tracks, locate a contact, watch, patrol, lay an ambush, and approach a lead. Rumors materialize all nine as two linked routes: witness-led search and observation-led interception. A recurring case begins with the exact referred contact; succeeding there unlocks inactive approach and watch branches. Disappearance/loss cases instead retain independent physical and witness roots. Issuance validates that initial frontier shape. Execution revalidates the immutable same-owner, same-case topology, while allowing the active frontier to advance to a single non-contact successor such as a patrol only when its authoritative predecessor attempt succeeded. Reissuing an already complete generated graph validates its stored blueprints and evolved frontier without reactivating initial roots; a partial generated graph fails closed. Successful actions unlock their successors, and failed actions reactivate a validated same-owner, same-case alternate. Failure text reports whether any other currently live-supported case route remains, including a patrol already supported by its exact clue. Approximate areas are private strategic geometry, not client-authored destinations. Resolution uses authoritative terrain, time of day, evidence age, relevant skills, bounded party assistance, observer familiarity, and strategic weather. Weather is queried from the actor's authoritative position and synchronized action start time; clients cannot submit it. Heavy precipitation penalizes visual field actions. Established snow can help track actions, while active snowfall partly offsets that help by obscuring visibility. Clear weather preserves the former resolution exactly, including the effective-skill clamp and generated route's guaranteed sixth attempt.
Quest generation commits incident-time precipitation inside its private
replayed GenerationContext. Rain and snow select PoorPerception (and a
clear NightWindow selects Darkness) only at the perception/confidence stage.
Reliability, deception, evasion, demeanor, memory, and disclosure mappings
remain unchanged.
Every attempt is idempotent and consumes strenuous strategic time, including a failed attempt. Failure may increase risk or uncertainty, but it does not silently invalidate alternate investigation routes. Approximate discoveries remain directions or areas. An exact map pin is disclosed only when an authoritative result supports exact observer knowledge. Journal and map-pin text use the site's safe generated name; opaque site IDs remain navigation authority and are not shown as destination labels. Watches, patrols, and ambush preparation remain strategic actions; they do not persist tactical tick state and cannot fabricate a combat result. Before spending time the reducer revalidates party readiness, co-location, journey and camp state, unresolved encounters, predecessor knowledge, position, and typed prerequisites. Party clocks synchronize first, with night defined as before 06:00 or from 20:00 onward. Browser estimates are broad method-derived duration ranges; exact terrain, needs, fatigue, success, and risk remain authoritative.
Location is revalidated at execution, not merely at issuance. Contact actions use the referred NPC's current settlement and presence window (or the same settlement as a bound ask-around action). Physical tracking chains progress from an area search through a route segment to a site. Every tracking edge must remain same-owner and same-case, its predecessor must have succeeded, and position validation recursively follows the coherent chain back to its area origin. The same rule gates issuance, observer projection, and execution, so an unexecutable successor is never advertised. Occupying another site from the same case counts only when its valid coordinates fall within that area's meter radius. Areas bind the origin settlement's coordinate mode: imported geographic worlds use great-circle meters, while abstract maps use the strategic-travel convention of Euclidean coordinate units as kilometers. Site and area modes must agree. Later site-targeting actions require actual site occupancy. Traveling elsewhere invalidates the attempt before time is spent or a lead is written.
Retrieve and rescue consequences re-read current custody and require the case objective, object kind, site holder, occupied site, and next version to agree. A purely stale version reissues the capability without spending time; a holder, site, or case mismatch fails closed. Investigation can discover, track, position, and prepare an ambush, but it never creates a mission, battle receipt, hostile disposition, drive-off fact, or capture fact. Authoritative non-kill tactical resolution begins only after authenticated combat succeeds. Strategic mission authority privately snapshots exact observer-authorized pending objective approaches and deterministically selects among compatible defeat, drive-off, and capture consequences at commit time. Investigation actions cannot select or invoke that result.
Quest authority
This page is the canonical technical reference for durable cases, objectives, contracts, local problems, mission and battle outcomes, NPC recruitment, and strategic incidents. These systems may interact, but none uses legacy quest identity as its authority boundary.
Cases, objectives, and contracts
World problems are represented by a private CaseAuthority. A case references
the corresponding private investigation case, optionally references a local
problem, and owns a typed objective expression and final resolution. It exists
independently of whether anybody offers or accepts payment for resolving it.
Objective expressions use disjunctive normal form: any alternative path may
resolve the case, while every typed leaf in that path must be satisfied.
Supported leaves cover defeat, drive-off, capture, survival, rescue, escort,
retrieval and return, locating, identification and exposure, proof and
testimony, protection, negotiation, release, exchange, and reporting. The
shared-core evaluator returns Pending, Satisfied, or Impossible and
retains per-leaf partial progress. An impossible path does not invalidate a
still-viable alternative.
Tactical servers never complete cases or pay rewards. Trusted battle commits
produce source-idempotent CaseOutcomeFact rows attributed to a case, party,
mission outcome source, and hostile group. The case evaluator rejects facts
from unrelated cases, parties, and hostile groups. A satisfied expression
records one CaseOutcome; if linked, the local problem receives the same
idempotent outcome.
A Contract is a separate private agreement. Acceptance assigns only the
contract and may disclose already-existing case information; it does not
create, delete, or resolve the case. Resolution changes an accepted contract
to ReadyToReport. Reporting at its issuer pays once, records paid_at, and
changes it to Paid. Withdrawing a contract leaves the underlying case and
investigation intact.
The modular investigation generator creates no contract. Its cases enter play through tavern rumors and NPC referrals, and their linked local problems resolve and replenish without acceptance, tracking, reporting, or payment. Legacy direct bounties may still create contracts; their presentation is deliberately an issuer belief, and canonical threat identity and count remain absent from the contract schema and gateway DTO.
Assets and subjects use one versioned custody row per stable object ID. A transition must advance exactly one version and carry a stable source ID; repeating the same source is idempotent, while stale, skipped, or cross-case transitions are rejected.
Cases, objective graphs, outcome facts, custody, contracts, sites, and
investigation truth are private strategic authority. The web process
subscribes to a trusted backend_contracts projection and combines it with
observer-specific investigation knowledge. Browsers never subscribe directly
to objective or hidden-truth tables.
Noncombat objective facts have owning-subsystem producers. Dialogue producers revalidate the selected character, party leadership, active session revision, persistent NPC presence, intended recipient, and observer knowledge. Locate, identify, expose, proof, testimony, and negotiation may advance a known case without accepting a contract; report-to-issuer additionally requires the session-bound active contract and exact issuer. Before exposing an eligible response, the server derives one exact case from session-relevant observer provenance and pre-issues a private session/case/objective binding. Effects only revalidate and consume that binding after the owning producer succeeds; they never search the character's other known cases. Each fact source includes the dialogue session, stable action ID, and objective ID, so retries are idempotent and distinct actions in the same minute cannot alias. There is no public generic fact or complete-objective reducer.
Objective producers
There is no generic reducer for applying arbitrary objective progress. Objective facts are emitted only by the subsystem that can validate the corresponding world event. Strategic investigation produces retrieve and rescue custody transitions; dialogue produces return, release, and exchange; case-site arrival produces escort; authenticated tactical completion produces defeat, drive-off, capture, or capture-target-killed; and strategic continuity guards produce survive and protect only after an uninterrupted deadline.
Each producer binds the expected case, party, target, hostile group, custody version, and stable source identity as applicable. Replays are idempotent, cross-case or stale-custody attempts fail, and terminal destruction or death marks only affected objective leaves impossible. Alternative branches remain available until the objective expression itself can no longer be satisfied.
Mission creation selects one eligible unresolved hostile approach rather than
choosing by objective precedence. The current kill-based tactical server and
autoresolver select Defeated. Investigation actions may prepare an ambush or
establish awareness, but they cannot emit DrivenOff or Captured; those
objectives remain pending until #207 adds an authoritative tactical producer.
Every shared hostile-result commit rechecks its selected resolution.
CaptureTargetKilled is never a successful result, and only Defeated may
produce battle loot.
Recurring generated cases bind Defeat and DriveOff alternatives to the same
hostile-group/site identity. Once an observer knows and enters the exact site,
the existing #217 mission seam materializes those leaves as weighted
MissionOutcomeCandidate rows; generation adds no parallel combat resolver.
Local problems
Local problems are persistent strategic conditions which affect settlements and roads before a character knows their cause. They are independent of legacy quests, contracts, rewards, and tactical tick state.
Authority and privacy
adventuresim-core::local_problem owns deterministic weighted generation,
absolute-time lifecycle evaluation, stable aggregation, caps, and checked price
adjustment. Relations have separate plausibility and curation weights. Zero is
impossible; rare relations may require a causal bridge, whose key is emitted for
later evidence authoring.
Generated quests use adventuresim-core::quest_generation to select canonical
truth and atomically link the case to this symptom authority. The local problem
stores the consequence mechanism, not a parallel monster answer. See
Quest generation and investigation.
Cause, disease identity, encounter archetype, opaque case reference, weights, bridges, generation entropy, explanations, and per-problem consequences are private. Public rows contain only observable symptoms. The authenticated strategic gateway receives character-scoped aggregate trade pressure and private rumor deliveries through gateway-filtered views; browsers cannot subscribe to either authority rows or per-problem consequence fingerprints.
Lifecycle and consequences
A problem has an absolute interval, monotonic mitigation, and an optional earliest resolution minute. Generation uses private entropy, and expired or resolved history does not prevent a later replacement. At most three active rows contribute to a scope, in stable ID order, with aggregate caps. Resolution also closes the public symptom interval.
- Trade pressure is applied after base and language pricing by the same checked integer basis-point function used for UI quotes and reducer settlement. Food lots are included; merchant catalog stock remains infinite.
- Route problems use a canonical sorted endpoint pair and affect only existing canonical encounter boundaries. Entropy domains, retries, chunking, and impossible habitat weights are preserved.
- Disease pressure reuses
first_eligible_presence_exposure_minutewith the stable problem ID as exposure source. Disease identity remains private until diagnosed.
The internal outcome seam accepts an idempotent source ID, authoritative minute, monotonic mitigation, or resolution. It is not a public reducer and cannot accept contracts, complete objectives, or pay rewards.
Markerless discovery
Dialogue with an available inn NPC surfaces one unknown unresolved problem. Overview dialogue does so only where no inn NPC is available. Safe rumors give the symptom and refer to a persistent NPC by name, visible description, occupation, and expected location tab. A private per-character receipt records the source and opaque case reference. Rumor text is delivered through a private session-scoped row and is merged into the authenticated SSR response; it is never written to the public dialogue-event table.
Later conversations with ordinary locals give a short summary and referral instead of replaying discovery. Discovery does not accept or mutate a quest, reveal a cause or destination, or create a map marker.
The first inn conversation records discovery only. Even if the innkeeper is also a generated witness, it does not deliver testimony in that session. The character must later address the bound referred NPC; selecting another local cannot reveal that witness's account.
Mission and battle authority
Combat authority is independent from contracts and legacy quest identity.
MissionIdidentifies one requested tactical or autoresolved combat opportunity.HostileGroupIdidentifies the particular persistent group occupying a case site. A random encounter is explicitly unbound; matching its species and headcount cannot defeat a case site's group.BattleIdidentifies one finished combat attempt.OutcomeSourceIdis the authenticated idempotency key for the strategic consequences of a victorious battle.
Private mission_authority rows bind a mission to its party, observer, exact
case site, case, hostile group, and scene. Case missions snapshot a private,
immutable set of exact mission_outcome_candidate rows derived from
observer-authorized mission_approach_capability rows. Each candidate names a
pending path and objective, compatible resolution, weight, and, for capture,
the exact subject and custody version. Capabilities require exact believed or
visited site knowledge; their IDs and weights have no public projection. Private
hostile_group_authority rows are materialized when a case site is created
and own enemy composition, immutable drop manifest, and defeated state.
Mission creation reads only these authorities, never a quest. Public tactical
views contain party-safe presentation data and never expose the case-site or
hostile-group binding.
The authenticated dispatcher uses the registered strategic-gateway identity. For each request it generates a 256-bit claim, stores only its SHA-256 digest through a gateway-only reducer, and waits for reducer success before spawning. The raw claim is passed only in the tactical child's environment; the full gateway token is explicitly removed. The child consumes the matching private claim exactly once when registering its server identity. Claims are never stored in public rows or command-line arguments. If process creation fails, the dispatcher revokes the still-pending claim so the request can be retried. A child that exits after process creation but before registration still requires cancellation or dispatcher restart; durable child supervision is a follow-up operational improvement.
Tactical servers keep positions, health, enemies, and per-tick simulation
transient. Their completion enum is only a compatibility transport: Failed
means failure, CaptureTargetKilled is explicit contradictory terminal
evidence that also fails without sampling, and the other values are the same
opaque authenticated success signal. Tactical requests and servers contain no strategic approach,
objective, subject, weight, or expected-result field. On success, strategic
authority revalidates the prebound candidates, canonically sorts them, and
performs a deterministic SHA-256-derived weighted draw from private
server-generated mission entropy. Caller-selected mission IDs therefore cannot
grind outcomes, while retries reuse the persisted entropy and select the same
result. Stale capture custody removes that
candidate; if none remains, the attempt fails without fabrication. Allied
autoresolve victory uses the same sampler.
The strategic commit validates party, mission, site, hostile-group, objective,
candidate, and capture custody attribution and inserts one private
outcome_source_authority receipt before writing the public battle result,
participants, and loot. Tactical drops come from the immutable hostile-group
manifest, not temporary enemy equipment. Only Defeated can mint group drops
or random gold. DrivenOff emits its typed fact without loot. Captured
atomically transfers the exact subject from the bound site and custody version
to the party and emits SubjectCaptured, also without loot. Success revokes
sibling approaches for that group and site. Replaying a source is a no-op, so
it cannot duplicate facts, morale, custody, loot, or reward shares.
Failed, cancelled, stale, defeated, and stalemated attempts are terminal under their mission ID. Defeat and stalemate retain only the bounded autoresolve diagnostic report and condition consequences; they do not create a strategic victory outcome or resolve a hostile group. A retry requires a new mission ID. Pending or active sessions for the same group remain mutually exclusive. Random encounters have no case manifest and remain defeat-only.
Legacy bounty completion is currently a downstream projection from a newly defeated bound group to its case. It is not an input or fallback for mission, battle, outcome, or loot authority. The generalized case/objective work replaces that final projection.
Recruitment and incident authority
Recruitment and strategic incidents are independent world systems. Neither is
a contract, objective, or legacy Quest.
NPC recruitment
An NPC company is advertised by a stable RecruitmentOfferId. Its
RecruitmentSourceId is the deduplication identity for the settlement/company
source. The offer owns the recruiting party, leader, settlement, creation and
expiry minutes, and an Open, Closed, or Expired lifecycle.
Settlement activity population creates recruiting companies and their ordinary party recruitment roles without accepting or creating a quest. Requests and acceptance revalidate the open offer, expiry, leader, role capacity, and co-location inside one reducer transaction. Repeating the same pending request is a successful no-op. Player-authored general recruitment roles remain independent and need no NPC offer.
Each generated company is anchored to a persistent settlement NPC and that NPC's scheduled observable presence. The offer source derives from the stable NPC identity; stale party, leader, settlement, or presence bindings close the offer. The service API returns recruitment companies separately from quest contracts, so an inn can surface a company when no quest posting exists.
The public offer contains only social presentation identity. It has no investigation case, witness evidence, hidden cause, or threat data.
Strategic incidents
IncidentId, IncidentSourceId, IncidentKind, and IncidentStatus form a
private strategic authority. The source ID is the retry/deduplication key. Each
incident owns its party, instigator, settlement, case-site binding, hostile
group binding, creation time, and lifecycle.
An incident uses the normal case-site location authority and mission/hostile
group authority, but it does not create a quest or contract. Starting one moves
the party to its incident site without changing Party.active_contract_id.
Leaving that site marks the incident avoided. A victorious tactical or
autoresolved mission matches the exact hostile-group ID and marks it resolved
before any legacy quest projection is considered.
Departure synchronization permits retreat only when the sole pending incident is the incident at the party's exact departing site. Incidents created at a different location, or multiple inconsistent pending incidents, invalidate the stale journey request. Activity incidents derive their source identity from the persisted activity occurrence minute rather than the random occurrence roll.
Consequently, an unrelated battle or incident can never complete a quest, and resolving or avoiding an incident cannot mutate quest/objective state. Tactical positions, health, damage, and tick state remain transient.
Combat
Strategic autoresolve uses shared bestiary combat profiles. Skeleton bone is a full-coverage innate protection layer with substantial resistance and no padding, making blunt attacks substantially more effective than cutting attacks through the normal force, resistance, padding, coverage, and penetration model. There is no species-level post-hoc damage multiplier. The tactical server does not yet receive canonical bestiary identity, so not every profile field changes real-time behavior; tactical enemy state remains transient. Combat is the solemn duty of any good knight or mercenary, and until we have a working fashion module, it'll be what players spend most of their time doing. So let's get it right!
Attacking
When the player clicks the Attack button, initiating an attack animation, we run a shapecast in front of the player character. If there is an intersection between the attacker's hitreg and some other actor's hitbox, we calculate input precision. Then comes the skill check algorithm.
Skill check algorithm
Broadly speaking, the flow goes like this:
- Calculate accuracy based on:
- The attacker's weighted weapon skill check. Each weapon distributes its check across Polearm, Axe, Bludgeon, Sword, Knife, Bow, Crossbow, Firearm, and Throw; hybrid tags are normalized.
- pass in LimbWeights configured for whatever limb(s) they are attacking with
- If they are two handing, 0.75 for main and 0.25 for off-hand
- Multiply by weapon term (small knife: 2.0, long hammer: 0.5)
- Multiply final value by input precision
- The attacker's weighted weapon skill check. Each weapon distributes its check across Polearm, Axe, Bludgeon, Sword, Knife, Bow, Crossbow, Firearm, and Throw; hybrid tags are normalized.
The weapon term also provides the strategic recruitment weapon precision scale. The current discrete recommendations are 0.5 for clubs and hammers, 1.0 for axes, 1.5 for ordinary swords and spears, and 2.0 for purpose-built precise weapons such as rapiers or bodkin ammunition. Damage type is not a recruitment role: slash, pierce, and blunt weapons are compared through this single precision scale instead.
2. calculate dodge_defense:
1. Calculate armor_dodge_term from their armor.
1. This isn't actually the weight of the armor; it's based on articulations on joints.
2. Full-plate gives 0.6, full-body chainmail is 0.8, and unobstructed joints is 1.0.
2. Calculate encumbrance_term from total weight versus leg-strength
3. Multiply a dodge skill_check by armor_dodge_term and encumbrance_term
1. LimbWeights should be something like 0.4 for each leg and 0.1 for each arm
3. calculate block_defense:
let side = // set to whatever side is holding shield
block = defender.skill_check(block, Some(LimbWeights { la: 1.0, .. }.flip(side))
shield = defender.shield_bonus()
`shield_bonus()` = 0 for weapon; 1–2 for a small shield; 2–4 for normal; 5 for pavise
$$ \mathrm{defense}(\mathrm{shield},\mathrm{block}) = 5 \cdot \left(1 - e^{-\tfrac{\mathrm{shield}+\mathrm{block}}{2}}\right) $$
-
Calculate
defensefrominput reflex:if defender is parrying: defense = block_defense * 2 * input_reflex elif defender is dodging: defense = dodge_defense * 1.5 * input_reflex else: defense = block_defense -
Modify defense by flanking penalty
- a is the angle that the attacker is facing and b is the angle that the defender is facing
- In layman's terms, you have zero defense if someone attacks from behind, full defense if they attack from in front, but the modifier starts at 1 below 45 degrees and is 0 at 135 degrees, rather than at 0 and 180
$$ D_{\text{final}} =D_{\text{base}} \cdot\mathrm{clamp}\left(\frac{\frac{3\pi}{4}-\left|\mathrm{atan2}(\sin(b-a), \cos(b-a))\right|}{\frac{\pi}{2}},0,1\right) $$
- Attack value is accuracy - defense
- If attack is less than 0, miss and apply surplus defense as unbalance penalty to attacker
- If attack is between 0 and 1, multiply attack force by attack
- 0.1 barely grazes the opponent, 1 is square-on, 0.5 is a glancing blow
- If attack is above 1 and the attacker's weapon is precise, attacker now attempts to bypass armor with surplus attack.
- An armor's "coverage" is subtracted from the surplus attack to obtain the "critical attack"
- If critical attack is greater than 0, attack bypasses armor completely and its final damage is multiplied by this number
- Though not necessarily relevant for the MVP, critical attacks are relevant even when targets are unarmored because this allows the damage multiplier to exceed 1.0, allowing for instantaneous stealth one-hit-kills.
- If a critical hit cannot be made, then attack just stays at 1.0 for a direct hit
Ranged attacks
Ranged attacks use the same attack-minus-defense exchange, armor coverage, penetration, padding, and critical-hit rules as melee attacks. The attacker's Bow, Crossbow, Firearm, or Throw distribution supplies the weapon check, both arms contribute to aiming, and the weapon's projectile energy replaces muscular striking force. Focus adds the character's Weapon accuracy and future input precision affect attacks; neither is a character attribute. Agility governs physical-skill learning and mastery.
An alert defender may dodge a projectile or interpose a shield using the normal Dodge and Block checks. An unaware defender has no active defense. A missed projectile does not unbalance its attacker. Current projectile energy defaults to 40 joules per kilogram of ranged weapon, giving the one-kilogram short bow a 40-joule baseline until ammunition carries its own mass and velocity.
Incapacitation
A character's incapacitation represents the sum of all disabling effects on them and corresponds to the state of their animation. When above half, they are "staggered" and each additional 1% of incapacitation causes a 2% penalty to movement and attribute checks, and when above 100% they are completely incapacitated (which also causes knockdown). Most negative effects that a character has can affect their incapacitation, past a certain threshold. Your incapacitation is displayed as a wheel in the center of the screen. If it is at 0%, the wheel is invisible, and as it increases it starts from 12 o'clock and extends as an arc clockwise. Each factor that contributes to incapacitation has a different color to differentiate them.
The strategic character panel uses the same colors for its segmented incapacitation meter, source meters, and source icons. Hunger and thirst share centered meters with their physiological reserves: reserve fills right, while incapacitation fills left after crossing zero. Exact percentages remain available on hover and to assistive technology, while the default view emphasizes the relative contribution of each source.
Each of the following factors range from 0% to at least 100%.
Imbalance (white)
Halbe: This was written in terms of energy, but might make more sense in terms of momentum.
The most direct way of being incapacitated, attacks which impart force on your character or losing your footing in difficult terrain can cause imbalance. Imbalance constantly recuperates. Your mass and the directness of an attack determine how much imbalance you actually take, and your agility determines how quickly it is regenerated.
# use these for calibration
# direct hits by trained warrior in joules: halberd ~120, longsword ~70, shortsword ~30 dagger ~20
# longbow arrow 80
# kg: armored knight ~90, goblin ~40
const STAGGER_RESISTANCE_JOULES_PER_KG = 10
const UPPER_MUSCLE_KG_PER_STRENGTH = 5
const MUSCLE_KG_TO_JOULES = 2
const UPPER_MUSCLE_KG_TO_PUNCH_KG = 0.1
# attack_directness is 1.0 if square-on, 0.01 barely grazes, in-between is a glancing blow of some magnitude
fn balance_damage(attacker, defender, attack_directness):
# todo: equation for calculating striking mass for a given weapon, for now its fixed
# balance_factor is 0 for a weapon balanced at the hilt, 1 for a weapon balanced at the tip
attacker_upper_muscle_kg = attacker.strength * UPPER_MUSCLE_KG_PER_STRENGTH
punch_kg = UPPER_MUSCLE_KG_TO_PUNCH_KG * attacker_upper_muscle_kg
striking_mass_kg = punch_kg + attacker.weapon.mass_kg * (1 + attacker.weapon.balance_factor * attacker.weapon.length_meters)
joules_of_attack = attacker_upper_muscle_kg * MUSCLE_KG_TO_JOULES * striking_kg
imparted_joules = attack_directness * joules_of_attack
resistance = STAGGER_RESISTANCE_JOULES_PER_KG * defender.mass_kg
defender.imbalance += imparted_joules / resistance
Exhaustion (grey)
Exhaustion represents how out of breath your character is. Most actions will not actually exhaust faster than it recuperates, but climbing, sprinting, and fighting with heavy weapons, shield, and armor can.
const BREATH_RECOVERY_PER_ENDURANCE_PER_SECOND = 0.002
# someone with 2 endurance (poorly fed Napoleonic soldier) can march 1.2m/s all day. Therefore a simple linear ratio between velocity and breath must be about:
const BREATH_PER_METERS_PER_SECOND = 0.0034
fn update_stamina(player):
player.breath_damage += dt * character.velocity * BREATH_PER_METERS_PER_SECOND
player.breath_damage -= dt * character.endurance * BREATH_RECOVERY_PER_ENDURANCE_PER_SECOND
Pain (pink)
Injuries are a source of constant pain. Pain is divided by will.
$$ \mathrm{pain}(\mathrm{damage}, \mathrm{will}) = \frac{\mathrm{damage}}{\mathrm{damage} + \alpha\cdot\mathrm{will}}\cdot e^{-\beta\cdot\mathrm{will}};\ \alpha=0.5,\ \beta=0.2 $$
fn update_pain_factor(character):
damage = character.body_parts.iter().map(|p| p.damage).sum()
character.pain = pain(damage, character.will)
Blood loss (red)
Unbandaged wounds will cause you to bleed out, which will eventually incapacitate you.
Fear (blue)
Morale only starts affecting incapacitation when it goes below 0, at which point each negative point of morale becomes fear, translating to 1% incapacitation.
Fatigue (black)
This does not significantly accumulate in the course of combat, but is more a function of marching all day or going too long without sleeping. This probably has a threshold after which it starts applying nonlinearly ~halfway through the day.
Penetrating
Each piece of armor has a "resistance" and "padding", both are in terms of joules. Resistance opposes cutting edges and piercing points. When one of those attacks connects, the imparted joules are reduced by resistance to determine how much energy penetrates, if any. Weapons also have a "penetration" coefficient. The actual resistance used for an edged or pointed attack is:
$$ \mathrm{resistance_{\text{final}}} = \mathrm{resistance_{\text{base}}} - \mathrm{flexibility} \cdot \mathrm{resistance_{\text{base}}} \cdot \mathrm{penetration} $$
Penetration coefficient examples:
- Clubs: 0.1
- Maces: 0.5
- Swords/axes/musket ball: 1.0
- Broadhead arrows or spear: 2.0
- Mail breaker, rapier, or bodkin arrows: 4.0
Any edged or pointed energy that penetrates is then applied as cut damage.
Pure blunt contact does not test against edge resistance: its force is transmitted directly to padding, which dissipates energy before blunt damage is applied. For mixed blunt-and-edged weapon definitions, penetrated force is partitioned evenly between the two modes so it is not counted twice. Energy absorbed by resistance still transmits 50% as blunt force and applies the other 50% as unbalance, as described above.
Damage
Cut
Cut damage is divided by the penetration coefficient before being applied. This represents the greater surface area of flesh that is being torn up. Essentially, this makes axes and swords particularly ineffective against armor, but does extra damage against flesh.
Calibration:
- 80kg male's forearm is about 1.2kg
- A 20j direct hit dagger stab against an unarmored forearm should do just enough damage to incapacitate
- The point of having more powerful attacks is not to do more damage to flesh, but to get past armor
- A knight in full-plate still should be vulnerable to a mail breaker or bodkin arrow in the gaps between plates which are guarded only by chainmail
- A 20j stab from a mail breaker should just barely be able to penetrate chainmail and damage flesh
Blunt
Halbe: We may want to distinguish between bruising and bone fracturing, perhaps by picking an arbitrary amount of blunt damage energy after which it starts to fracture the bone.
Halbe: I'm not certain what a good physical base measurement is that we could use for mapping kj of energy to damage. Damage might be best represented as how many kgs of mass have been rendered inoperable, but its not clear to me how to convert between the two. Ultimately though, the damage value relevant to stats maps "0" to "gains no function from the body part" and "1" means "body part is fully functioning", so the "displaced kgs of mass" would itself be an intermediate value not displayed to the player.
Durability
Every durable item defines an elastic/yield threshold, catastrophic fracture threshold, ordinary wear rate, and catastrophic failure share. Impacts below yield do no condition damage. Above yield, ordinary wear accumulates continuously; above fracture, additional damage is assigned to the bin matching the impact severity. The failure share makes segmented construction localize a broken plate while a monolithic breastplate loses much more usefulness from a comparable fracture.
The five-bin condition remains one visually continuous bar. Bins indicate the Smithing skill needed for weapons, armor, and shields, or Tailoring for clothing, not discrete named faults. The first two bins are yellow and field-repairable; the last three are red and require settlement facilities. Stiff weapon steel has a relatively high yield threshold but a closer fracture threshold. Ductile armor yields and dents sooner while being harder to shatter.
Condition continuously lowers weapon precision (and other edge-sensitive performance) and increases the handling/mobility penalty of armor and shields. Armor coverage is not reduced merely because a local hole exists. Thus deformation of a helmet or breastplate can still impede movement without pretending that the whole protected region has disappeared.
Strategic autoresolve
The strategic autoresolver is a bounded abstract battle built from the pure
melee and ranged exchanges in adventuresim-core. It begins with two symmetric
pre-engagement phases:
- Every melee combatant makes a contested Stealth attempt against a randomly selected enemy's average Eyesight and Hearing. Both checks add a seeded random value from 0 to 5. Success grants one full-precision melee attack against the flat-footed target, with no active or facing defense.
- Ranged combatants fire while enemy melee combatants close. Their number of opening attacks is the ranged weapon's range divided by the fastest closer's movement speed and the weapon's attack interval. Melee combatants form a screen at two-meter intervals. If the closing side has surplus melee combatants able to bypass that screen, they must travel a semicircle around it; this detour increases the ranged firing window. Weapon melee reach and ranged range are separate autoresolve inputs.
During the main engagement, pairings are recomputed every round. Every active defender receives one melee opponent before surplus attackers are distributed for a second opponent, then a third, and so on. Every surplus attacker applies the current 90-degree flanking penalty. A melee screen therefore forces an equal number of enemy melee combatants to target it before exposed ranged combatants, and the same rules apply to allies and enemies.
Melee remains round-based: every capable melee combatant attacks once per main round. A faster melee weapon instead reduces the simulated input reflex of the defender, representing less time to react. Ranged combat runs on elapsed time; weapon attack interval determines how many shots occur in each one-second main round. Ranged combatants target opposing ranged combatants before melee targets. Every defender chooses dodge, parry/block, or no active response according to the response with the best expected result.
Every ranged attack consumes one generic arrow. When a combatant runs out, it becomes a melee combatant and uses its separately equipped melee weapon, if it has one. Player ammunition spent in autoresolve is removed from personal inventory. Enemy ranged profiles carry a bounded encounter supply and a melee fallback.
Targeted body part and hit precision are drawn from a deterministic seeded random stream. Pain, blood loss, existing strategic incapacitation, and temporary imbalance can remove a combatant from the fight. The battle ends when one side is incapacitated or after 256 main rounds, in which case it is a stalemate.
Autoresolve persists final player wounds, blood loss, and spent ammunition. It also writes a compact report containing the seed, victor, round count, summary, and an expandable exchange log. Enemy health and temporary combat state remain transient, so this diagnostic report does not change the tactical persistence boundary.
Strategic encounters pass an explicit Normal, AlliesSurprise, or
EnemiesSurprise opening into autoresolve. Awareness is not rolled again in
combat, and exactly one side receives a surprise turn. Quest and random combat
share the complete persistent-outcome commit path (injuries and retained
projectiles, blood loss, ammunition, weapon/shield/armor contact wear, combat
dirt and blood filth, morale, loot classification, and diagnostics). Random
encounter reports use encounter IDs and never create quest battle results or
complete an active quest.
Strategic
This is the intended strategic/tactical contract. The stealth detection model and its tactical handoff are not yet implemented; #212 tracks the work. Current quest combat is not evidence that stealth scenes already exist. When traveling, the party has a detection radius and a perception multiplier. The detection radius is the radius at which an enemy with a perception multiplier of 1.0 will detect you.
The detection radius is mostly based on party size versus the party member with the highest stealth score, who is ostensibly scouting ahead of the rest of the party. But every party member's stealth score does contribute marginally, which could mean multiple scouts if there's multiple with relatively high scores or just ensuring that everyone avoids leaving tracks.
The party perception multiplier should essentially give more weight to the party members with higher stealth, as they are further ahead, and is fundamentally based on their eyesight attribute.
If a party encounters an enemy party which does not detect them, they can choose to fight, sneak past, or take the long way around. The long way is guaranteed to succeed, but adds the most travel time. Sneaking past checks the stealth of all party members equally, so the weakest link can get you caught. If they choose to fight, then enter a tactical scenario in which the enemy party does not yet detect the players.
Strategic awareness is resolved once from independent deterministic party and enemy rolls. Party-only awareness offers sneak, detour, or an allies-surprise attack. Sneaking performs a second domain-separated whole-party check using the weakest member; failure starts a normal battle. Enemy-only awareness starts an enemies-surprise attack and bandits may demand surrender. Mutual awareness offers a normal attack, sustainable-speed run when eligible, and bandit surrender. Neither-aware results do not interrupt. Autoresolve never rerolls stealth, so only the authoritative side receives a surprise opener.
Tactical
The players start positioned relative to their stealth skill, essentially far enough away that enemies do not detect non-scouting party members regardless of line of sight.
For the MVP, we'll put some effort into the detection algorithm, accounting for light level, line of sight, and modify footstep sound by stealth check versus weight. But we aren't going to have a super detailed stealth AI. No patrol routes, search pattern logic, footprints, or enemies realizing that their allies are missing. At most, if they see a dead body they get a bonus to their ability to detect enemies due to now being on high alert. But in the future, there will be lots of opportunities to make this system more robust.
When an enemy does not detect you or for a short reaction-time window after they do, they are unable to dodge. This translates to a significant surplus of accuracy, allowing you to perform instantaneous kills on less-than-fully armored opponents or bypass the armor of fully-armored opponents.
Even an instantaneous takedown creates some noise, so unless nearby enemies are asleep this is typically just going to begin combat rather than allow you to take down an entire enemy camp in stealth.
If you do not manage to kill your target before their flat-footed timer has passed, they will alert nearby allies
World source manifests
World schema v18 replaces the old name/URL/license labels with a canonical, typed manifest for every distribution used by the offline compiler. Each entry has a stable ID, release status, canonical URL and optional DOI, typed licence, operational notices, access method, spatial and temporal coverage, preparation recipe, and content identity. Human-readable Markdown remains alongside these fields for the in-game audit trail.
The compiler sorts entries by stable ID and rejects duplicates, non-canonical
order, empty or oversized fields, missing notices, malformed hashes, report
claims without their source entry, and unknown serde fields. There is no
Unknown identity. A source is either reproducible from a raw/prepared SHA-256
or curated revision digest, or explicitly records an unpinned or
release-blocked state. The latter states never report themselves as
reproducible.
Identity and cache boundary
The manifest digest is BLAKE3 over the world schema version, inference-rules
version, world year, complete SpatialGridSpec, and sorted distribution
manifests (including notices and preparation). Changing any field changes the
digest. Source ordering, local paths, and file timestamps do not. The complete
compiled artifact is still hashed separately; both IDs are retained by the
SpacetimeDB import session. No separate world-data cache currently exists, so
this digest defines the key that a future cache must use.
The import session also retains complete deterministic audit Markdown for every used distribution: ID, name, release, licence, content status and identity, URL, DOI, notices, access, spatial and temporal coverage, preparation, and notes. Each typed source is embedded as canonical compact JSON. The compiler rejects the import before calling a reducer if the fully JSON-quoted argument would exceed the 24,000-character Windows transport budget; legal text is never silently truncated.
The stored digest is compiler/operator-attested under the trusted first-caller import model. SpacetimeDB syntax-checks the digest and binds it to the import session, but it does not receive the typed manifest and therefore cannot recompute the digest. It is an audit/cache identity, not independent proof against a malicious or noncanonical client.
Operational notices and unresolved boundaries
- Viabundus: retain attribution and CC BY-SA 4.0. The project applies a
conservative BY-SA treatment to its generated map and terrain contributions,
keeps those artifacts outside the software AGPL, and carries the complete
source-specific terms in
MAP_DATA_LICENSE.md. This policy is specific to those reviewed packs and does not authorize an unrelated combined output. - Copernicus DEM: retain the prescribed Copernicus/WorldDEM production credit and European Commission/ESA no-liability notice.
- HYDE 3.5: retain HYDE attribution, the CC BY 3.0 licence link, and an indication that Adventure Simulator interpolates and classifies the source.
- Copernicus forest and EU-Hydro: credit the European Union/Copernicus, identify project modifications, and do not imply endorsement.
- Jung PNV, SoilGrids, and EGDI: retain CC BY 4.0 attribution and identify gameplay conversion changes. EGDI additionally retains the Maltese contribution and disclaimer.
- EU-Trees4F: retain dataset and publication citation despite CC0.
- IEG religion: source images remain rights-reserved and are not redistributed. Only the coarse curated intermediate is checked in.
- NOAA OWDA: cite NCEI and Cook et al.; compiled output remains bounded per-settlement derived data, not the grid or annual series.
Current release blockers are explicit: GLO-30 tile selection, the manually
acquired HYDE 3.5 files, EGDI, and EU-Hydro lack checked complete content
inventories. The forest marker pins only the preparation format, so forest
remains non-reproducible until every consumed raster is inventoried and hashed.
SoilGrids is a rolling service whose strict prepared manifest supplies the
actual retrieval timestamp and snapshot identity; that does not make raw
latest reacquisition reproducible. These entries do not claim legal
resolution or reproducibility that is not present.
scripts/world_source_init.py provides bounded plan/init/verify workflows for
these accepted sources. Only the immutable EU-Trees4F archive is acquired; the
authenticated or incompletely pinned sources fail closed until reviewed
inventories are committed. The curated IEG CSV is validated in place and its
rights-reserved reference images are never mirrored. Sidecars use sorted fixed
metadata and actual sizes/hashes, reject unknown inventory fields, traversal,
symlinks, redirects outside fixed hosts, oversized content, and partial or
checksum-failing publication.
The EU-Trees4F initializer pins the exact JRC ENS_CLIM archive. It retains the EU-Trees4F v2 Figshare citation and CC0 notice, but does not claim that a Figshare-hosted archive is byte-identical; confirming that relationship remains an explicit provenance blocker.
Source-separated developer bundles
scripts/world_data_bundle.py provides a distinct distribution boundary for
development inputs. Its ZIP is a manifest-and-notices collection: source
components remain separately addressed and are installed into the compiler's
existing source paths only after complete verification. It carries no compiled
world artifact. The policy includes every active input but rejects LUH1,
rights-reserved IEG map images, and raw OWDA grid/annual data; IEG's committed
coarse CSV stays in the repository and OWDA may appear only as a bounded
per-settlement derived profile. This engineering policy is fail-closed and does
not itself resolve source-specific redistribution terms or the licence status of
a future combined world-data release. See wiki/reference/world-data-bundles.md.
World-data input bundles
A world-data input bundle is the supported convenient development handoff:
download one reviewed ZIP, verify it, install it, then run just compile-world.
It is a collection of separately addressed inputs, not a combined derived
world artifact and not a declaration that all component licences are mutually
compatible.
just init-world-data
just verify-world-data-bundle /path/to/adventuresim-world-inputs.zip /path/to/adventuresim-world-inputs.release.json <published-descriptor-sha256>
just install-world-data /path/to/adventuresim-world-inputs.zip /path/to/adventuresim-world-inputs.release.json <published-descriptor-sha256>
just compile-world
just init-world-data is the standard developer path. It downloads the exact
full release pinned in the checked-in world-data-release.lock.json from the
project's public R2 development URL, resumes an interrupted byte-range download,
verifies the separately pinned descriptor, and atomically installs the
source-separated inputs. It does not download a combined world-1544.json.
Every full release is required to contain both the reviewed Viabundus v2
component and the four HYDE 3.5 c9 inputs (cropland.nc, grazing_land.nc,
urban_area.nc, and general_files.zip), as well as the other required source
components. Their payloads, inventories, notices, and licences remain separate
inside the collection.
The installer retains the downloaded ZIP below target/world-data-bundle-cache/
for later verification and requires roughly 50 GiB of free disk space while it
installs. If a different release is already installed, rerun the script with
--replace only after checking its recoverable backup.
just rebuild-world-data deliberately skips R2 and compiles a world from
already installed local inputs. It is the explicit rebuild path; it does not
re-acquire the upstream sources or overwrite the pinned release.
The installer stages and verifies every member before it changes any destination.
It refuses to merge with an existing local component. To intentionally replace
one, use just replace-world-data /path/to/adventuresim-world-inputs.zip /path/to/adventuresim-world-inputs.release.json <published-descriptor-sha256>; the
previous component remains recoverable below target/world-data-backups/.
Do not remove that backup until the new compile has succeeded.
Verification and installation require the separately published canonical release descriptor. It contains the expected archive SHA-256 plus hashes of the canonical manifest and component inventory; it is deliberately outside the ZIP so a rebuilt archive cannot authenticate itself. The release maintainer uploads both files to the central release and publishes the descriptor's SHA-256 in the release notes through the normal project review process. The developer supplies that independently published digest to the verify/install command; a ZIP and descriptor that merely agree with each other are rejected without it.
Archive contract
At the archive root, bundle-manifest.json is canonical JSON with a sorted
component inventory. Every payload component has a separate
NOTICES/<source-id>.md; its files live only below
payload/<source-id>/. Each component names its source/version, distribution
form, installer destination, notice, and sorted relative path/size/SHA-256
records. The verifier rejects unknown manifest fields, noncanonical order,
duplicates, extra members, unsafe paths, encrypted or symlink ZIP members,
unbounded compression, and hash/size mismatches. It intentionally permits ZIP64
when Python supports it.
Hidden local transfer fragments ending in .part are not source inputs and are
excluded from a build. A completed payload remains independently enumerated and
hashed in the archive manifest.
The default build command refuses an incomplete collection: it requires every
active compiler input, the IEG checked-in marker, reviewed layouts, and its
canonical per-component file inventory. --partial exists only for explicitly labelled test or
non-developer archives and must not be published as the developer release.
The Viabundus component includes only the five audited CSVs consumed by the
importer plus its official source sidecar. The sidecar may describe additional
supplementary CSVs in the upstream release; those files are intentionally not
copied into the bundle. A full strategic-map rebuild also consumes the
supplementary water-1500.csv. After installing the source bundle, run
python scripts/init_viabundus.py --force to replace the bundle's importer-only
Viabundus directory with the complete upstream CSV set before running
just build-strategic-map.
The policy is explicit and fail-closed for every current compiler input: Viabundus, HYDE 3.5, GLO-30, Copernicus forest, Jung PNV, EU-Trees4F, prepared SoilGrids, EGDI, curated IEG religion, OWDA-derived profiles, and EU-Hydro. It rejects LUH1, IEG maps/images, and raw OWDA grids or annual series. IEG's coarse curated CSV remains a checked-in repository asset, so the bundle records its notice but never copies a payload over a version-controlled file.
OWDA is permitted only as a bounded, per-settlement derived profile component. The raw NOAA NetCDF source is not eligible for the bundle. A release must use the documented derived profile form consumed by the importer; do not substitute the source grid merely because it is convenient.
That component contains exactly settlement-profiles-1544.json, installed at
target/world-data-sources/prepared/owda/. It is canonical JSON with
schema: 1, source: "noaa-owda-v1-derived", version: "1544", year: 1544,
the hashes of the matching Viabundus sidecar and settlement-ID inventory, and
sorted profiles. Each profile has settlement_id, sampling (direct or
nearest), current_milli_pdsi, mean_milli_pdsi, drought_summers, and
wet_summers.
It therefore carries a bounded 20-year summary for a particular settlement, not
OWDA coordinates, grid cells, or annual values.
Building a release collection
Release maintainers prepare each source's approved raw, prepared, curated, or
per-source-derived directory independently. They must review the applicable
terms, attribution, source inventory, and every modification before publishing;
the tool does not make a legal clearance decision. It never accepts or creates a
combined world-1544.json input.
For example, with already reviewed local directories:
python3 scripts/world_data_bundle.py build --partial --output partial-test-inputs.zip \
--component hyde-3-5-c9=target/world-data-sources/raw/hyde35-land-use \
--component viabundus-v2=viabundus \
--include-checked-in
The final archive should be hosted in the project’s chosen release storage with its descriptor and descriptor checksum. Create the reviewed descriptor only after the final archive is immutable:
python3 scripts/world_data_bundle.py describe adventuresim-world-inputs.zip \
--output adventuresim-world-inputs.release.json
Source bytes are deliberately not committed to this repository.
Publishing to the project R2 bucket
The release tool can upload a verified full ZIP and its external descriptor to
the fixed adventuresim-world-data Cloudflare R2 bucket. It reads the local
repository .env without displaying its values. The file must provide
R2_ACCOUNT_ID, R2_S3_ACCESS_KEY_ID, R2_S3_SECRET_ACCESS_KEY, and
R2_S3_API_ENDPOINT; R2_API_TOKEN is not used for S3 object upload. The
endpoint must be the HTTPS R2 endpoint for the stated account. Install AWS CLI
v2, which uses multipart upload for this large object, then publish only after
the release descriptor is final:
python scripts/world_data_bundle.py publish adventuresim-world-inputs.zip `
--descriptor adventuresim-world-inputs.release.json `
--descriptor-sha256 <published-descriptor-sha256>
The command validates the ZIP and descriptor before uploading, writes the ZIP
and descriptor below releases/world-data/, and verifies each resulting R2
object's content length. It never uploads a partial profile. The resulting
object locations are:
s3://adventuresim-world-data/releases/world-data/<archive-name>.zip
s3://adventuresim-world-data/releases/world-data/<archive-name>.release.json
Uploading does not make clients select the new release automatically. After
both immutable objects are publicly readable through the project's
pub-46168a4accb04d08ad0a558b0a2abfaa.r2.dev custom R2 development domain,
update the checked-in world-data-release.lock.json with their public HTTPS
URLs, the exact ZIP byte size, and the SHA-256 of the external descriptor.
just init-world-data downloads exactly that pinned pair; it never discovers a
"latest" object by listing the bucket.
Compiled runtime release
The source-input workflow is separate from the small compiled runtime release
pinned in world-runtime-release.lock.json. That release contains
world-1544.json, the AVIF strategic-map manifest/pack, the final coherent
terrain-routing manifest/pack, the strategic-map licence, and a generated world
data notice. Its exact derived bytes are independently hashed; upstream
reproducibility warnings remain embedded in the world source manifests and are
repeated in the generated notice.
just load-world runs just init-world-runtime automatically. If every pinned
runtime file already matches, initialization performs no network request. On a
fresh checkout it downloads the single archive from
s3://adventuresim-world-data/releases/world-runtime/, verifies the checked-in
archive and member hashes, and installs the files under target/ without
invoking the source compiler. It then reset-publishes the current module and
loads the compiled world, discarding all existing rows in the selected
loopback adventuresim-* database. A conflicting local build is never
overwritten implicitly; just replace-world-runtime retains replaced files
below target/world-runtime-backups/. A previously pinned runtime that is still
byte-for-byte intact is recognized as downloaded output and upgraded
automatically, with the prior files retained by the same backup mechanism. The
optional second argument selects the database to recreate while retaining
spacetime_module as the default, for example
just load-world http://127.0.0.1:24610 adventuresim-dev-example for an
isolated strategic profile. The load-viabundus-world compatibility alias
accepts the same arguments.
Release maintainers build and publish it after just build-strategic-map:
python scripts/world_runtime_release.py build --repository . `
--output target/adventuresim-world-runtime-1544-YYYYMMDD.zip `
--lock-output world-runtime-release.lock.json `
--release 1544-YYYYMMDD
python scripts/world_runtime_release.py publish `
target/adventuresim-world-runtime-1544-YYYYMMDD.zip `
--lock world-runtime-release.lock.json
The publisher validates the complete archive before writing the immutable
object below releases/world-runtime/ in the same R2 bucket used by the source
bundle. Commit the generated lock only after the public object is verified.
Canonical spatial grid
World-data enrichment stages use one source-independent grid identity. Its CRS
is EPSG:3035 (ETRS89 / LAEA Europe), its serialized origin is exactly (0 m, 0 m), and cells are square. The default cell size is 1,000 m. Deliberate
alternatives must be integer metres from 250 through 100,000, inclusive, and
divisible by 250. Alternate projections, origins, or rectangular cells are not
valid world artifacts.
adventuresim-world-schema owns the validated SpatialGridSpec wire type.
Private fields and custom deserialization prevent malformed artifacts from
bypassing its constructors. The importer owns WGS84-to-EPSG:3035 projection,
millimetre quantization, and cell assignment. Euclidean division makes negative
coordinates and exact cell boundaries deterministic. Raw TIFF, COG, vector,
and source-manifest parsing stays in individual importer source modules.
Coverage is not part of the shared grid. Each source manifest must state its own extent and distinguish these cases explicitly:
- the requested cell is outside source coverage;
- the cell is covered but all relevant observations are nodata;
- the cell has usable observations.
Stages must document and record any fallback instead of silently mapping outside-coverage or nodata states to zero.
Identity and future caches
The world schema version, inference-rules version, and complete grid spec are serialized in metadata and therefore participate in the final BLAKE3 artifact ID. Old artifacts missing these fields fail deserialization; they are not assigned defaults.
No intermediate cache framework exists yet. When one is introduced, every key must contain:
- world schema version and inference-rules version;
- the complete serialized grid spec;
- stage name and stage implementation/version;
- source checksums sorted by their stable source identity.
Any mismatch means regeneration. Source coverage extents belong in the source
manifest and will affect its checksum rather than expanding SpatialGridSpec.
The complete canonical manifest checksum and operational-notice contract are
documented in wiki/reference/source-manifests.md.
Viabundus world data
Canonical MVP compilation retains only nodes, settlements, and complete road
edges whose endpoints are within [8.965, 50.877, 11.110, 52.211]. This
filter runs before environmental enrichment, so out-of-bounds settlements do
not consume raster sampling work or enter SpacetimeDB. The independently
generated presentation map clips full-precision road and water geometry at the
same exact boundary.
The strategic world-import pipeline uses Viabundus Pre-modern Street Map 2, version 2 (released 25 April 2025), edited by Bart Holterman et al.
- Source record: https://doi.org/10.5281/zenodo.16611998
- Project: https://www.viabundus.eu
- License: CC BY-SA 4.0
The initializer sidecar records the byte size and SHA-256 of each downloaded CSV. Import requires a bounded, deny-unknown v2 sidecar with the canonical Zenodo record, unique safe CSV names, and an inventory of every consumed CSV; it verifies consumed bytes before granting reproducible snapshot status. Legacy sidecars without sizes remain explicitly release-blocked.
For normal development, just init-world-data installs the reviewed upstream
CSVs into the Git-ignored viabundus/ directory as the Viabundus component of
the pinned source-separated bundle. The native Rust world compiler reads the
files from its source-specific sources::viabundus module, then enriches the
draft with required values from the other initialized sources. just compile-world writes the validated, schema-versioned artifact to
target/world-1544.json. The generated strategic graph contains
the source attributes required to route between and identify settlements in
1544: nodes, active land/ferry edges, settlement metadata, active alternative
names, and settlement/city descriptions. Description HTML entities are decoded
and source markup is removed by the Viabundus parser, so only plain text enters
the source-independent world schema. Each settlement also retains its
approximate population estimate. It is an adapted
dataset and must retain this attribution and CC BY-SA 4.0 licensing when
distributed.
The settlement Map screen uses the separately generated
target/strategic-map/strategic-map-v1.json presentation package and
target/strategic-map/strategic-map-tiles-v1.pack world-tile asset. just build-strategic-map derives both versioned files with an embedded content digest from the
initialized Viabundus v2 roads, ferries, and 1500 water polygons, generalized
Copernicus GLO-30 elevation, and every available prepared Copernicus forest
tile. It clips the view to the supported northern-European envelope and
simplifies source geometry for a multilevel raster presentation; it does not change or replace
canonical routing data. Native elevation is classified into hilly areas rather
than absolute-height colour bands. Forest
coverage is deliberately partial: the generator renders only
installed TCD/DLT tile pairs and records their exact bounds instead of filling
missing regions with inferred vegetation.
The server exposes individual AVIF images from an indexed pack beneath a small
inline SVG settlement overlay. Each tile URL includes the pack's SHA-256 and is
served with public, max-age=31536000, immutable. The browser loads only tiles
covering the current viewport at an appropriate zoom from the Paper pyramid.
The offline raster compiler belongs to adventuresim-world-import, not the web
server. strategic-web optionally loads STRATEGIC_MAP_BUNDLE_DIR (default
target/strategic-map) at runtime and has no raster renderer or encoder
dependency. A missing or invalid bundle does not prevent startup: settlement
selection and direct travel continue through the surrounding HTML interface.
Current and selected settlements, locally issued available quests, the party's
active quest when it is at the issuing settlement, direct-route state,
population-class settlement pictograms, collision-managed names, destination
links, and the straight selection line remain dynamic HTML/SVG and are served
on every map response. Label priority responds
to the current zoom while the raster package remains unchanged and cacheable.
A four-pixel encoded gutter prevents seams between lossy
tiles, while the deepest level uses high-quality AVIF. The map uses a
single green forest mask at 20 percent canopy, light brown for hilly open
ground, and dark green where forest and hills overlap. It draws no symbolic
hill or mountain stamps; native elevation remains in the independent terrain
pack for routing and future terrain presentation. Forest is classified once on the
prepared 0.001-degree canopy grid; every coarser level is an area average of
2-by-2 children, and adjacent mip levels are blended while rendering. Forest
boundaries therefore retain one sampling history across zoom levels instead of
switching between a procedural low-detail field and native samples. The
coverage remains offline raster content rather than dynamic browser geometry.
Viabundus zoom importance also
controls road visibility, weight, and ink strength, while a subtle globally
positioned parchment texture keeps adjacent tile gutters identical.
The stable strategic-map-v1.json and strategic-map-tiles-v1.pack filenames
are versioned, not content-addressed; the pack digest query parameter is every
tile route's cache key.
Adventure Simulator's contributions to the generated map and terrain packs are
distributed under CC BY-SA 4.0 rather than the repository software's AGPL.
Every bundle must retain the generated STRATEGIC_MAP_DATA_LICENSE.md notice
or provide a reasonably prominent link to the canonical MAP_DATA_LICENSE.md;
the server exposes the latter at /map/data-license.
The compact schema-5 deployment manifest carries renderer revision 10, reviewed
source identities, coverage counts, and the indexed AVIF pyramid, but not the
offline roads, compound water rings, elevation cells/contours, or forest
regions used to render it. Its embedded SHA-256 covers every deployed field;
strategic-web revalidates that digest and the exact source URLs before
rendering. A
legacy initializer sidecar without recorded byte
sizes is accepted only with the explicit
legacy-release-blocked-missing-sizes package status.
Active Viabundus bridge and toll nodes are projected onto their incident travel
edges with their from, to, or both endpoint identity intact. Ferry routes
and land routes with an optional bridge are distinct enum variants, so invalid
combinations cannot enter the import schema. These are edge properties rather
than settlement properties so travel encounters and tactical scene generation
can use them without implying that the infrastructure lies inside a neighboring
settlement. Contradictory equal start/end years are retained in the compiler's
source model and reported, but do not invent an active feature interval.
Each imported settlement has the prototype's shared merchant services, and newly created characters start at a random loaded settlement. The settlement overview lists historical aliases and exposes one deterministic historical description with its source language; population-based English flavor text remains the primary description. Non-settlement description categories such as bridges, tolls, and ferries remain deferred and are counted by category in the compiler build report.
The import does not claim that every represented line is an exact historical
road. Viabundus' certainty value is preserved on each travel edge so gameplay
and presentation can account for uncertain reconstructions later.
The source parser and world-building orchestration live in
adventuresim-world-import. Source-independent import records live in the
lightweight adventuresim-world-schema crate shared with the strategic
SpacetimeDB module. Heavy source readers must remain in the native importer so
they cannot add filesystem or geospatial dependencies to the database module.
Strategic route terrain
World schema v25 and inference rules v9 attach a required RouteTerrain to
every imported travel edge. The record is a static strategic fact used for
travel planning and encounter selection. It never persists tactical positions,
damage, HP, enemies, or simulation ticks.
Weather is a calculation-only overlay over this immutable package. Version 1
uses coarse quarter-degree cells and six-hour absolute-time intervals. Route
cells use their own imported elevation and wetland facts; settlement soil or
hydrology is never projected onto arbitrary cells. Departure route payloads
attest the weather rules version, aligned interval, condition, intensity,
antecedent moisture, and snow cover. Validation rejects unbounded or
inconsistent snapshots, and the gateway cache distinguishes them.
Route search, duration, persisted span weights, and check_millirank all use
the same departure overlay; weather is not a post-processing adjustment.
Rain only increases effective Wetlands when the imported normalized weight is at least 100/1000. Displaced weights are deterministically renormalized to keep the five-member total at 1000. Independent saturation severity allows a 1000/1000 wetland to become slower without corrupting the mixture.
Geometry and elevation
Viabundus supplies topology and endpoint coordinates rather than complete road
polylines. Documented edges therefore retain the endpoint interpolation below.
Terrain-inferred edges instead sample their canonical A* polyline. The compiler chooses
N = min(1000, max(1, ceil(length / grid-cell-size))) segments, yielding a
bounded profile of two through 1,001 samples with unique permille progress and
required 0/1000 endpoints.
Each profile point samples GLO-30 at the center and the eight positions one configured canonical-grid cell away. The shared strict reader validates tile georeferences and handles nodata consistently with settlement elevation. Decoded tiles use a deterministic least-recently-used cache bounded to 64 MiB; eviction changes memory use only, never sample values or artifact identity. Missing tiles and terminal voids use the explicit sea-level fallback; the build report and edge Markdown record that choice.
Consecutive center samples produce ascent, descent, and signed maximum grades.
Interior projected coordinates use sign-symmetric nearest-integer rounding, so
reversing an edge yields the exact reversed coordinate sequence.
The 3x3 neighborhoods produce Horn slope/aspect, mean absolute center-neighbor
difference (TRI), and relief. Aspect is Flat below 10 permille mean slope;
otherwise a circular mean selects one of eight closed compass directions.
Rules v6 classify a route as:
Flat: maximum slope below 30 permille and relief below 30 m.Rolling: below 80 permille and 100 m.Hilly: below 150 permille and 300 m.Mountainous: everything else.
A center at least 20 m above/below its eight-neighbor mean marks a ridge or valley. A likely pass requires opposing high neighbors and lower orthogonal neighbors. Adjacent identical detections are deduplicated deterministically.
Water, seasons, and encounters
EU-Hydro crossings and ferry waterways become canonical nearest facts at zero meters. Feature kinds are river, canal, ditch, inland, tidal, and coastal. Seasonal rules are deliberately small and exact: ford/ferry facts dominate spring flood, autumn mud, and winter ice severity; nearby freshwater can add low mud or ice risk; mountainous or 1,000 m routes add medium winter snow. No summer-drought route rule exists in v5.
Static encounter tags cover terrain class, steep (at least 150 permille), rough
(TRI at least 20 m), landforms, bridge/ford/ferry, water banks/shores, and each
seasonal hazard. Empty collections mean confirmed absence, never unknown.
Viabundus slope_multiplier remains a separate source cost hint and is not DEM
grade.
Both the offline validator and strategic import reducer recompute profile ascent/descent, grade extrema, relief, class, seasonal risks, and encounter tags. Malformed or contradictory derived facts are rejected. Collection decoding is capped before allocation and canonical uniqueness is defined by logical key (progress, feature, hazard, or tag), not by the complete payload.
The official full GLO-30 and EU-Hydro audit remains blocked until their authenticated, completely pinned source inventories are available locally. Synthetic tests exercise deterministic algorithms and strict boundaries; this document does not claim issue #62 complete.
Native routing skill mixture
The separate native terrain-routing pack (schema 6) retains wetland, canopy, and hill coverage independently. Runtime routing cells normalize them to exactly 1,000 permille: Wetlands receives its area share first, Forest follows canopy density over the remaining land, Hills receives the hill-covered share of the remaining non-forest ground, and Plains receives the remainder. Urban is part of the Terrain skill domain but has zero route weight until an authoritative built-up-land source is added; roads are infrastructure over their underlying terrain and are never treated as Urban.
Plains, Forest, Hills, Wetlands, and Urban are intuitive mental checks governed
by Intelligence on the shared rank-zero-to-five curve. Party
checks use bounded party aggregation, then the cell mixture's dot product.
Movement speed is multiplied by 1 + check / 10. The departure profile is in
the A* cache key and the resulting adjusted spans are validated and persisted,
so skill can affect route choice while an active journey remains stable.
Actual walking trains every living traveler and conserves exposure across the normalized mixture. Non-road movement grants one exposure hour per movement hour. Roads still train their underlying terrain, discounted by underlying off-road speed divided by 5 km/h: 0.25 open, 0.20 sparse woods, and 0.15 deep woods. Camp intervals grant no Terrain training. Wetland road exposure is 0.10, from 0.5 km/h divided by 5 km/h.
terrain-routing-base-v3 is a documented-road-only inference input and cannot
be served as the final pack. terrain-routing-v3 is rebuilt after world
compilation with the accepted inferred polylines. Both manifests carry a purpose,
road-geometry digest, Jung wetland source digest, and package digest. Wetland
ground moves at 0.5 km/h unless overridden by water or a road.
Schema 6 uses one flag bit for the canonical EPSG:3035 1 km cultivation
classification and another for native wetland coverage, so cells remain five
bytes. Both bits decode to area fractions when cells are coarsened. The manifest validates
the grid CRS/resolution, HYDE dependency digest, allocator rules version, and
square/native-cell counts. Runtime sampling is bounded by the existing chunk
LRU.
Elevation world data
Settlement elevation comes from the Copernicus DEM GLO-30 digital surface model. The current source is modern rather than a historical reconstruction; terrain height is sufficiently stable for the game's plausibility-oriented 1544 world generation.
- Product DOI: https://doi.org/10.5270/ESA-c5d3d65
- Product information: https://dataspace.copernicus.eu/explore-data/data-collections/copernicus-contributing-missions/collections-description/COP-DEM
- Terms: Copernicus DEM licence
The *_DEM.tif tiles belong in the Git-ignored
target/world-data-sources/raw/elevation/ directory. just plan-glo30 prints
the deterministic request and redacted CDSE_TOKEN_FILE preflight,
init-glo30 refuses until the complete tile inventory is pinned, and
verify-glo30 checks a strict local source-inventory.json. This
release-blocked workflow never logs or stores a token.
sources::elevation uses the pure-Rust tiff crate and does not require GDAL.
Its strict tile, georeference, nodata, and nearest-valid-pixel reader is shared
with route-terrain enrichment. Route sampling uses a deterministic 64 MiB LRU
decoded-tile cache, while the settlement batch remains grouped one tile at a
time. It groups settlements by one-degree GLO-30 tile, decodes only the 159
tiles used by the current Viabundus settlement set, and releases each raster
before reading the next. A settlement receives a required ElevationMeters
value; ElevationBand is derived from it rather than redundantly stored, so the
two can never contradict each other. There is no unknown variant. Invalid or
void source pixels are replaced by the nearest plausible pixel within eight
raster cells of the same source tile, then by sea level if that local window is
entirely void. The
build report counts these fallbacks. The verified 1544 build sampled all 6,041
settlements without using a fallback.
The settlement Map presentation classifies a native cell as hilly when its elevation difference to any of its eight neighbours implies a slope steeper than 15 degrees using the latitude-correct source-cell distance. Hilly open ground is light brown; hilly ground with at least 20 percent canopy is dark green. Low zooms use a naturalized aggregate of the same classifications.
Mountains are deliberately not an elevation band. A one-kilometre summary grid median-filters native samples, then requires at least 300 metres of local relief in a seven-kilometre radius together with either 150 metres of local one-kilometre relief or at least 15 percent steep terrain. Connected components smaller than roughly ten square kilometres are removed before engraved ridge marks are emitted. This excludes high plains and isolated DEM spikes while retaining low but rugged ranges. Browsers receive only compressed tiles; raw DEM pixels are not served and the presentation layer is not persisted in SpacetimeDB.
Elevation is stored on settlements because it describes the settlement's own location and can directly influence scene selection, climate inference, agriculture, travel preparation, and UI presentation. A future source may add route elevation profiles. See route-terrain.md; settlement elevation is never used as a proxy for terrain along an entire road edge.
Native strategic terrain pack
build-strategic-map reads the 12 whole-degree source tiles intersecting the
exact 8.965–11.110°E, 50.877–52.211°N playable bounds into
the documented base and final terrain-routing-*-v2.json/.pack artifacts. The packs preserve
each source tile's native 1,800/2,400/3,600 by 3,600 grid instead of expanding
it into database rows or ElevationCell structs. Independently deflated
256×256 chunks carry signed elevation, road/open/sparse-woods/deep-woods/water
surface, exact bounded canopy percentage, a native 15-degree hill bit, and an
explicit infrastructure-crossing bit.
The runtime decodes the native hill bit as 0 or 100 percent hill coverage. When pathfinding coarsens a window it averages that percentage, rather than promoting a cell when any sample is hilly. Canopy and hill coverage therefore remain independent and a wooded hillside stays a mixed terrain cell.
The compiler writes STRATEGIC_MAP_DATA_LICENSE.md beside the terrain and map
outputs. It contains the prescribed Copernicus WorldDEM-30 production credit,
liability notice, modification statement, and the separate CC BY-SA licence
for Adventure Simulator's contributions. Distribute that notice with the pack;
the repository software's AGPL does not license these generated data artifacts.
The manifest and pack are separately SHA-256 addressed. Readers reject wrong dimensions, overlapping or truncated chunks, digest mismatches, excess entries, and oversized decompression. Runtime decompression uses a deterministic 32 MiB LRU. The compressed pack remains range-readable on disk, is size-checked before opening, and is stream-hashed at startup, so the complete native grid never resides in RAM. Chunk I/O and decompression occur outside the cache lock before a race-safe insert. The bounded map's z3 paper tiles sample this pack at approximately 25 m/pixel. Every presentation tile lies inside native-detail coverage, so close zooming does not need continental fallback tiles.
The same pack is the strategic pathfinding input. A bounded search window begins
at the native nominal 30 m spacing and coarsens deterministically only when the
hard 750,000-node cap requires it. Eight-neighbour A* minimizes directional
travel time: roads are fastest, open ground is slower, sparse and deep woods
are progressively slower, and positive elevation gain adds an uphill cost.
Water is impassable except where imported infrastructure marks a crossing.
Search costs use seconds internally so rounding does not compound at every
30 m cell; persisted journey time is rounded once to whole strategic minutes.
Each sampled cell also derives a normalized permille Terrain distribution:
Forest is the canopy percentage, Hills is the hill fraction of the remaining
non-forest share, Plains receives the remainder, and Urban is currently zero.
The departure party's weighted Terrain check multiplies speed by
1 + check / 10; A* includes that profile in both its costs and cache key, so
expertise can change the chosen route as well as its duration.
Any missing source tile intersecting the playable boundary remains a hard build error. Whole source cells are retained internally, while the manifest, cell lookup, and route planner enforce the exact decimal playable bounds.
Routes are simplified to at most 512 geographic points for transport and are stored only for an active journey together with exact ordered terrain-time spans, their Terrain mixtures, departure checks, road exposure discounts, and the terrain package digest. The raster remains an optional on-disk asset with a 32 MiB decompression cache. If it is absent, corrupt, outside its coverage, or cannot produce a bounded route, the existing HTML travel flow remains available and labels its straight-line value as a legacy estimate.
Surface water and road crossings
Settlement water access and travel-edge crossings are sourced from the Copernicus EU-Hydro River Network Database v1.3.
- Product: https://land.copernicus.eu/en/products/eu-hydro/eu-hydro-river-network-database
- DOI: https://doi.org/10.2909/393359a7-7ebd-4a52-80ac-1a18d5f3db9c
- User guide: https://land.copernicus.eu/en/technical-library/eu-hydro_user_guide
- Projection: ETRS89 / LAEA Europe (EPSG:3035).
- Source period: primarily 2006, 2009, and 2012 imagery, supplemented by EU-DEM drainage modeling. This is used as plausible geography for 1544, not as evidence that every modern canal or watercourse existed then.
Download the basin GeoPackage distribution and extract its .gpkg files under
target/world-data-sources/raw/hydrology/. Nested basin directories are
accepted. Override the directory with --hydrology-dir.
The importer has been exercised against the official Elbe basin GeoPackage as
part of a complete world build. That distribution uses the valid legacy
GeoPackage 1.1 GP11 application identifier and mixes ISO dimensional type
codes on collection roots with equivalent EWKB Z/M flags on their children.
The source boundary accepts the OGC GP10, GP11, and current GPKG
identifiers and normalizes only those equivalent nested Z/M flags before
decoding. It continues to reject unknown application identifiers, embedded
EWKB SRIDs, incompatible child types or dimensions, truncated geometry,
overflow, empty geometry structures, and trailing bytes. Geometry parsing is
bounded by blob size, nesting depth, structural nodes (including polygon
rings), and coordinate count. X/Y ordinates must be finite and fall in the
deliberately generous -1,000,000 to 10,000,000 metre safety envelope, which
contains the EPSG:3035 European area of use and EU-Hydro source extent. A
feature may occupy at most 10,000 ten-kilometre spatial-index cells. These
limits are cumulative as well: one import accepts at most 256 GeoPackages,
1,000,000 decoded features, 50,000,000 decoded coordinates, and 10,000,000
spatial-index references. Polygon rings must contain at least four coordinates
and be exactly closed in every dimension; lines must contain at least two
coordinates. These defense-in-depth limits keep malformed source data from
causing unbounded parsing or index construction. Read-focused SQLite fixtures
cover those contracts alongside an ignored integration test for a locally
installed official distribution.
just plan-hydrology performs a redacted CLMS_TOKEN_FILE preflight and
prints the fixed v1.3 request contract. Because official archive/item IDs and
the complete basin GeoPackage inventory are not pinned, init-hydrology
refuses network acquisition. verify-hydrology validates a supplied strict
local inventory; the source remains release-blocked.
Parsed source features
The compiler recognizes the official River_Net_l, Canals_l, Ditches_l,
InlandWater, Transit_p, and Coastal_p feature classes. It also accepts
the equivalent names exposed by the EEA map service. Relevant features are
clipped to a ten-kilometer margin around the imported world before enrichment.
When a basin GeoPackage provides the standard RTree extension, the SQL reader
applies that envelope before decoding geometry; packages without it use a
compatible full-table scan and the same exact geometry-bounds filter.
For flowing water, STRAHLER, HYP, and NVS become bounded Strahler order,
perennial/intermittent/ephemeral persistence, and navigability. Dry source
segments are omitted. Missing and sentinel attributes are resolved to
plausible defaults while parsing; raw -9999, null, or unknown values never
enter the canonical schema. AREA_GEO classifies inland water by gameplay
size, with geometry bounds as a deterministic fallback.
Canonical settlement model
A settlement independently records nearby flowing, inland, and marine access within two kilometers. Flowing access is either a river or a river with a nearby canal, so a canal-only settlement state cannot be represented. Inland water is fresh pond/lake/great-lake access. Marine water is either tidal (treated as brackish for gameplay) or open coast (salt water). Absence means the settlement is landlocked with respect to that category, not that salinity is unknown.
These distinctions can drive fresh- and salt-water foods, harbor or fishing work, water transport, irrigation, flood scenes, and local encounter dressing.
Canonical edge model
Hydrology finalizes the road draft. A land route owns zero or more typed river, canal, or ditch crossings, each with its position along the edge and a plausible bridge-or-ford traversal. A ferry instead owns exactly one river, inland-water, tidal-water, or coastal-water payload. Consequently a ferry with land crossings, or a land route with a ferry waterway, cannot be represented.
Straight endpoint-to-endpoint geometry is used because Viabundus currently imports topology rather than complete road polylines. Existing Viabundus bridge endpoint evidence wins over the size-based bridge/ford inference. If a known bridge has no mapped EU-Hydro segment, the compiler supplies a plausible small perennial river crossing at that endpoint. Ferry edges without a nearby mapped water feature receive a plausible small perennial river rather than an unknown waterway.
The subsequent route-terrain stage reuses crossings as zero-distance water adjacencies. The nearest feature of each other category may be retained within the existing two-kilometer threshold when the full official distribution is available. These facts feed only versioned static seasonal-risk and encounter selectors; they do not create tactical simulation state.
Historical land-use world data
The world compiler derives the 1544 land-use profile from HYDE 3.5 c9. HYDE is a global 5-arcminute historical reconstruction: it is regional evidence, not an exact observation of an individual settlement.
- Project and release archive: https://landuse.sites.uu.nl/hyde-project/
- The HYDE 3.5 release README applies CC BY 3.0 to all HYDE data.
Manual preparation contract
Download the three HYDE 3.5 c9 April 2025 NetCDF inputs from Utrecht University's public HYDE vault:
The directory is protected by an interactive anti-bot page, so the repository
initializer does not automate this download. Use a normal browser and retain
the release filenames. The separate general_files.zip release input is still
required for the matching general_files/garea_cr.asc cell-area grid.
Place exactly these release files in the Git-ignored
target/world-data-sources/raw/hyde35-land-use/ directory, or point
--land-use-dir at another directory:
cropland.nc/cropland— cropland area in km²grazing_land.nc/grazing_land— grazing-land area in km²urban_area.nc/urban_area— urban area in km²general_files.zip/general_files/garea_cr.asc— HYDE grid-cell area in km²
For normal development, just init-world-data installs these exact four files
into that directory from the pinned reviewed source-separated input bundle. Its
HYDE component retains a separate notice and exact file inventory; the archive
is not a combined derived world artifact. Manual browser retrieval is the
fallback for preparing or independently auditing the HYDE component. See
wiki/reference/world-data-bundles.md.
The importer requires NetCDF-4 inputs with time, lat, and lon dimensions
in that order, matching 4,320×2,160 global 5-arcminute coordinate grids. It
requires HYDE 3.5's 365_day time axis and verifies matching axes before
sampling. It streams only the requested settlement cells from each large time
slice rather than retaining the full grids in memory.
HYDE expresses land use as areas. The compiler brackets the requested world
year in HYDE's time axis, linearly interpolates cropland, grazing, and urban
area, and divides each by garea_cr.asc. At 1544 this interpolates 44% from
the 1500 snapshot toward 1600. The remaining area is natural/seminatural land.
Small source overlap (up to 5%) is normalized deterministically; greater
overlap, malformed values, partial nodata, or missing source files fail the
build. A cell with no usable complete profile receives the documented
deterministic fallback and is counted in the build report.
Canonical land use is stored as bounded basis-point fractions for cropland, grazing land, built-up land, and natural/seminatural land. The four fractions sum to exactly 10,000 and support agriculture, livestock, encounters, and forest-cover fallbacks. The strategic map compiler separately reads raw interpolated HYDE cropland km² for every source cell intersecting the playable bounds. It does not reuse rounded settlement profiles. Largest-remainder rounding gives each HYDE cell a whole-square quota while preserving the rounded global area with less than 0.5 km² residual.
Those quotas are allocated to exact EPSG:3035 1 km squares by deterministic four-neighbour region growth. All settlements seed one global frontier; settlement-less HYDE cells receive a deterministic fallback seed, and adjacency crosses HYDE boundaries. Final inferred roads are merged before this allocation and therefore participate in both suitability and package identity. Bounded uniform-grid indexes measure exact point-to-line-segment distance without scanning every feature for every square. Suitability strongly prefers settlement proximity, gives roads only short-range influence, prefers an access band near water rather than banks, requires at least 75% of sixteen explicit within-square samples to be non-water passable land, penalizes 15-degree slopes/local relief, and weakly penalizes modern canopy. HYDE's observed historical cropland takes precedence over Jung potential-natural wetland vegetation because the latter describes the uncultivated counterfactual; mapped water remains ineligible. A HYDE cell clipped by the playable boundary may request more area than the arbitrary boundary can represent with complete, usable canonical squares. Rules version 2 deterministically saturates only those clipped edge cells at their usable-square capacity, including zero, and reports the omitted area. Un-clipped cells may saturate only when canonical-grid/raster discretization is at most 2 km² and at most 5% of the cell quota. Larger coastal or usable-land contradictions still fail the build. Soil is deliberately omitted: the current SoilGrids stage exposes settlement samples rather than a bounded map-wide raster, and expanding that compiler surface is disproportionate to this allocator.
The selected square IDs, HYDE source digest, rules version, residual, and counts determine the final terrain package identity. Exact square boundaries are used for both foraging legality and map paint.
Historical summer drought and wetness
Settlement hydroclimate is sourced from the NOAA Old World Drought Atlas (OWDA) v1.0, a tree-ring reconstruction of annual summer Palmer Drought Severity Index (PDSI) across Europe and the Mediterranean.
- Pinned release: OWDA v1.0, dataset DOI
10.25921/rjm6-mq74. - Authoritative file URL: https://www.ncei.noaa.gov/pub/data/paleo/drought/owda.nc
- Publication: Cook et al. (2015), Old World megadroughts and pluvials during
the Common Era, DOI
10.1126/sciadv.1500561. - Pinned file: exactly
228226363bytes; SHA-256c044aa52e9e81932841b642b6977fa6f84beb9fe73c3db502b90f4295b1d65bd. - Grid: 0.5 degree point grid, 114 longitudes by 88 latitudes.
- Time coverage: AD 0 through 2012.
- Maintainer audit/derivation input: NetCDF-4 classic-model/HDF5.
Release maintainers run just init-owda to download or verify the pinned file at
target/world-data-sources/raw/climate/owda.nc. Preparation is atomic: a
temporary sibling is size- and checksum-verified before replacement. An
adjacent ignored .owda-source.json records the version, URL, both DOIs, size,
and checksum. A mismatched existing cache fails closed; use
python scripts/init_owda.py --force only to replace it. Override the compiler
path with --drought-netcdf. The raw file is never a standard developer input
and is never included in a world-data input bundle. The importer uses a
pure-Rust, read-only NetCDF-4 decoder; no NetCDF/HDF5 native library enters the
shared schema or database module.
Standard developer input and release derivation
Developers install the reviewed bundle's
target/world-data-sources/prepared/owda/settlement-profiles-1544.json and run
the ordinary just compile-world. That bounded profile contains exactly one
sorted entry per bundled Viabundus settlement, its selected-year/20-year summary,
and truthful direct or nearest sampling classification. It contains neither
OWDA coordinates nor annual values.
After auditing the raw file and preparing the matching Viabundus
.viabundus-source.json sidecar, a release maintainer produces that
file deterministically with:
cargo run -p adventuresim-world-import -- \
--drought-netcdf target/world-data-sources/raw/climate/owda.nc \
--derive-owda-profiles target/world-data-sources/prepared/owda/settlement-profiles-1544.json
The command refuses a missing grid sample rather than emitting a deceptive
profile, generates (or validates) settlement-ids-1544.json, hashes both
matching Viabundus records into the output, and is
limited to the reviewed 1544 release. Bundle verification checks those hashes,
the exact settlement-ID coverage, integer bounds, and sampling classification.
Source boundary
The parser requires the documented lon=114, lat=88, and time=2013
dimensions; monotonically spaced longitude, latitude, and annual time axes;
and an f64 pdsi(lon, lat, time) variable with the source long name and units.
It reads the twenty summers ending in the selected world year. A cell must have
all twenty finite, three-decimal values or twenty NaNs denoting no source
coverage. Partial histories, infinities, unexpected precision, changed axis
orientation, and values outside the audited PDSI range fail at the source
boundary.
The downloaded 228 MB file was verified directly. Across the complete dataset, 8,826,343 of 20,194,416 values are finite; values span -11.996 to 13.431 and have exactly three decimal places. The 1525–1544 window contains 5,414 complete grid cells with stable coverage.
Canonical model and sampling
PDSI is stored as a bounded signed integer in thousandths, not a raw float. Standard thresholds derive extreme, severe, moderate, or mild drought; normal conditions; and mild through extreme wetness. Each settlement stores:
- its reconstructed 1544 summer PDSI;
- mean PDSI over 1525–1544;
- the number of moderate-or-worse drought summers (PDSI at most -2);
- the number of moderately-or-more wet summers (PDSI at least 2).
The history constructor guarantees that both counts fit the twenty-year window, cannot sum past it, include the current summer in the correct category, and admit the stored mean under the bounded drought, normal, and wet value ranges. Canonical data distinguishes reconstructed profiles from complete inferred profiles; there is no unknown state.
Sampling first uses the containing half-degree grid point. For a coastal or otherwise missing cell, it selects the physically nearest reconstructed point, with longitude distance scaled by latitude, up to 1.5 degrees. A location beyond that limit receives a neutral inferred profile. An audit of all 6,041 Viabundus settlements active in 1544 produced 5,458 direct samples, 583 nearest neighbors, and no fallbacks.
The data can directly affect current harvests, seasonal moisture and yields, water stress and availability, fire risk, forage, river conditions, and travel. The twenty-year history can inform reserves, prices, migration pressure, and whether a single dry or wet summer is locally unusual. Selected-year drought does not permanently assign or alter a settlement's biome.
Attribution and redistribution boundary
The repository and compiled world contain only bounded per-settlement derived values: selected-year PDSI, twenty-year mean, drought/wet counts, and the reconstruction/inference classification with concise provenance. They do not contain source grids, annual series, the complete NetCDF file, journal prose or figures, or rendered journal/source maps. Reusers should cite both the NOAA dataset DOI and Cook et al. paper DOI above. The local source cache remains ignored and must not be committed or redistributed through this repository.
Forest-cover world data
Settlement-scale forest cover comes from the Copernicus Land Monitoring Service High Resolution Layer Tree Cover and Forests, using the 2018 Tree Cover Density (TCD) and Dominant Leaf Type (DLT) products. This is modern data, used as a plausibility input for the game's 1544 setting rather than as a claim about the exact historical tree cover around a settlement.
- Product family: https://land.copernicus.eu/en/products/high-resolution-layer-forests-and-tree-cover
- DLT dataset DOI: https://doi.org/10.2909/82f93572-9888-47ef-97a1-5cac5985a26a
- Terms: Copernicus full, free, and open data policy
Initialize the default playable-area coverage with:
just plan-forest-cover
just init-forest-cover
just verify-forest-cover
scripts/init_forest_cover.py reads COPERNICUS_CLIENT_ID and
COPERNICUS_CLIENT_SECRET from the environment or the Git-ignored repository
.env, without displaying either value. These are Sentinel Hub OAuth client
credentials. They do not authorize direct CDSE OData or S3 object downloads;
the initializer instead uses the official Sentinel Hub Process API and its
public CLMS BYOC collections.
The authoritative playable bounds are 8.965-11.110 degrees east and
50.877-52.211 degrees north. The default integer EPSG:4326 source envelope is
therefore 8-12 degrees east and 50-53 degrees north: the smallest 12
one-degree tiles that cover the playable area. Override them with --west,
--south, --east, and --north. Each request is independently restartable
in a staging directory. The existing source directory is replaced only after
all 24 output rasters verify; the previous directory is retained below
target/world-data-backups/.
When an existing installation already contains valid cells from the requested
envelope, preparation reuses them in staging instead of downloading them
again. This makes narrowing an older broad installation cheap and preserves
the broad set in the normal recoverable backup.
The prepared source inventory records the exact byte size and SHA-256 of every consumed TCD/DLT tile. A source-separated world-data bundle therefore pins the installed result even though a later invocation of the upstream processing service could produce revised bytes. The legacy release verifier still reports this source as release-blocked because it cannot independently pin the upstream Process API result before acquisition; the local prepared result is nevertheless exact and repeatably verifiable after download.
Prepared tile contract
The initializer requests the official 2018 100 m Tree Cover Density collection
(edd3c5f5-da8e-463f-8c9a-712aa451d37e) directly. It derives leaf type from
the official 100 m Broadleaved Cover Density
(a06a42ae-f899-4a07-a5cd-fb7fd920d6c1) and Coniferous Cover Density
(a0edd575-c763-4c4a-a910-631df3df4506) collections. Those density products
are themselves the official aggregation of the 10 m DLT pixels. A cell is
broadleaf or conifer when that type is at least 75% of its classified tree
pixels, mixed otherwise, and 255 when no leaf type applies.
The resulting rasters are 1000-by-1000-pixel
one-degree, EPSG:4326, RasterPixelIsArea, single-band UInt8 GeoTIFFs. This
fixed 0.001-degree grid is approximately 100 m at European latitudes and makes
the prepared format deterministic. They live in the Git-ignored
target/world-data-sources/raw/forest-cover/ directory. A locally prepared
directory contains forest-cover-manifest.json with this version marker:
{"format":"adventuresim-copernicus-forest-2018-v1"}
The pinned, externally inventoried world-data release uses the reviewed
adventuresim-copernicus-forest-2018-v2 marker. V2 changes the distribution
identity, not the raster interpretation: both accepted markers require the
same source year, resolution, aggregation rule, class mapping, filenames, and
strict GeoTIFF validation described here. Other markers and unknown manifest
fields remain rejected. The local initializer continues to emit v1 and verify
its separate source-inventory.json; pinned-release v2 integrity is supplied
by the release descriptor and per-file bundle inventory.
Each used degree tile requires a pair named for its southwest corner:
The marker itself is not a content inventory. Local v1 preparation uses
source-inventory.json; pinned v2 installation is bound by the verified
world-data release inventory. Both pin every TCD/DLT tile by checked size and
SHA-256 outside the marker.
TCD_N48_E002.tifDLT_N48_E002.tif
Southern and western coordinates use S and W. Both rasters in a pair must
be 1000 by 1000, have identical transforms, and span exactly one degree. TCD
values are canopy percentages from 0 through 100. DLT emits 1 for at least
75% broadleaf, 2 for at least 75% coniferous, and 3 for a mixture where
neither type reaches 75%. Use 255 where no leaf type applies or either source
is nodata. This code 3 is part of the preparation contract; it is derived from
the two official density products rather than asserted to be a raw DLT class.
The importer groups settlements by degree tile and reads only tile pairs that
contain settlements. A source density of zero becomes ForestCover::Open.
Positive density becomes ForestCover::Wooded(Woodland), whose bounded
CanopyDensity makes zero-density woodland unrepresentable and whose required
DominantLeafType is broadleaf, coniferous, or mixed. There is no unknown
variant.
If density is nodata, the importer creates a deterministic plausible density from HYDE 3.5 natural/seminatural land use; cells with less than 5% natural land become open. If only leaf type is missing, elevation supplies a deterministic broadleaf/mixed/conifer fallback. The build report counts every settlement where either fallback was used. Malformed GeoTIFF structure, unsupported raster encodings, mismatched paired transforms, and missing required tiles are not silently accepted. Reserved or unclassified cell values take the documented plausible fallback path and are counted.
The settlement Map presentation may additionally generalize any installed TCD/DLT tile pairs into one naturalized forest mask at 20 percent canopy cover. It retains the exact bounded percentage in its offline inputs rather than turning presentation data into sparse/deep classes. Hilly forest is rendered dark green while flat forest is green. Production raster generation first classifies one canonical 0.001-degree canopy mask, then constructs every lower level of detail by area-averaging 2-by-2 child cells. Rendering uses bilinear sampling within a level and blends adjacent mip levels, so zooming changes the amount of retained detail without switching to a separately sampled or procedurally distorted forest mask. This is explicitly partial-coverage presentation data: absent tiles stay absent, tile coverage is recorded in the map package, and no missing regional forest is inferred from a settlement sample.
Published map and terrain packs retain the Copernicus source, modification,
and no-endorsement statements in STRATEGIC_MAP_DATA_LICENSE.md, which the
offline compiler writes beside every output directory. That notice must remain
with redistributed bundles or be available through an equivalent prominent
link.
Forest cover is stored on settlements because it describes the immediate area and can drive timber and foraging products, scene vegetation density, visibility, encounters, and fuel availability. Continuous route or canonical regional forest data still belongs in later spatial products rather than being inferred from one settlement sample; the generalized raster layer is not such a canonical world product.
Potential-natural-vegetation world data
Potential vegetation is sourced from Martin Jung/IIASA, Current and future
European potential vegetation types v1.1, DOI
10.5281/zenodo.14627466. The pinned
version was published 2025-01-10 under CC BY 4.0 and covers continental Europe,
including Turkey, at nominal 1 km grain.
Canonical temporal metadata records the model-input window, 1990–2020, rather
than presenting the publication year as a single observation year.
Run just init-jung-pnv to download the categorical current raster plus the six
current-class COGs into target/world-data-sources/raw/jung-pnv/. The atomic
initializer pins every official filename, byte size, published MD5, and verified
SHA-256, rejects oversized streams, and writes a deterministic adjacent
jung-pnv-manifest.json. Rasters and the manifest are ignored local source data;
they are never distributed with the repository. Use --potential-vegetation-dir
to select another verified initialization directory.
The importer validates the actual source contract: 5,583 by 4,474 Float32 cells,
1,000 m pixels, top-left (944000, 5416000), 512-square Deflate tiles, NaN
nodata, and the source's user-defined GeoTIFF keys carrying the EPSG:3035 LAEA
parameters. Posterior COGs contain seven interleaved samples named
mean/sd/q05/q50/q95/mode/cv; world compilation consumes the mean and validates
the complete band contract. The categorical raster accepts only 1 through 6.
Every import revalidates the manifest's record/version/license/file identities,
the exact byte sizes, and SHA-256 values before opening a raster. COG tiles are
decoded on demand with checked channel lengths and a byte-accounted cache capped
at 64 MiB.
Canonical cells receive nodata-aware area-weighted posterior means. When every class has a valid posterior, six independently quantized 0..10,000 basis-point scores are stored; they are not asserted to sum to 10,000. Otherwise the categorical raster is used, choosing greatest valid overlap with stable class ties. A cell with neither form of source evidence receives a deterministic non-unknown class inferred from already typed forest, elevation, latitude, and HYDE 3.5 context. Reports reconcile posterior, categorical, and inferred outcomes exactly to settlement count.
Inference-rules version 9 and world schema version 25 identify the complete post-hydrology synthesis contract. Older artifacts or caches cannot share identity with Jung-derived or reconstructed historical results.
Potential vegetation remains stored unchanged as the modern-climate ecological envelope. After soil and hydrology finalization, the compiler separately stores dominant 1544 cover. The greatest sampled HYDE 3.5 human fraction is selected first (stable tie order: built, cropland, pasture), then becomes direct only when it meets its own threshold: 10% built or 35% cropland/pasture. Deterministic missing-HYDE profiles are never labeled direct. Natural cover is derived from Jung, Copernicus forest structure, EU-Trees4F candidates, soil/geology, elevation, hydrology, latitude, and OWDA moisture. Only genuinely close natural scores use coordinate-and-schema hashing as a deterministic tie-break. Fallback Jung wetland/marine classes cannot emit water cover: wetlands require wet soil plus freshwater/tidal convergence, while transitional water requires tidal evidence.
Attribution/modification notice: Adventure Simulator downloads Jung's published v1.1 rasters unchanged, then projects settlement cells, area-aggregates posterior means, quantizes values, and applies documented categorical/inference fallbacks.
The terrain pipeline separately extracts a bounded wetland mask from the pinned
1 km wetland posterior (mean >= 0.5), using categorical class 5 only where the
posterior is nodata. Pixel centers are clipped to playable bounds and capped at
100,000 candidates. The pinned SHA-256, accepted pixel count, and terrain package
digest are recorded; this is source coverage, not settlement extrapolation.
Tree-species world data
Plausible settlement tree species come from EU-Trees4F v2, a modeled dataset for 67 European tree species. The importer uses the current-climate ensemble layers as environmental evidence for world generation; it does not treat them as observations from 1544.
- JRC project page: https://forest.jrc.ec.europa.eu/en/activities/forests-and-climate-change/
- Dataset and CC0 licence: https://doi.org/10.6084/m9.figshare.17032328
- Data descriptor: https://doi.org/10.1038/s41597-022-01128-5
- Archive SHA-256:
be115f771e5598e6fd180621e1a32922880cf7ac8e2cb59ba0eabd7f15bfeda4 - Archive size:
73,796,217bytes - Pinned JRC ENS_CLIM URL: https://ies-ows.jrc.ec.europa.eu/efdac/download/EU-Trees4F/EU-Trees4F_ens-clim.zip
The automated identity applies to that exact JRC ENS_CLIM archive. Its byte equivalence to a Figshare-hosted archive has not been established and remains an explicit confirmation blocker; the EU-Trees4F v2 Figshare citation and CC0 notice are retained without making an equivalence claim.
Keep EU-Trees4F_ens-clim.zip at
target/world-data-sources/raw/tree-species/. just init-tree-species
downloads into a temporary candidate, verifies size and SHA-256, publishes a
content-addressed generation and canonical sidecar, then atomically replaces
the active archive. --force is required to replace an invalid existing file.
Use plan-tree-species or verify-tree-species for non-mutating workflows.
Override it with --tree-species-archive.
Source semantics and parsing
For each species, the importer requires one complete current-climate triplet:
prob_pot: modeled habitat suitability on a 1/12-degree EPSG:4326 grid, stored as an integer score from 0 through 1000. This is not abundance or a literal occurrence probability.bin_pot: the suitability surface after the source's species-specific threshold, on a 10 km EPSG:3035 grid.bin_nat: the thresholded surface masked by the source's expert native-range evidence, on the same 10 km grid.
The cur2005 filename label is a reference scenario, not a direct 2005
observation. The model combines occurrences centered roughly around 2005 with
older climate normals that better represent conditions under which established
trees grew. One retained species, Robinia_pseudoacacia, is naturalized rather
than historically native to Europe.
The importer reads the DEFLATE-compressed ZIP in memory-bounded chunks without
extracting paths. It requires the pinned 67-species current-layer manifest,
validates signed Int16, nodata, dimensions, PixelIsArea transforms, CRS keys,
units, and layer value domains, and rejects impossible bin_nat=1/bin_pot=0
states. Probability and binary rasters deliberately use different grids and
are sampled independently. A binary candidate whose probability cell is
nodata is omitted rather than assigned an invented score.
Canonical model and use
Each settlement stores TreeSpeciesProfile, with no unknown state:
Modeledcontains the twelve highest-suitability candidates at most. Every candidate has a validated scientific-name identifier, a bounded suitability score, and either within-native-range or outside-native-range evidence.Inferredcontains a nonempty, duplicate-free list selected from typed potential vegetation when the rasters yield no modeled candidate.
The constructors keep profiles nonempty, unique, bounded, and deterministically ordered. The same constructors are used by JSON parsing and the SpacetimeDB import reducer, so invalid states cannot bypass the source boundary.
The official archive was decoded in full: all 201 current rasters passed their contracts. Sampling all 6,041 active Viabundus settlements produced modeled profiles for 5,986 and deterministic vegetation-based profiles for the remaining 55, retaining 71,053 ranked candidates in total.
Game systems can use these profiles for timber and fruit products, fuel and charcoal availability, forest tactical scenes, shipbuilding or carpentry materials, forage, and visual biome selection. These are plausibility inputs, not claims that a named settlement historically grew every listed species.
SoilGrids prediction and soil finalization
Settlement soil is based on ISRIC SoilGrids, rolling version 2, under CC BY 4.0. Restricted legacy European vector data is not an accepted runtime or distributable input.
The compiler consumes a prepared, content-addressed EPSG:3035 subset rather than making network calls. The fixed contract contains 250 m predictions for six depths (0–5, 5–15, 15–30, 30–60, 60–100, and 100–200 cm), and Q0.05, Q0.50, mean, and Q0.95 for sand, silt, clay, coarse fragments, organic carbon, pH, CEC, and bulk density. SoilGrids publishes the two water-retention products only as 1 km aggregated means, which are resampled onto the requested canonical grid without inventing unavailable quantiles. It also contains the most-probable WRB group and Histosols/Leptosols probabilities.
Preparing data
The initializer defaults to a plan because European preparation is large and requires GDAL:
python scripts/init_soilgrids.py --grid-cell-size-meters 1000
python scripts/init_soilgrids.py --prepare --grid-cell-size-meters 1000
python scripts/init_soilgrids.py --verify-only --grid-cell-size-meters 1000
Only strictly constructed official files.isric.org WebDAV/VRT URLs are used.
GDAL opens the exact allowlisted master VRT through /vsicurl/ so relative tile references
remain attached to the official URL. Only the exact HTTPS host and
/soilgrids/latest/ path are accepted. Metadata-probe redirects are disabled
and fail closed; a metadata redirect requires an explicit code/source-contract update. Preparation is
fixed to the aligned EPSG:3035 Europe extent and invokes gdalwarp with
-t_srs EPSG:3035 -tr N N -tap. The atomic manifest records retrieval time,
source inventory, source and prepared sizes/SHA-256 hashes, extent, origin,
CRS, and cell size. A complete generation is staged under a content-addressed
directory and becomes active only when the root manifest pointer is atomically
replaced, so a failed 207-file build cannot corrupt the prior generation. The
Rust importer rechecks inventory, hashes, dimensions, nodata, Float32 band and
compression shape, EPSG:3035 GeoKeys, transform, units, and canonical grid.
Interrupted preparation preserves its private .soilgrids-staging directory.
Each completed raster is validated and hash-checkpointed before the next layer
begins, so rerunning the same --prepare command reuses only those verified
layers and retries the remainder. An output from an interrupted gdalwarp that
was not checkpointed is discarded; an incomplete staging directory is never
included in a bundle or selected by the importer. Network retries use bounded
exponential backoff (10 seconds, doubling to a five-minute cap) for both
metadata retrieval and complete GDAL layer attempts.
latest is a mutable rolling publication, not an immutable source pin. The
manifest therefore records source_reproducibility: unpinned-rolling-latest,
observational source SHA-256/size, ETag and Last-Modified when supplied, and
exact prepared hashes. Because rolling source bytes can change between the
observation and GDAL requests, those source fields do not bind the prepared
bytes. The prepared SHA-256 is the authoritative local snapshot; future raw
The canonical source manifest uses the strict preparation manifest's actual
retrieved_at value and prepared-manifest digest. This identifies the local
snapshot but does not claim that the rolling latest source is reacquirable.
reacquisition is not claimed reproducible.
Preparation requires both gdalwarp and gdalinfo. Every staged TIFF must pass
end-to-end JSON inspection (fixed size/extent, exact transform, EPSG:3035,
single Float32 band, NaN nodata, and DEFLATE) plus a second prepared hash check
before the active manifest is atomically replaced.
The initializer configures GDAL /vsicurl/ retries for transient HTTP 429/500/
502/503/504 responses and retries a failed layer a bounded number of times after
removing only that layer's incomplete staged TIFF. It never publishes a partial
generation; after the bounded retries are exhausted, rerun --prepare once the
official service is healthy.
The fixed continental extent currently fits the importer's 32-million-pixel
bound only at exactly dividing sizes of 1 km or coarser (for example 1 km and
5 km). Although the global SpatialGridSpec still permits 250 m, SoilGrids
250/500 m preparation is rejected pending tiled ingestion; 750 m is rejected
because it does not divide the extent exactly.
The repository does not claim a complete official European audit until those layers are prepared. Plan and unit-test modes remain useful without GDAL.
Typestate and rules
- After Jung PNV and EU-Trees4F, depth values are thickness-weighted into 0–30 cm summaries; water capacity uses 0–100 cm. Quantiles are aggregated separately and checked for ordering. Texture sums, units, nodata, ranges, and grid identity are trust boundaries.
- A private prediction draft retains exhaustive WRB, Histosols/Leptosols probabilities, texture, water, carbon, stones, acidity, CEC, fertility, confidence, and modeled-or-inferred evidence.
- Geology consumes that prediction and elevation. Parent material in final soil is typed geology lithology, never a free-form soil-source code.
- Religion and drought run; EU-Hydro returns a resolved hydrology draft.
- The soil finalizer combines prediction, geology, hydrology, Jung wetland evidence, and elevation. Peat requires Histosols probability, wet PNV, and hydrology together—organic carbon alone never creates peat. Shallow/rock uses Leptosols plus geology/elevation. Drainage/flooding use WRB, retention, and hydrology.
- The final historical-environment stage consumes the finalized profile. It can use acidity/fertility for heath, shallow/rocky/dry conditions for sparse cover, and convergent soil/hydrology/Jung evidence for wetland. It does not mutate peat from SOC alone, invent slope/roughness, or replace geology-derived parent material.
Rules are deterministic and versioned. A full prepared-source audit remains blocked by the absent official HYDE 3.5, forest, EU-Hydro, and prepared SoilGrids inputs; this repository does not claim full #67/#68 source coverage.
Surface geology
Settlement surface geology is sourced from the EGDI 1:1 Million
pan-European Surface Geology dataset (EGDI-GE-1M-SURFACE, created
2016-05-04). It aggregates national geological-survey data using INSPIRE and
OneGeology lithology and geochronology codelists.
- Metadata: https://metadata.europe-geology.eu/record/full/5729ffdf-2558-48fc-a5d2-645a0a010855
- WFS catalogue: https://maps.europe-geology.eu/wfs/
- License: Creative Commons Attribution 4.0. The metadata includes an additional attribution/disclaimer for the Maltese contribution.
- Scale: 1:1,000,000.
- Compiler input: GeoPackage, EPSG:3034 (
ETRS89-extended / LCC Europe).
Preparing the source
Place the exported GeologicUnitView.gpkg at
target/world-data-sources/raw/geology/GeologicUnitView.gpkg. The world
compiler reads it directly; --geology-geopackage overrides the path. The
checked source boundary requires the GeologicUnitView feature table, its
geom polygonal geometry column, EPSG:3034 metadata, and the
rtree_GeologicUnitView_geom spatial index. The boundary verifies the EPSG
authority mapping and requires every non-empty GeoPackage geometry (but not an
OGC empty geometry) to have exactly one usable index entry.
The official WFS can return GeoJSON samples, but the full layer contains more
than 240,000 features. Preparing a local indexed GeoPackage avoids repeatedly
downloading or scanning the service. The accepted aggregate still lacks a
committed exact size and SHA-256. just plan-geology prints the fixed contract,
init-geology refuses acquisition, and verify-geology validates a strict
local source-inventory.json. The source remains release-blocked.
Imported model
For every settlement the importer projects its coordinates into EPSG:3034, queries the GeoPackage R-tree, and performs an exact point-in-polygon test on the candidate GeoPackage geometries. It retains a bounded geologic-unit identifier and reduces the source codelists to typed gameplay categories:
- unconsolidated deposits such as clay, sand, gravel, till, peat, alluvium, loess, and volcanic ash;
- sedimentary rock such as limestone, chalk, sandstone, shale, evaporite, coal, and chert;
- igneous rock such as granite, basalt, gabbro, rhyolite, and tuff;
- metamorphic rock such as slate, schist, gneiss, quartzite, and marble;
- mixed rock, breccia, and mélange.
Geologic age is reduced to typed intervals from Quaternary through Precambrian,
including broader Cenozoic, Mesozoic, Paleozoic, and Phanerozoic source terms.
Within a mapped EGDI unit, lithology and age each independently record whether
they came from the source attribute or from a deterministic inference. An EGDI
polygon whose age is the source marker unknown therefore remains a mapped
unit with an inferred age. A wholly inferred profile is a different canonical
type containing bare values, so it cannot falsely contain mapped evidence;
canonical data never stores an Unknown variant.
Unrecognized but present lithology terms become typed mixed rock. A missing lithology becomes plausible sandstone. Missing ages are inferred from lithology: unconsolidated deposits are Quaternary, coal is Carboniferous, other sedimentary rock is Jurassic, crystalline igneous/metamorphic rock is Precambrian, and mixed rock is Paleogene. Settlements outside source coverage receive a complete inferred setting based first on their SoilGrids prediction, with sandstone as the general fallback.
These categories provide quarry/building-stone, clay, chalk, slate, salt, coal, cave/karst, mining, architecture, and tactical-material priors without claiming that every plausible resource was historically exploited.
Verification
Parser tests create a small real GeoPackage boundary, including the application ID, geometry metadata, spatial index, and GeoPackage binary multipolygon. An ignored test can verify a full manual download:
$env:EGDI_GEOPACKAGE = "C:\path\to\GeologicUnitView.gpkg"
cargo test -p adventuresim-world-import samples_downloaded_egdi_geopackage -- --ignored
The downloaded 675 MB file was verified with 243,092 feature rows, its EPSG:3034 metadata, spatial index, and a real mapped sample.
Official religion in 1544
Settlement religion is reconstructed from the Institute of European History (IEG) maps of the legally recognized religion of European territories in 1500 and 1555. This models public law and institutions, not personal belief.
- IEG map collection: https://www.ieg-maps.uni-mainz.de/mapsp/mapconfession.htm
- Source rights statement: © IEG Mainz / Andreas Kunz. The source page does not state an open-data license; only the project's coarse derived intermediate is checked in.
- Reference maps:
IEG_Europe_1500_religion.gifandIEG_Europe_1555_religion.pdf. - Game year: 1544.
The published maps are illustrative rasters without a machine-readable
geographic boundary layer. The checked-in
assets/world-data/ieg-religion-1544.csv is therefore a deliberately coarse,
human-curated intermediate between the two maps. Each row is a named bounding
region with an explicit unique priority; lower numbers are evaluated first, so
small territories override broad ones even if the file is reordered. The
shapes are gameplay priors and do not claim to reproduce historical borders
exactly.
Settlements outside the curated regions receive a complete plausible fallback: Roman Catholic in the general Viabundus coverage and Eastern Orthodox in the far-eastern Ruthenian portion. No canonical record stores an unknown religion. The compiler refuses to use this fixed intermediate for a year other than 1544.
Imported model
Canonical data distinguishes:
- an established official religion;
- parity, where two recognized western confessions have equal legal status;
- multi-confessional status, where multiple confessions are legally present;
- religion determined at the municipal level.
The supported denominations are Roman Catholic, Lutheran (the IEG Wittenberg Reformation category), Reformed, Anglican, Eastern Orthodox, and Islamic. Pair arrangements carry a pair-specific church enum, so the settlement's current single church cannot name a denomination outside the legal arrangement. The existing church/priest gameplay identifier is derived from that typed denomination during database import rather than supplied as a second potentially contradictory field.
The compiler reports the number of curated regions, settlement samples, and
fallback samples. --religion-regions can point to another intermediate using
the same checked CSV boundary.
The checked intermediate was audited against all 6,041 Viabundus settlements active in 1544. Its 13 explicit regions omit places for which the historical source identifies only generic Protestantism, allowing denomination inference to resolve those settlements without creating a catch-all canonical religion. Remaining unmatched settlements use the Roman Catholic fallback. The Upper Rhine multi-confessional approximation retains its specific priority over the broader Hessian region.
just verify-religion validates the committed file's fixed size, SHA-256,
exact column order, ascending unique priorities, coordinate bounds, statuses,
and 13-row revision. plan-religion reports that identity.
init-religion always refuses so the rights-reserved GIF/PDF source images are
never downloaded or mirrored.
Strategic industries and commodities
World schema v21 and inference rules v6 attach a nonempty, bounded
InferredIndustryProfile to every imported settlement. The profile describes
plausible local production in 1544; it is strategic world data, not a tactical
simulation or a market inventory.
Industry inference runs after route-terrain finalization. It consumes only the canonical HYDE 3.5 land-use reconstruction, historical vegetation, finalized SoilGrids/EGDI soil and lithology, OWDA moisture, EU-Hydro access, settlement population, and incident finalized routes. Scores and thresholds use integers and basis points. Outputs are sorted, unique, and limited to 24 per settlement.
Routes can downgrade Regional production to Local or Marginal; they never
invent a crop, fishery, deposit, fuel, or construction material. Mining
currently contains coal only and requires explicit coal-bearing sedimentary
geology. Crystalline rocks never imply metals.
Derived outputs cover agriculture (grain, flax, wool, dairy, hides), freshwater, estuarine, and marine fishing, exact mapped quarry stone, coal mining, clay and earthenware pottery, convergent peat cutting, woodland products and charcoal, evaporite/saline/coastal-fuel saltmaking, and evidence-backed construction inputs. Peat requires an organic/Histosol/peat parent plus wet convergence; topsoil carbon alone is insufficient. Coastal brine requires open-coast access and woodland or peat fuel, so Baltic access alone does not imply solar salt.
If no derived output clears its threshold, exactly one marginal fallback is chosen in stable precedence: freshwater fish, grazing dairy, cropland grain, woodland fuelwood, then common aggregate. A fallback cannot claim regional scale or an arbitrary resource.
Offline validation and the SpacetimeDB reducer recheck profile bounds, canonical ordering, route scale limits, and resource evidence. Build-report counters reconcile settlements, derived/fallback outputs, and every industry category. Required Markdown provenance fails closed when the source-note bound cannot hold it.
The complete official-world audit remains blocked until all upstream distributions are available locally. The synthetic rules matrix is deterministic and does not close issue #62 by itself.