Feature/cpr vehicles - #84
Merged
Merged
Conversation
added 13 commits
August 16, 2026 19:46
Cyberpunk RED vehicles. A point release rather than a minor: this adds a second system to the vehicle feature that landed in 1.9.0 rather than introducing one.
It imported getPreset twice - once for the wireframe, once for seat names - which meant Cyberpunk RED could only have a seating diagram by forking the component. Both now arrive through a look(type) prop. Seat names and the wireframe are the only two things that vary between rulesets, so that one seam is the whole of it: CWN answers with its book table via vehicleLook, and the next system answers with its own. A test drives the window from a look with no CWN behind it at all. If that test ever needs the CWN table installed to pass, the seam has leaked.
Cyberpunk RED has water and civil aircraft the CWN table has none of. Three of its fourteen vehicles land on shapes the app already ships; these are the rest. Keyed by what the shape is rather than by any book's name for it - sportbike, coupe, speedboat, aerodyne, airship - so they belong to the app rather than to a ruleset, and a third system can reuse a speedboat without inheriting someone else's vocabulary. Tests cover the contract the seating diagram depends on rather than whether a drawing looks like a helicopter: every shape draws something, stays inside the 0..100 box the seat anchors are percentages of, and is stroke-only on currentColor. A shape quietly drawn at another scale would put every seat in the wrong place and still look fine on its own.
Name, SDP and its maximum, SP, seats, hull shape, speed and cost as reference, and notes. Four vehicles, empty ones collapsed, SEATING in the header beside the collapse toggle. No preset picker, and there cannot be one: the corebook's vehicle table is book content and cyberpunk_red.ts holds labels and dice math only. Players type the four numbers that matter from their own copy, and HULL picks a drawing rather than a vehicle. The labels are Cyberpunk's - SDP, SP, SEATS - while the field ids stay the app's generic vocabulary, because the roster, the seating window and the hull bar are shared with CWN. SDP is a damage pool and SP is armour that subtracts; renaming the storage per system would fork three components to say the same thing in a different accent. Hull labels live in their own module and name shapes rather than anyone's model names, which is both the licensing line and simply truer - a speedboat is a speedboat in every ruleset.
A vehicle is a big enough thing to sit with the weapons rather than below the pocket contents, and the two GEAR tabs should read the same way. Pinned by comparing the two templates rather than by asserting a position, so if one ever moves the other has to follow or the test fails.
A registry of which systems have vehicles, written against a real second
system rather than guessed at, and the active system threaded through the
roster and seating calls instead of a hardcoded constant.
It stays small because the templates agree on field ids: a vehicle is
vehicle{i}_* in both, and hp/armor/crew mean the same things where Cyberpunk
shows SDP, SP and Seats. So the roster, the seating window and the hull bar
are one implementation, told which system is live.
Told, rather than assuming - a table that has played both should not find last
campaign's cars on this campaign's roster. Tests cover that in both
directions: neither system lists the other's vehicles or players, and neither
can seat into or damage the other's cars.
The combat path keeps the CWN default: it is CWN's own rules, and Cyberpunk
routes no damage through a vehicle at all.
Flat 6d6 to both parties, no SP either way. Armour applying is the natural assumption and the wrong one, so the exception has its own test - it is exactly the kind of thing a later refactor tidies away. The self-harm is the part a table forgets, so the result line always states both hulls: ramming a parked car to death costs you most of your own. Everyone aboard both vehicles is named for Whiplash, which is what the seat roster is for - the app knows who is aboard, so nobody has to go round the table asking. Whether the ram stops you is reported rather than enforced, since we do not model movement: flatten what you hit and you drive on, leave it standing and you stop. A pedestrian never stops a vehicle and, if they survive, may end up on it. The ramming vehicle is never taken from the payload - it is whichever one the caller holds the driver seat of. That is the whole permission model, so a client naming its own vehicle cannot ram with a car it is not in.
Sitting with the carried weapons is where a player looks for what they can do to a token, so that is where the driver's weapon belongs - GALENA . RAM . 6d6, offered only to whoever holds the driver seat, decided by the server from the seat rather than from anything the client sends. Targeting a token also answers the question the seating window could not: you hit what they are in. Aim at someone sitting in a car and the car takes it; aim at someone on foot and they do. Resolved server-side through the roster, because a client deciding that could aim at a passenger and leave the car untouched. No skill and no to-hit roll, so the row shows no skill and the aimed-shot control hides while it is selected - there is no check for either to modify. The seating window keeps its own RAM button for the case a token cannot cover: a parked car with nobody in it.
Import ------ The vehicle table cannot ship, so without this a player whose car already exists on a sheet somewhere else retypes it. Their own sheet is their own data, which is what makes importing the route the presets cannot be. Aliases bridge Cyberpunk's labels to the app's storage - SDP lands in the damage pool, SP in armour, Seats in crew - for one unnumbered vehicle or a numbered fleet. SDP also seeds the current pool, or every imported car would arrive already wrecked, a blank pool reading as zero on the roster. A bare SP is deliberately left as body armour: on a Cyberpunk sheet that is personal armour far more often than a vehicle's, and guessing wrong would overwrite the wrong field silently. Ram --- It carried no skill, so it fell through the melee check and offered FIRE. You drive into the thing; the button says SWING. Speed ----- The placeholder read '20 MOVE', which looked like a format the field demanded. It is free text that nothing reads.
The dialog took a filled PDF from the start, but there was nowhere to get one - you needed a sheet whose field names happened to match our aliases. This generates that sheet. The field names are the contract, so the layout lives beside the importer and a test walks every label on every form through mapFields. A label the importer does not recognise is a box a player fills in and loses, and nothing else would have caught it. Duplicate names are caught too: a second box with the same name silently overwrites the first on upload. Forms exist for the three systems that have importers, and generic is asserted not to have one - a download that cannot be uploaded is worse than no button. CWN gained vehicle aliases in the process; it had none. Less load-bearing there than in Cyberpunk, since picking a book type fills the stat block, but a car edited away from its preset should still survive the trip. Plain layout on purpose: stat names and boxes, not a reproduction of anyone's character sheet design.
One picker, the same shape as CWN's TYPE: it is what gets stored, it chooses the wireframe, and it fills the pool, armour, seats and name. The difference is where the numbers come from - seventeen archetypes we authored on a scale of our own, because the corebook's table is not ours to ship. They are approximate on purpose and every field stays editable, so a table wanting their own book's values types four numbers over the top and keeps the seats and the drawing. That is the workflow, not a shortcoming of it. The separate HULL field is gone. It was a second thing to keep in step with the archetype for no gain - CWN stores one type and derives the drawing from it, and now so does this. A new vehicle starts as the smallest archetype rather than blank, for the same reason CWN starts one as a Motorcycle: an unset type draws nothing and seats nobody. The name follows the archetype until someone names it themselves, and then it is theirs.
cprVehicles.test.ts was never added - the anchor line I matched on used the last-item tree character. The CP:R template and the import dialog both still described themselves as they were before this branch.
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