Feature/cwn vehicle seating - #83
Merged
Merged
Conversation
added 30 commits
August 13, 2026 21:31
Phase one of CWN vehicles: the sheet block and firing a mounted weapon. No token, no map presence — a vehicle is somewhere to keep stats and roll from, which is what CWN needs and what SR6 will later need more of. Mounts nest under their vehicle — vehicle1_weapon1_dmg — so a mount belongs to a vehicle rather than to a shared pool. That naming is what lets one resolver read both: getWeapon takes a prefix and a row count, and `vehicle1_weapon` is as valid a prefix as `weapon`. Every existing caller passes two arguments and is unaffected, which a test pins. A mount is fired as its own action by a gunner, not as a variant of your personal attack, so the client names the vehicle and the socket handler routes to getVehicleWeapon. Merging mounts into the personal weapon list would have implied you can fire the turret while driving. The section reuses layout 'weapons', which is the row-chunking layout rather than a weapons-only one — the name is historical. That avoids a new SectionLayout member and a renderer branch doing exactly the same thing. It does mean the row shape must divide evenly into `columns`, so each vehicle is exactly three rows of six and notes live in their own section, as they already do for weapons. Armour here is Armour Rating, subtracted from damage, where personal armour is AC and avoids the hit. They are not interchangeable and the field hint says so — applying it is phase two, along with the moving/stationary AC and destruction at zero HP. Driving needed nothing: rolls.js already has drive mapped to dex_mod, so the existing skill roll is the check.
Phase two, and stopping short of wiring on purpose. vehicleAc, applyArmorRating and vehicleDestroyed encode the arithmetic from the CWN vehicle combat rules. The AC one is worth having in code rather than at the table because the two cases are not symmetric: moving adds the driver's Drive skill, stationary applies a flat -4. An implementation that read "+/- driveSkill" would look right and be wrong, so there is a test for exactly that. Armour Rating subtracts from damage. Personal armour is AC and decides whether the hit lands at all. Confusing the two is the likeliest mistake in this subsystem, so they are named differently, documented against each other, and tested against each other. What is deliberately not done: applying any of it to a live attack. getAttackTarget resolves only token rows, and makeEmitResult needs the target's x/z for the attack line — so a vehicle held on a sheet cannot be the target of an attack at all. Wiring these needs a way to target a vehicle, which is the token question that has been open since the spec was written. Building a fake wiring around that would have been worse than leaving the seam visible. Crash damage on destruction is also absent: the rules give "damage from the last attack, Luck save for half" but no dice for the speed-based component, and inventing numbers for someone's ruleset is not mine to do.
Six vehicles declared and all six rendered gave eighteen rows of empty fields on a blank sheet — which is what the screenshot showed, and not what was asked for. Sections can now declare `groupSize`, the number of fields in one repeated entry. A section that declares it shows only the entries that have data, plus one blank to type into, plus a button revealing the next. Weapons and spells declare nothing and render exactly as before. Visibility is derived from the data rather than remembered, which answers the second half of the request for free: the vehicles you filled in are the ones that come back after a reload or a login, because nothing has to remember anything. Whitespace counts as empty, so a stray space cannot pin a row open, and any field counts as data — someone may fill in a mount before naming the vehicle. Also fixes a header the screenshot exposed: every row sat under the vehicle line's labels, so a mount's name appeared under VEHICLE and its damage under HP. Each row of an entry now carries its own headings, shown once on the first entry since the shape repeats. Persistence needed nothing. updateSheetField writes data[fieldId] with no whitelist, so vehicle fields are stored like any other and read back whole. One mismatch caught in passing: the sheet was raised to six vehicles while the resolver still accepted three, which would have rendered vehicles whose mounts silently refused to fire. Both are six now and a test reads the template to pin the pair.
Three requested changes to the Cyberpunk RED stats block: BODY gains a roll. That needed both sides, not just the button: the sheet template renders the control, but the roll itself is resolved server-side by field id through getRoll, and CPR_STATS did not list body. Adding the template's roll alone would have drawn a button that failed when pressed. It now rolls 1d10 + @Body with explode10, like every other stat — a stat roll that did not explode would be quietly wrong rather than visibly broken. BODY takes MOVE's place and EMP takes BODY's, which leaves the grid reading INT REF DEX TECH COOL / WILL BODY EMP LUCK MOVE. The two without buttons end up last, and there is a reason they have none: MOVE is a movement allowance rather than a check, and LUCK is a pool you spend. Both now say so in a comment on each side. The third test is the general form of the bug rather than the instance: every field the template gives a button to must have a server-side roll. The two live in different files and nothing else made them agree. Checked it fails with body removed again, rather than assuming it would.
Version bump, changelog for the CWN vehicles and CP:R stats work, and the README structure caught up with the sheet files and two new test files it did not list.
The server has taken a vehicleIndex beside the weaponIndex since the mounts landed, resolving a mount through the same path as a carried weapon. The panel never sent one, so a mount on the sheet was unreachable. The picker now lists mounts after the carried weapons, labelled with their vehicle. Rows are keyed by the (vehicle, mount) pair rather than by index, since mount 1 on two different vehicles are both weaponIndex 1 and one would otherwise shadow the other. Counts come from the template constants, because listing fewer than the sheet declares silently strands the last vehicles' mounts. Vehicles hang off a config entry, so the other two systems sharing this panel are unaffected even if their sheet data happens to hold the same field names.
One block rather than one per vehicle, because you can only be inside one at a time. Riding along points at another player's sheet by name, so a car with four people in it has one set of HP and armour instead of four copies that each take the damage separately. Moving is declared rather than inferred from the token being dragged: it is an eight point AC swing between moving and stationary, and inferring it from a drag would make it accidental. Fields only - nothing reads them yet.
readOccupancy reads the declaration off a sheet; getVehicle reads the vehicle off whichever sheet holds it. Every unreadable state - an index past the rows the sheet declares, a ride naming nobody, a row with no HP maximum - comes back as on foot, which is how a character was attacked before vehicles existed. A broken reference should cost someone their cover, never make them impossible to hit. Nothing calls either yet.
Armour Rating cuts the damage and the rest comes off the vehicle's HP on its owner's sheet. Nobody inside is touched until it is destroyed, at which point it stops being cover and the next shot resolves against them normally. The defender's sheet is now read once before the to-hit rather than lazily at damage time, because whether they are in a vehicle decides the AC. It was already being fetched twice further down, so this is one fetch where there were two. A rider's damage lands on the owner's sheet, so four people in one car share one HP pool instead of each taking the hit separately. Firing out of a moving vehicle takes -4, whether the gun is a mount or one you are leaning out of the window with, and the roll history names the vehicle and shows the armour subtraction so an absorbed hit does not read as a bug.
Occupancy is mirrored from the sheet onto the token, alongside the name and description that were already mirrored there - only the derived combat numbers, never the sheet. Riders are re-mirrored when the owner saves, because a passenger's badge is read off someone else's sheet and would otherwise go quietly stale. Attackers see TARGET IN VEHICLE with the AC, Armour Rating and HP before they commit, and the token carries a badge that is always drawn rather than gated behind hover: cover you have to hover to discover is cover nobody accounts for. The attack prompt also quotes the vehicle's AC rather than the occupant's, since the vehicle is what the shot has to beat. resolveOccupiedVehicle now delegates to the same resolver the mirror uses, so what the badge claims and what the attack applies cannot drift apart.
Mounts resolved off the attacker's own sheet, so the only person who could use a turret was whoever owned the vehicle - while the sheet's own tooltip says firing a mount costs the gunner their main action. A dedicated gunner is a role the rules describe and the code could not do. The resolution needed nothing: rollToHit already builds from the attacker's sheet, so a gunner fires someone else's cannon with their own skill and attribute mod. Only the weapon row moves. The client cannot label what it cannot see, so the server sends the ride's mounts down beside the sheet - only the mount rows, and only to someone who has declared they are sitting in that vehicle. Firing one carries no vehicle index, since the car is whichever one they have declared they are in: nothing for the client to name and nothing for it to get wrong. No seat gating and no action-economy checks. Nothing else in the combat system polices how often you fire, and mounts should not be the exception.
The 1.8.2 entry described the vehicle rules as encoded but not wired, which stopped being true. Rewritten to describe what actually landed: mounts that fire, a gunner who can use a car they do not own, attacks that hit the vehicle, and the badge that makes the cover visible before someone shoots at it. Also notes the new locations column and the reordered defender-sheet read.
The dropdown listed MY VEHICLE 1 through 6 whether or not any of them existed, so picking your car meant knowing which row it sits in - and six placeholder rows are six ways to pick the wrong one. Options can now be derived from the sheet's own data, so the list holds the vehicles you have named, by name. The vehicle you are currently in stays listed even if its row is later emptied. A select whose value is not among its options silently displays the first one instead, which would quietly move you into a different car. Also drops the stray blank: the renderer always prepended an empty option, so a template naming its own blank state got two.
The field holds a login name and was free text, so a typo dropped you back to being on foot with no cover and nothing on screen to say why. It is now a dropdown of the other players holding a sheet in this system - the right list rather than whoever happens to be online, since the car is read off their sheet and that exists whether or not they are connected. WHOSE never said whose. It is OWNER, and the tooltip now says it means the other player rather than you. Choices a template cannot derive from its own data arrive through a renderer context rather than a prop threaded through five section layouts for the sake of one field. An owner who is no longer listed still shows, tagged, instead of the field appearing blank while it holds their name.
OWNER claimed something about the vehicle when the field describes an arrangement, and it read as a seat role next to SEAT. RIDING WITH says it from the passenger's side and pairs with RIDING IN. The badge and the attack panel now name everyone in the car, which is the thing you actually ask at the table - three people behind one Armour Rating is a different decision from one. Shown only when the car is shared; your own name back at you is noise. No seat count. Recording a capacity would only matter if something enforced it, and the software does not overrule the table on how many fit in a car any more than it counts actions. The mirror recomputes every player rather than the one who saved: boarding someone's car changes what their badge says as much as yours, and working out which subset to refresh means working out the whole thing anyway. That also retires the rider-refresh special case for less code than it replaced.
A vehicle declares a layout and the layout decides what seats exist, so a motorcycle does not show four empty doors. BIKE, CAR and VAN to start; adding one later is an image and a seat list. Anything without a layout is a car, which is what every vehicle that already exists is. Movement moves off the occupant onto the vehicle. Each person declaring it separately meant a driver and a passenger could disagree about an eight point swing in the car's AC, and with a shared window there is one obvious place for it to live. The attack path resolves the attacker's vehicle to find the firing penalty rather than reading their own sheet, since the car may be someone else's. Layout and movement are stored but not rendered on the sheet: the vehicle row is exactly eighteen fields and the renderer chunks by that, so adding two would mean inventing four more to fill the row. The window is where both belong. Seat ids exist on both sides - validated here, drawn there - and a test reads the frontend source and fails if they drift, the same way the roll buttons are pinned to their server rolls.
seatIn, seatOut and setMoving, with the checks the window should never be able to trip: a seat the vehicle does not have, a vehicle that is not there, a player with no sheet. A seat holds one person and a person is in one seat, so seating someone empties whatever seat they were in and turns out whoever was in this one. That is what choosing a name in a dropdown means - a statement about where everyone is, not an addition. Anyone may seat anyone, because piling into a car is a decision the table makes out loud and the window only records it. Getting out is the occupant's alone, or the GM's, and that is checked on the server rather than by hiding a button: a client sends whatever it likes.
Ten presets from the CWN table. Picking one fills the stat block; armour is left unset on the vehicles the book marks * or ** because those are immunities rather than numbers, and inventing a rating would be worse than the GM reading the note the preset writes. Hardpoints bound the mounts. A Motorcycle carries none and must not be offered a gun; a Tank carries three and was short one. Mount rows go from two to three. Crew is the seat count, so seats are derived from it rather than declared per vehicle type: ids are positional, driver then seat2..seatN, and the server needs only the number to validate. The names a player sees live with the presets, where they are display text that cannot disagree with anything. Guns are deliberately not seats. A Tank is crew 3 with three hardpoints, so it can never man every gun and drive at once; making each hardpoint a seat would hand it a fourth body and quietly resolve a tension the rules intend. Speed becomes the number the book prints rather than free text, and the vehicle carries its own Trauma Target - hits on a car should not roll against the trauma target of whoever happens to be sitting in it. Retires the IN A VEHICLE block and the data-driven select options added for it: seating is shared state now, and an abstraction with no users is worse than re-adding it when something needs it.
Seven shapes for ten vehicles. Sharing is fine where the silhouette is honest - a Truck and an APC are both boxes on wheels from above - but a Helicopter and a Dropcraft are not the same object and should not borrow one outline, so the helicopter gets a rotor disc and tail rotor and the GEV a hover skirt rather than the tank's tracks.
The vehicle descriptions contradict three of the shapes. A Micro Flyer is a microlight - fabric over spars, light enough to break down into a pickup - not the jet fighter it was drawn as. A CASRA is a Close Air Support Rotorwing Aircraft, so it is rotors rather than wings. A Dropcraft is a VTOL transport built to loiter and unload under fire. All three had shared one fixed-wing outline, which was not a simplification but a wrong drawing. The APC gets its own hull too: the book leads on its armour and its mounted weapon, and borrowing the delivery van said neither. One shape per vehicle now.
Picking a TYPE fills the stat block from the book table, and the row draws only the mounts the vehicle has hardpoints for. A motorcycle showed three empty mount rows and an empty stat block, which stated something false about it and left the numbers to be copied in by hand. Two general mechanisms rather than a CWN special case: a select can declare presetFill, writing sibling fields when it changes, and a section can declare rowHidden to drop a row of a repeated entry. Both are template data, so any system can use them. Picking a type will not rename a vehicle someone has already named, and the immunity rule for the * and ** vehicles is appended to the notes once rather than on every re-pick. A mount past the hardpoints is hidden only when empty. Hiding one with data in it reads as loss, and a GM may have overloaded a vehicle deliberately - which a pre-existing test caught before it shipped.
Helicopter, APC, GEV and CASRA all have Trauma Target 10, not 12, and the Dropcraft has Armour 12, not 10. All five came from transcribing the table out of a low-resolution image, and none of them looks wrong once entered - a vehicle with the wrong TT just takes traumatic hits slightly too rarely. Verified against the PDF this time. A test now holds the ten rows verbatim in the book's own column order, so the next transcription error fails there rather than at someone's table.
Picking a type filled the row and then reverted it. Each field change is a separate read-modify-write of the whole sheet blob on the server, so the eleven a preset sends raced, all but one were overwritten, and the losing values came straight back on the echo. They go as one importSheetFields now - a handler that already existed for exactly this, one read and one write. Vehicles had no way out. Each filled entry gets a REMOVE that blanks its thirty fields in one write, behind a confirm click because a stray click on that would be expensive. Entries blank in place rather than shifting up: occupancy refers to vehicles by position, so closing the gap would silently move a passenger into a different car. Editing a vehicle now refreshes the badges too. Only name and description were mirrored, so changing a car's armour left every passenger showing the old number - their sheets were never touched, and nothing else was recomputing them.
The ten weapons a hardpoint can carry, as a TYPE picker on each mount that fills damage, trauma and skill. Five are purpose-built vehicle weapons; the rest are Heavy weapons from the personal tables that a mount can hold, and two of those have no fixed damage at all - a grenade launcher fires whatever you loaded. TYPE takes the column SHOCK had. No vehicle weapon in the book has shock, so a picker earns the space more than a field that is always blank. The book's damage notation carries markers the dice roller cannot read, so the damage stored is clean dice and the ! rides on the trauma value instead: 'd20/x4!'. That marker turns out to be a rule I was getting wrong - only weapons carrying it can inflict Traumatic Hits on a vehicle, and every trauma die was working on cars. A pistol's trauma die is devastating to a person and does nothing at all to a car. A vehicle also rolls trauma against its own Trauma Target rather than against that of whoever is sitting in it.
A section always showed one entry past the last filled one. That predates the + ADD button and duplicated it, so a single vehicle rendered as two - the second full of ghost placeholder text that reads like real data at a glance. Only entries holding something show now, plus one on an empty sheet to start from, and + ADD is how you get another. Removing one clears the blank it revealed. Notes move onto the vehicle. One box at the foot of the page for six vehicles cannot say which one it is describing, and the immunity rule a preset writes had nowhere sensible to land. Each vehicle carries its own, spanning the grid on a row of its own, and the preset appends to that one - never over what a player already wrote there. Repeated entries are now grouped by field count rather than row count: rows vary in width once one of them spans the grid, so counting rows sliced the groups in the wrong places. Existing vehicles_notes content is orphaned by this - it was one shared field and there is no way to know which vehicle any line of it referred to.
The 24 fittings from the book as a per-vehicle list: add, remove, and a running POWER and MASS line against the hull's totals that turns red when overloaded. Mounted weapons count toward the same budget, because the book is explicit that a hardpoint costs Power and Mass just as a fitting does - leaving them out would flatter every armed vehicle. A list rather than a dropdown filling fields, because a fitting comes off again. Several rewrite the stat block - Extra Durability is +25% HP, Hardpoint Support adds a mount - and a control that wrote those numbers in would have no way to take them back. The effects are printed on the chips; the numbers stay yours. Power Systems carry a negative drain since they supply Power rather than spending it, which keeps the budget a plain sum instead of two special cases. Only fittings the hull is big enough for are offered. POW and MASS join the vehicle stat block, and the book-parity test now pins them too. That made the stat block fourteen fields, which does not divide into rows of six - so a mount began midway along a row and stopped being recognisable as one, hiding no mounts on a motorcycle. Fields can declare startsRow now.
Fourteen stat fields do not divide into rows of six, and CSS grid flows a short row's spare columns full of whatever comes next - so POW and MASS shared a line with MOUNT 1, and every label after that sat above the wrong field. Each row starts at column one now, which forces the break. The stat block reorders while fixing it, so the short row is the one that matters least: identity and defence, then capacity and budget, then cost and size on their own. A sparse row of reference data reads better than a sparse row of numbers the fittings budget depends on. The full-width labels were also inheriting the section's centring.
Two Power Systems and a tool rack showed POWER -6/8. Netting a supply off the drain makes the spend go negative, which reads as nonsense - you have not used minus six Power. The book calls them 'adds Power at a cost in Mass', so they raise the total: the same vehicle now reads POWER 0/14 (+6). The number left of the slash is what is being used and never goes below zero, and the supply is visible as the reason the total grew.
The piece the redesign was missing. Retiring the IN A VEHICLE sheet block left the seating actions with nothing calling them, so the branch could not ship. A car picker across every player's sheet, the vehicle's wireframe with a leader line and a dropdown per seat, a MOVING toggle on the car, and its AC, AR, HP and crew count so the table can read a vehicle without opening anyone's sheet. The server grows a roster: occupants are spread across their own sheets - the car does not know who is in it, the passengers know which car they are in - so one pass turns that inside out. Only the numbers a table needs travel. Seat anchors are generated rather than hand-placed. Crews run from one to sixteen, and sixteen tuned positions per vehicle would be a lot of numbers to get subtly wrong; the driver takes the nose and the rest fill alternating sides. Turning someone else out is refused in the window as well as on the server - the client check saves a round trip, the server one is what makes it a rule.
Says which vehicle a character is in, and clicking it gets them out. The icon is inline rather than an imported .svg so it takes the theme through currentColor, the same reason the person and eye icons are inline. Shown on anyone's token but inert unless it is yours, or you are the GM. Hidden would be wrong: seeing that someone is in a car is the reason it is on their token at all. The server refuses the same thing independently - this only saves a round trip. The sheet had no way to know where you are sitting since occupancy stopped being sheet data, so the server sends the vehicle alongside it.
added 19 commits
August 15, 2026 14:38
Version bump, changelog, and the project structure caught up with the eight new modules and four new test files. Filed as 1.9.0 rather than 1.8.2: this adds a subsystem and a database column, and 1.8.2 is claimed by the CP:R stat work on its own branch, which should go out first and separately.
CUSTOM was a hole rather than a feature. With no type a vehicle had no crew, no hardpoints and no Trauma Target - so it seated one person, refused to fire the mounts you could still fill in and see in the weapon picker, and took traumatic hits twice as often as any real vehicle. None of that said anything on screen. The section now holds nothing until you add something, and adding gives you a Motorcycle to change into whatever you meant. That also retires the blank entry of placeholder text that read like real data at a glance. Changing the type renames the vehicle unless you have named it yourself. The old rule kept any non-empty name, so a MOTORCYCLE changed to a Tank stayed called MOTORCYCLE - a name that is only a type label is not a name. Sections can seed an entry on ADD rather than revealing a blank row; weapons and spells still reveal, as they did.
WHO IS ABOARD in the VEHICLES section header, beside the collapse toggle rather than inside the section, so folding the section away does not take it with them. Sections declare the label; what the button does is the surface's business, and the standalone sheet tab passes no handler so it shows nothing - it has no windows to open. WHO_IS_ABOARD on the token menu too, but only when the table owns a vehicle at all. An empty roster means the button says nothing worth saying. Knowing whether there are any vehicles moves the roster into a hook the App holds, and the window takes it as props. One subscription, so a button that opens the window and the window itself cannot disagree about whether there is anything in it.
The window said NO VEHICLES on a sheet with a helicopter plainly on it. The hook was handed socketRef.current rather than the ref: a ref's current is not reactive, so mutating it does not re-render, and a hook that reads it once before the socket exists binds to nothing and never binds again. Takes the ref and waits for it, the way useInitiative already did - which is why that pattern is there. A test holds it: hand the hook an empty ref, fill it in afterwards, and the request still goes out.
Half the car sat below the fold. The window's content pane caps at 300px and scrolls, and the diagram was a square the full width of the window - so it overran every time, hiding the seats that matter most on a five-seater. The diagram is sized from the vehicle instead. Seats stack in two columns down the hull, so what sets the height is rows a side: two for a car, eight for an APC. A car now needs a fraction of the room sixteen people do, and the window widens to match rather than leaving the art floating in it. Square and centred, so the art, its leader lines and the controls at either edge scale as one piece and cannot drift apart. Bounded by the viewport at the top end, so a Dropcraft cannot run off the screen either.
A Dropcraft's right-hand seats were cut off. The window class sets max-width 400px, and a max beats a width outright - so the inline width the diagram asked for was ignored and the window never grew past 400 whatever it was given. Raised inline, bounded by the viewport instead. The dropdowns listed login names. Nobody at the table thinks of each other by account name, so the roster sends the character name alongside the username: the username stays the key everything is written against, the name is only the label. The occupant list on the badge and in the attack panel reads the same way, falling back to the login where a sheet has no name yet. identity is required lazily inside the one function that needs it - it requires this module for the vehicle mirror, and a cycle at load time left one of the two half-built, which failed four unrelated combat tests.
The window sat empty until a vehicle was added or removed. A client asks for the roster the moment its socket connects, which is before it has been identified - the handler needs a username and drops the request silently, so nothing ever came back. The next sheet save was what finally got one through. Pushed on identify instead, once the system is known to be CWN. The explicit request stays for everything after that.
Seat markers, leader lines and badges were a hardcoded amber. In monochrome that was the one coloured thing on an otherwise white interface; under Cyberpunk it sat on a yellow wireframe it barely separated from. Each theme sets a --vehicle accent instead: amber where the wireframe is green or red, cool blue where it is already yellow or orange, white in monochrome. The badge on the sheet's title bar also read as coloured text on a bar painted that same colour - invisible in monochrome. It sits on black there, the way the buttons beside it already do.
Amber on pink, on red, on cyan. The accent was a hue no theme owned, so it read as something borrowed from another interface wherever it landed. Two changes. The accent is now each theme's own secondary rather than one colour imported across all of them. And the seat furniture stops using it at all: the leader lines, the labels and an empty seat are part of the diagram, so they are drawn in the diagram's colour. The accent is left to mark the one thing worth calling out — that somebody is sitting there. Same on the token badge, which is framed like every other token label now, with the accent only on the text.
Three from a look at it running. The badge was drawn in the seat accent, which read as a stray teal among green buttons. The accent means 'somebody is sitting here' inside the diagram; a button sitting among buttons should look like them, so it takes the theme green. WHO_IS_ABOARD said what the window contains rather than what it is. The token menu says VEHICLES, matching the window and the sidebar entry; the sheet's section header says SEATING, since a button labelled VEHICLES inside a section called VEHICLES says nothing. The badge moves to the token window's title bar, where the sheet already puts it. Full width in a column of full-width buttons was more room than a short vehicle name needs.
Clicking it emptied the seat but the badge stayed. selectedLocation is a snapshot taken when the token was clicked, so it still said 'in a vehicle' after the server had recomputed otherwise - the seating window, reading the live roster, updated correctly at the same moment. It reads the live row now, the way the hit points menu already did for the same reason. It also read as a label rather than a control. Yours says LEAVE MOTORCYCLE, because a button should say what it does; someone else's stays a statement of fact, since it does nothing when clicked.
The anchor put seat one alone at the nose and started pairing from seat two, so a car read as driver at the front, shotgun behind them on the left, and B.LEFT across the aisle on the right. Shotgun is the seat beside the driver. Seats pair off from the front instead: the first two are the front bench, the next two the row behind, and an odd seat left at the end takes the centre line at the back — which is where a rear gunner sits, and where a lone rider belongs on a motorcycle. A truck reads as a two-seat bench, a tank as driver, gunner and a commander behind them.
The roster hook and six test files were missing, and the vehicle modules had been listed inside templates/ rather than beside it - so the tree said they lived somewhere they do not. The CWN template line still described an occupancy block that was retired and a field count from three revisions ago.
The window bullet described what it does without saying how anyone reaches it, and left out that players are listed by character name. Also adds the theming line: the vehicle accent follows each theme and marks occupancy rather than decorating the diagram.
The whole subsystem is new in 1.9.0, so a bug found and fixed inside it never reached anyone. Listing those under Fixed tells a reader upgrading from 1.8.1 that something was broken in the version they were running, which is not true — that is development history, not release notes. The three ways into the seating window are a list now rather than a sentence buried in a paragraph, since finding it is the first thing anyone needs.
…eating # Conflicts: # CHANGELOG.md # README.md # frontend/package.json # package.json
Combat already wrote a vehicle's HP when someone shot the car. What it never covered was the repair afterwards, or a crash, or a ram - all of which meant opening the owner's sheet and editing a number by hand. The bar grades green to amber to red at the same thresholds the character health windows use, and reads WRECKED at zero. Both directions clamp to the hull server-side. Destroyed is derived from HP rather than stored, so an unclamped write could put a vehicle in a state the rules have no name for - above its maximum, or below zero and wrecked twice. Everyone sees the bar; only the owner and the GM get the buttons. Taking someone else's car apart is what shooting it is for.
The box was stretching to the buttons' height while its own 2px padding kept the text pinned near the top, which read as a taller, misaligned control. Zero vertical padding lets the flex row set the height and the browser centre the value in it, so the three controls are one band. Digits only, stripped rather than rejected: a fumbled 1e2 or a pasted 12hp leaves you with the number instead of an empty box. type=number would not have done it - it still accepts e, +, - and a decimal point. Four digits is the cap, which is more hull than any vehicle in the book has.
The box was never too tall - it was the only control at full height. .win95-window .upload-btn carries margin-top: 15px, which in a flex row pushes both buttons down inside the stretched line while the box fills it, so the box read as oversized and the row as misaligned. Cleared on these two buttons rather than in the stylesheet, where every other window still wants that gap.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Test plan
Pre-merge checklist
Code quality:
Version & Release:
frontend/package.jsonversiondocker-compose.ymlAPP_VERSIONCHANGELOG.mdwith release notesBefore merging to main:
Related issues