id: mounting title: A platoon aboard a transport is worth something the basis cannot say status: open dependsOn: [candidates, features] exitCriterion: > A carried unit's worth is scored, and a platoon offered a ride takes it when the ride is worth more than the ground. #
mounting #
The route exists and nothing chooses it. canCarry is on the wire, a candidate
that walks into a carrier's hex and ends with LOAD is generated, and in the
acceptance scenario it was offered once and declined:
| round | the platoon chose | mount offered |
|---|---|---|
| 1 | stand @ (6,1) | 0 |
| 2 | stand @ (5,0) | 1 |
| 3-7 | stand / flank | 0 |
It is scored as an ordinary stand in the carrier's hex - no line of sight, nothing adjacent - so it loses to every alternative, and by round 3 the platoon had walked away and it was never offered again.
What a carried unit is worth #
Two things are measured and no current column expresses either:
- it moves at the carrier's speed. A foot platoon runs 1 and a Badger runs 9, so the ride is worth nine times its own pace; a Maxim hover transport is 12. A battle armour squad runs 4, so the same Badger is worth 2.3 - the multiplier is a fact about the pair, not about mounting.
- it cannot be shot at separately:
Entity.isTargetableanswers false once the transport id is set.
Whether it can fire is unverified. isEligibleFor(FIRING) answers true
for a carried unit, which is not the same question as having a legal shot, and
the difference has not been settled. Do not price a silence that has not been
measured.
So the trade is at least mobility and protection, bought with something not yet established. The question this epic answers is what that is worth.
What mounting is #
A MoveStepType, not an action. LOAD, MOUNT and UNLOAD are steps, so
mounting belongs to the movement decision, and SdsClient.buildPath already
carries them - MoveStepType.valueOf has no whitelist. Measured: FORWARDS+LOAD
is legal at 2 MP, MOUNT at 0, LOAD alone from an adjacent hex refused.
Stacking and loading are separate permissions. A carrier's hex is barred by
stacking and open for loading, and MegaMek grants the two independently, so a
carrier that can take a unit makes its hex terminal: it may end there, and
nothing walks through. Without that the search never reaches the carrier and
there is no route to append LOAD to.
Three predicates, and they are not the same question:
Transporter.canLoad- capacity and type alone, blind to where the machines are. It accepts a foot platoon into aFixedWingSupportflying at elevation 999.Entity.canLoad- that plus a position check. The boolean toggles it:canLoad(e, false)matches the transporter,canLoad(e)applies it. This is the one to ask.MovePath.isMoveLegal- neither. It is true for an Atlas that has no handles for the squad, because it validates the path; the load is resolved by the server afterwards.
Open #
Getting off is the carrier's decision #
A carried unit gets no turn, so nothing asks it where it wants to be put
down: in a match, from the round after it mounts, only the carrier is ever
offered a movement decision. UNLOAD is measured to match - it is the
carrier's step, and every form of it on the passenger's own path is refused:
| path | form | legal | mp |
|---|---|---|---|
| carrier | UNLOAD bare |
no | - |
| carrier | UNLOAD naming the passenger |
yes | 0 |
| carrier | FORWARDS then UNLOAD naming the passenger |
yes | 1 |
| passenger | any form | no | - |
Three steps, three different rules, which is why each was measured rather
than reasoned from the last: LOAD is refused from an adjacent hex and
MOVE_ILLEGAL from inside the carrier's own; MOUNT is the passenger's, legal
from beside the carrier, and free; UNLOAD is the carrier's, needs the
passenger named, is free, and is legal both standing still and after driving.
So a drop costs the carrier nothing but the ground it covers, and the carrier can move and then put its passenger down.
What a drop does, measured #
The named hex is honoured. The carrier sent
{"name":"UNLOAD","target":1,"hex":{"x":3,"y":1}} from (3,2) and the passenger
was on (3,1) in the next frame, twice in one match. So a drop location is chosen
rather than decided by the rules, and SafeDropoff has something to choose
between.
A dropped unit gets a movement turn the following round. Dropped in round 2's movement phase, it was asked for a movement decision in round 3.
A unit dropped in the movement phase gets neither a firing turn nor a
movement turn that round. Measured on scenarios/tactics/mount-close.mms,
where the same platoon is asked for firing decisions with a menu of seven in
five other rounds - so the absence in the drop round is the drop and not a lack
of targets:
| round | passenger's movement turn | passenger's firing turn |
|---|---|---|
| 1-3 | yes | yes, 7 candidates |
| 4 | yes (it mounts) | - |
| 5 | none - the carrier unloads it | none |
| 6 | yes | yes, 7 candidates |
Two instruments failed before this one and both failed on the target rather than the mechanism: a Locust that stayed out of the platoon's range all match, and a gun emplacement that was destroyed in round one and stalled the match. An UrbanMech walks 2 and survives.
The jar agrees, and names the mechanism. MovePathHandler.processSteps
sends an UNLOAD step to TWGameManager.unloadUnit, which reaches
Entity.setTransportId(-1); that one setter sets both unloadedThisTurn and
done. isSelectableThisTurn is false while either holds, and
determineTurnOrder allocates turns only to entities that pass it, so no firing
turn is generated. isEligibleForMovement tests unloadedThisTurn directly.
resetEntityPhase clears done between phases but not unloadedThisTurn,
which is cleared only in Entity.newRound - which is why the flag survives
movement into firing. The one exception is a grounded Small Craft unloading
something that is not infantry: that branch clears both flags and grants a
movement turn at half MP. It grants movement, not shooting.
This is what MegaMek does. Whether it matches the rules is a question for jmm. In particular, a dropped unit shooting the round it is put down is a thing the code refuses in every branch, and it is worth confirming that is the rule rather than an implementation choice.
So a drop is positional: what the passenger gets is a place to be at the start
of its next turn, and SafeDropoff is about where it wants to stand, not
where it wants to shoot from this phase.
This is what MegaMek does. Whether it matches the rules is not established here.
Four duties and two profiles, one mechanism #
TaskOverride and TacticOverride now carry Spot, Board, Carry,
Deliver, and the Embark and SafeDropoff profiles. None of them needed
new machinery. Spot was expressible in existing columns, Embark found the
basis gap and recorded it, and the drop-off column is the one addition jmm has
sanctioned. Four consumers of one design is a better answer to "can tactics be
built from what exists" than any single tactic working.
The algorithm, as jmm gave it #
- Calculate eligible moves for the carrier with normal feature scoring
- From that set, calculate all eligible drop-off points for the passenger (typically a subset of moved locations + possibly some adjacency superset depending on type of dismount)
- For the set of all possible drop-off points, score the passenger's evaluation of where it would LIKE to go
- For all scored drop-off points, they give their score to the hex the carrier would have to walk to, in order to use that drop-off point
- This is now a per-hex feature for the carrier, which is 0 in hexes that aren't drop-off eligible, and some normalized-to-1 as "best dropoff I could ask for"
- The carrier has a TacticsOverride to have it even consider that feature which would normally be weighted 0
It is a back-projection. The passenger's valuation of a drop hex flows backwards through the reachability relation onto the carrier's move hex that would enable it. The carrier is not evaluating the passenger; the passenger's own scoring is being projected.
And it is a new column, which is worth stating rather than slipping past:
Spot needed none, Embark found a gap and recorded it, and this one jmm has
explicitly sanctioned.
The clamp is the hard part #
The biggest issue with this is that we have to ensure the feature stays 0 if none of the drop-off points are any good for the carried unit at this time. That's how you don't have 3-range infantry mount, and then immediately dismount next turn not being any closer to the enemy. If none of their hexes would lead to good shots they should score the hex 0 as a clamp.
A relative normalisation defeats this outright. "Normalised to 1 as the best drop-off I could ask for", read as best within this set, makes the least-bad drop of a uniformly bad set score 1 - and the platoon dismounts pointlessly, which is the anti-pattern the clamp exists to stop. The reference has to be absolute: what a good position is for that unit, not the best of what is on offer.
range_band_fit is the measure, and two halves of it had to be fixed
before it could read. Both were found by using it, and both were the same
defect in different places: a conventional platoon's numbers are not on its
weapons.
-
The damage.
projected_damagesummedweapon.damage, andWeaponType.getDamage()answers a sentinel for an infantry weapon --15.0for a point that delivers 3.00.ConvInfantry.getDamagePerTrooper()times the trooper count is the real figure, it is on the wire asinfantryDamage, andprojected_damagereads it. -
The reach. Fixing the damage moved the column not at all, because
projected_damagestill askedbrackets::projectedfor the shot - and that refuses a weapon whose bracket damage is nought before it reports the range. Every bin on a conventional infantry weapon is nought, so the rifle that does the shooting was thrown away at the damage gate. Measured off the wire, the 25-trooper Motorized Infantry platoon's whole armament:weapon usable short medium long damage bins Rifle (Bolt-Action) yes 0 0 3 0.00 / 0.00 / 0.00 Swarm Mek yes 0 0 0 0.00 / 0.00 / 0.00 Stop Swarm Attack yes 0 0 0 0.00 / 0.00 / 0.00 Leg Attack yes 0 0 0 0.00 / 0.00 / 0.00 The rifle's long bracket of 3 is the only range in the list, and it is the three-range platoon of jmm's requirement.
brackets::reachesanswers the half that is populated - which bracket, and what the range and the weapon's own modifier charge - and the platoon supplies the damage.
With both, best_damage for that platoon reads 0.24 where it read 0.00, and
range_band_fit reads 1.00 inside three hexes and 0.00 beyond.
The clamp, demonstrated #
scenarios/tactics/mount-approach.mms is the instrument, and it exists because
neither earlier scenario could ask the question. mount-probe.mms mounts
reliably and its enemy is never in reach, so every drop reads nought and a
working clamp is indistinguishable from a broken one; mount-close.mms starts
everybody on one edge, where the platoon has a shot from the ground and never
boards. This one puts them in sequence: the pair deploy apart, the platoon
boards while there is nothing to shoot at, and the carrier then drives the
length of the board towards an UrbanMech slow enough to still be there.
So the same question is asked in both directions inside one match. Seed 1:
| round | carrier | enemy | drop hexes offered | best worth | past the clamp | carrier chose |
|---|---|---|---|---|---|---|
| 1 | (3,2) | (12,14) | - | - | - | the platoon mounts |
| 2 | (3,2) | (12,11) | 6 | 0.00 | 0 | drives to (3,8) |
| 3 | (3,8) | (10,10) | 4 | 0.00 | 0 | drives to (8,9) |
| 4 | (8,9) | (9,9) | 4 | 1.00 | 4 | drop at (7,8) |
| 5 | (8,9) | (9,9) | - | - | - | the platoon is on the ground at (7,8) |
Both outcomes, and the reason differs the way the requirement says it should: in rounds 2 and 3 the enemy is seven and eight hexes off, outside the rifle's three, so a platoon put down there would be no closer to anything. In round 4 it is adjacent.
It clamps within a round as well as between them. On seed 2 and later in seed 1 the carrier offers six hexes and keeps two, so this is a per-hex bar rather than a per-round switch: the hexes beside the carrier are not all worth the same.
For this passenger the column is a step, not a slope, and that is a fact
about the design rather than about the feature. A bolt-action rifle has a single
bracket - 0 / 0 / 3 - so its to-hit is the same at one hex as at three, and
range_band_fit reads 1.00 everywhere inside the reach and 0.00 outside it. That
is all a clamp needs, and it is not enough to rank one drop hex above another
by range. A passenger with two brackets would separate them; this one cannot.
Suppressing them all is how the carrier drives on, and it needs no separate
branch. Duty::Deliver narrows the carrier's menu to its UNLOAD routes; with
none left the narrowing is empty, harmonise::constrain restores the whole
menu and counts the duty unsatisfiable. Rounds 2 and 3 above are that count.
See plan/ev.md for the category the damage figure belongs to - four weapon
classes now where getDamage() returns a code.
The pattern is still right. It is Bounded, and its denominator is best_damage(posture.unit) -
the unit's own best firepower - so it scores against that unit's own standard
rather than against the board, the force, or the other candidates. Its zero case
is the clamp, already written: "A machine with nothing left to fire has no band
to be in. Zero rather than a half: there is no range that would help it."
Two jobs, and the vocabulary already separates them #
"A feature like this is my best option" and "stay 0 when none of them are any good" pull against each other: the least-bad drop of a uniformly bad set is that unit's best option. They are two jobs and the basis already distinguishes them.
Localis min-max across this decision's own candidates - literally "my best option". It exists:damage_lead. AndWeightswill not accept it, enforced by acompile_faildoctest, because a local column "means nothing across two decisions". It ranks a menu and cannot be weighed against anything outside it.Boundedmeans a rule gives the domain, and the rule may be about the unit:range_band_fitdivides bybest_damage(posture.unit)and returns zero when the unit has nothing that would help.
So the carrier's column must be Bounded with a per-unit reference, and the
clamp is its zero. The carrier is not choosing among drops - it is weighing
dropping against driving on, which is a comparison across candidates of
different kinds, and a Local column cannot be weighted into that at all. A
self-relative ranking is welcome inside the drop set, where argmax is the only
thing asked of it.
That resolves both of jmm's constraints without a fourth normalisation: rank within the set if it helps, but what reaches the carrier is measured against the passenger's own standard, and is nought when nothing clears it.
This is a missing feature, not a missing capability. Bounded demonstrably
takes a per-unit reference today, so the drop-off column is a feature to write
rather than a normalisation to invent.
A carrier is a specific population: an Atlas and a Manticore have MekArms,
ClampMountMek, RoofRack and TankTrailerHitch and carry no infantry at all.
286 designs have an InfantryCompartment, and the 1047 with
BattleArmorHandles are all OmniMeks.