27959990eaee4da23eef80567bef617a93f36d2c
109
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
946323b696 |
Gyro: damage fan-out FUN_004b2980 -- the cockpit hit-BOUNCE is live (task #56)
Re-disassembled the unexported gap 0x4b2980-0x4b2d8b byte-exact (capstone over
section_dump.txt) and reconstructed Gyroscope::ApplyDamageResponse:
- zero-damage + Collision(type 0) no-op; hit direction = Damage::damageForce
or a RANDOM horizontal fallback when ~zero (per-component sign roll then
value roll -- binary-legal until senders fill damageForce)
- direction rotated by the yaw-only torso-twist frame (placeRot=(0,twist,0)
-> euler->matrix, the FUN_00408744 raw row-dot reproduced verbatim)
- per-type scaling amount / damageMultiplier[type] * damageResponse[type]
.{trans,pitchRoll,yaw,vibration}; Explosive alone multiplies burstCount;
each clamped at 1.3f
- four kicks: impulse(trans, dir), torque(pitchRoll, dir), impulse(vibration,
vibrationDirection@0x390) and verticalImpulse(yaw, dir)
Layout corrections (byte-verified): gyro+0x390 is ONE Vector3D vibration axis
init (0,1,0) -- was mis-split as scalars animationOffset/Scale/Phase; mech
+0x5c4 is a FLOAT rumble-period countdown (gyroRumbleTimer) -- was mis-typed
int clipLoadGuard.
Call sites wired (binary-gated):
- take-damage hub: mech.cpp TakeDamageMessageHandler, FIRST action (@0x4a0264
in hub FUN_004a0230) -- an invalid-zone hit still bounces
- crushable-icon crunch: mech4.cpp, torque 0.4 along the bounce delta-v + up
impulse 0.2 (@4aa81e/@4aa86c)
- firing recoil: projweap.cpp FireWeapon, damage>3 -> impulse (0,0.6,-1.5)
x damage/16 (@4bc136-4bc19c) via the new Mech::GetGyroSubsystem accessor
DEFERRED (recorded in KB with the byte recipe): the alternate-gait engage
jolt + 0.4s rumble (@4aa158-4aa365) -- its gates are [T3]-flagged and it
mutates the #49/#50-stabilized gait machine.
Verified live: [gyro-dmg] fires per hit with correct type scaling; eyePosition
shows the damped bounce (ramp to ~0.015u, oscillating Y, decay to zero); kill
chain un-regressed; no NaN.
Also recorded in KB: the OFFSET-COCKPIT verification -- Thor's authored
jointeye tranx=+1.13 matches its canopy X-center exactly AND our in-game eye
reproduces it to the hundredth (the placement model is correct per-mech); and
the playable roster is 8 models (strider/raptor/firstrtr/blkjack have assets
but no model resource -- BT_FORCE_MODEL with those names = NULL-resource AV,
cdb-verified, not a code bug).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
cb85517ede |
Gyro: byte-exact re-enable -- ctor map, integrators, jointeye writers (task #56)
The Gyroscope subsystem is LIVE again (was a stub since the NaN revert).
Reconstructed byte-exact from @004b3778 / @004b2ec0 / @004b30ec / @004b33e0 /
@004b34ec + the byte-verified dispatch in the unexported Mech performance
FUN_004a9b5c (calls @0x4aaf74/0x4aaf83):
- ctor field map corrected: springConstant@0x1E8 / dampingConstant@0x1F4 were
mislabelled (the old 'eyeOrientation = r->springConstant' poison line), the
0x254-0x2B3 block (externalPitchPtr, work matrix, placement) was missing so
everything after was mis-offset, and clamps/accumulators were 0xCD fill --
every field now initialised per the binary; 33 offsets static_assert-locked,
sizeof == 0x3D0 exact
- integrators: state-minus-target displacements, componentwise damping
OVERWRITE (FUN_004086d0 is a component multiply, not a cross), position step
without dt, clamp to [posSpring, negSpring]; IntegrateBody's X/Z-crossed
force + integration terms
- writers: WriteMechJoint drives ONE node ('jointeye', type 5): TRANSLATION =
eyePosition spring, ROTATION = bodyOrientation spring; WriteEyeJoint is a
multiplicative sway attenuator on 'jointlocal' (post-anim only)
- dispatch moved out of GyroscopeSimulation into the Mech performance tail
(GyroFrameJointWrite bridge, mech4.cpp) with the binary gates; gyro<->torso
pitch link wired (gyro+0x258 = &torso currentTwist) via complete-type-TU
bridges, retiring the SubProxy::linkTarget landmine
- Mech ctor: deathAnimationLatched/legResetLatch were never initialised --
0xCDCDCDCD gated the writers off silently
Runtime-verified: joints resolve (both BallTranslation), Performance installs,
WriteMechJoint finite from frame 0 (no NaN), Madcat cockpit un-regressed,
combat targeting healthy. Empirical: spring targets are SYMMETRIC -> eye
equilibrium (0,0,0), clamps +/-0.1-0.15u -- the gyro is the hit-BOUNCE
mechanism, not a steady eye offset (hypothesis disproven, recorded in KB).
Pending (task #56 tail): the damage->gyro fan-out FUN_004b2980 (unexported
gap) so hits actually kick the springs; the mech+0x3F0 overspeed sway model;
the torso-pitch EyepointRotation writer (FUN_004b66b4) + glance-look states.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
abaa145b6f |
Cockpit: authentic eyepoint + canopy lattice render (task #55)
The first-person cockpit now shows by default and matches gameplay footage (verified on the Madcat). Two decomp-verified reconstructions: EYEPOINT (FUN_004579a8 + caller part_014.c:5525, FUN_004c22c4): - eye offset = siteeyepoint GetBaseOffset (not GetSegmentToEntity, not the upright hack), parented on the PARENT segment DCS (btl4vid.cpp) - live view = affine INVERSE of the eye world matrix (L4VIDRND Execute) -- replaces the hand-rolled LookAtRH whose +Z-forward/+Y-up row guess aimed some mechs into the canopy; combine order fixed to baseOffset * R - chase camera basis + aim boresight re-expressed for the inverse convention (row2 = back); boresight now derives from the view matrix (the stale rows aimed the pick ray at the sky = no target, no discharge) CANOPY (*_COP.BGF, dpl_Punchize geogroups): - every shell is an open strut lattice (38-59% boundary edges, all 12 mechs); rendered single-sided with per-face INWARD winding (l/r torso patches are mirrored, so no global winding works) the struts are the dark frame and the openings show the world - frame colour = the 'softer' ramp near its dark end (texture-less unlit ramp; BLXSKIN.BMF has no texture -- the old "punch texels" theory was wrong) - scoped by filename (meshIsCop); wrong drop-punch + view-cut paths removed - BT_FORCE_MODEL=<name> forces the player mech for per-mech bring-up KB: new context/cockpit-view.md topic; punch split corrected in bgf-format; gotchas 14 (LookAt axis guess) + 15 (per-patch edge namespaces); env gates + eye/punch addresses in decomp-reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0bfb3d4ab3 |
Warp: restore the translocation-vortex look + fix texture-scroll precision collapse (task #52)
The death/respawn "blue whirlwind" (tsphere) now matches the original cabinet
photo (capture.png): a smooth spinning lavender vortex with a bright core.
Root cause of the long-standing "radial spokes" artifact was NOT the warp code
but a general engine bug: L4D3D::SetTextureScrolling computed its texture-matrix
offset as scrollDelta * absolute_time, which grows unbounded and collapses UV
float precision -> a smooth scrolled cloud shatters into grainy radial steps.
Wrapped with fmodf(..., 1.0f) (identical under REPEAT tiling, full precision).
This also cleans the scrolling bexp beam grit and any other SCROLL material.
Visual reconstruction (verified against the real 45-vtx TSPHERE.BGF bicone,
offline-rasterized then ported):
- view ON-AXIS (eye centred on the throat) + spin in place -> concentric rings
(decomp FUN_00453dc4 does spin-about-local-Z + submit; the port had stubbed it)
- bintA cloud through a WIDE lavender ramp at full contrast (drawn as SKY);
no geometry "bands", no log-polar twist, no off-axis tornado (all discarded)
- tessellate the 12-facet bicone smooth; isotropic + trilinear; ramp baked into
the texture and drawn SELECTARG1(TEXTURE) to avoid double-tinting
Env knobs (BT_WARP_*) default to the verified values; BT_WARP_SELFTEST/SELFSHOT
are an off-by-default visual-verification harness (backbuffer frame dump).
Docs: new context/translocation-warp.md (geometry/material/visual/lifecycle/env);
reconstruction-gotchas.md gains the accumulated-time precision-collapse bug class;
rendering.md / multiplayer.md / decomp-reference.md / CLAUDE.md cross-linked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
a35f321ba1 |
Warp: exact POVTranslocateRenderable replica (collapse/throb/reveal + world mask)
Stop approximating -- replicate POVTranslocateRenderable::Execute() behaviorally,
byte-for-byte on the constants and transitions (engine T0, L4VIDRND.cpp:1763-2076),
per the user's "replicate the effect exactly". The five prior piecemeal attempts
each fixed one facet and broke another because they left out the WHOLE mechanism.
Full state machine (btl4vid.cpp), driven by two decoupled events (NOT the
SimulationState trigger -- that dial also drives camera/POV+targeting, the
|
||
|
|
062c66999e |
Warp polish: animate scroll, recenter, no depth-clip, expand-only respawn
Four fixes for the user's "static / above the viewport / cavern clips it / doesn't
shoot off" report, each root-caused:
1. STATIC swirl -> now animates. The material's SCROLL (0.0 0.0 0.1 0.5) only
reaches the draw via a per-texture .met file (L4D3D.cpp:640) that tsphere lacks,
and the ramp-bake path leaves doScroll=false -- so the swirl was frozen. Set the
authored scroll rates (u -0.1/s, v +0.5/s) + REPEAT wrap on the sphere's draw-ops
at load, so SetTextureScrolling churns the swirl (the "spins around").
2. "Above the viewport" -> recenter. tsphere.bgf is modelled with its centre at
y=+8.25 (logged: center=(0,8.25,0.47) r=21.5); scaled x100-150 that threw the
sphere ~1000 units overhead. Translate the mesh's mCullCenter to the origin
before scaling so it sits ON the eye/anchor. (Added public-ish mCullCenter use;
l4d3d.h already exposes it.)
3. Cavern clip -> Z-test off. The arena is an enclosed cavern; a depth-tested 150x
sphere gets occluded by the ceiling/walls. Draw the POV overlay with ZENABLE off
(ZWRITE already off, so no depth pollution). BT_WARP_ZTEST=1 restores it.
4. "Fades in then disappears" -> expand-only. A respawn now starts in the EXPAND
phase (1->150 over 1s) so the swirl immediately bursts and blasts outward ("shoots
off into the distance"). The collapse (100->1) is the DEATH warp, not the
respawn. BT_WARP_COLLAPSE=1 plays the full collapse->expand.
Build + render + no-crash verified 2-node; expand warp fires POV/world. Still
deferred: the SetIsDead world-mask (black-out that makes the expand "reveal" the
world) -- carries the camera-coupling risk, add scoped if wanted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
a9d1534292 |
Warp: faithful POVTranslocateRenderable -- eye-centered, ramp color, no spin
Deep-dived the authentic engine effect (L4VIDRND.cpp POVTranslocateRenderable, T0)
and reworked our one-shot to match it, fixing the user's "blob from my own POV /
not aligned to my orientation" + "gray, not blue" reports:
1. PLACEMENT was the misalignment. The engine parents the sphere to the VTV/eye
("rotated and scaled around the VTV", L4VIDRND.cpp:1812) -- it is centered on and
oriented to the viewpoint. We world-fixed it at the mech's feet, so from your own
camera it sat off-axis and, once expanded, engulfed the eye as a shapeless blob.
Now: the own-respawn warp (BTStartWarpEffectPOV, from btplayer) uses
world = Scale(s) * inverse(view) -> centered on + carried by your eye (the tunnel
wraps you). Observing a PEER (BTStartWarpEffect from mechdmg) stays world-anchored
at the peer's point.
2. GEOMETRY SPIN was inauthentic. myRotateY/myRotateYSpeed are set in the engine
ctor but NEVER applied -- the swirl's rotation is entirely the material's texture
SCROLL (bintA, SCROLL 0.0 0.0 0.1 0.5). Dropped our world-Y geometry spin.
3. COLOR (gray) fixed at the source. The blue/white is the "sky" RAMP remapping the
grayscale bintA cloud per-texel (argb=lerp(0,0,0.6 -> .99,.99,.99, luminance),
L4D3D.cpp:480). The port bakes exactly this but GATED it off for normal-bearing
meshes; tsphere has normals so it arrived raw gray, and a MODULATE tint can't add
the blue floor (it only multiplies) -> the wash-out. Un-gate the ramp for
tsphere_mtl (bgfload.cpp) so the bound texture is already the blue-white swirl;
draw it MODULATE by white (no tint), alpha from TFACTOR.
4. SCALE to authentic 100 (collapse) / 150 (expand end) -- correct now that it's
eye-centered (you're meant to be inside it as it collapses through you and blasts
back open, the "blast off into the distance").
Still deferred (higher risk / SimulationState-coupling trap): the WaitForReincarnate
Lissajous wobble + the SetIsDead world-mask (black-out during the dead hold). Noted
for follow-up; the placement/color/motion are the parts the user flagged.
Smoke-verified 2-node: own respawn logs POV(eye), observer logs world-anchored, no
crash. BT_WARP_ADDITIVE / BT_WARP_COLOR / BT_WARP_SCALE remain as live tuning knobs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
160b78e38d |
Respawn: swirl warp, cockpit-not-black on respawn, observer sees peer un-wreck
Three respawn-visual bugs the user saw, each grounded in the engine/decomp:
1. Warp = flat blue BLOB, not the demo's swirly blue/light-blue/white shimmer.
Ground truth (content/VIDEO/MAT/DAY/BTFX.VMF): tsphere_mtl = a SCROLLING bintA
texture (tsphere_scr_tex, SPECIAL SCROLL) + EMISSIVE {0.7,0.5,1} + RAMP "sky"
(dark-blue->white). The swirl IS the scrolling texture; the colour is the
emissive/ramp. Our draw did COLOROP=SELECTARG1/ARG1=TFACTOR, which REPLACES
every texel with one flat colour -> the blob. Fix: MODULATE the (bound,
scrolling) texture by a TFACTOR set to the authentic EMISSIVE hue (0xB380FF)
so the swirl survives and reads blue-white. DrawMesh's cached SetTexture
(L4D3D.cpp:1215) leaves our MODULATE ops standing since textured meshes drew
first. Additive glow, all state saved/restored.
2. First-person view BLACK on the dying/respawning mech until V. SetViewInside's
body-hide + '_cop' canopy suppression + viewSkeleton update were gated !wrecked,
and RebuildMechRenderables (respawn un-wreck) restored the full OUTSIDE torso
with no '_cop' rule -> the cockpit eyepoint ended up wrapped in opaque geometry.
Fix: factor the per-segment mesh selection into ApplyViewSkeleton(viewpoint,
inside) shared by SetViewInside AND RebuildMechRenderables, so respawn re-asserts
the inside skeleton + '_cop' hide; record viewSkeleton even while wrecked. Only
the mech the local camera views FROM gets the inside treatment (a replicant is
always outside).
3. OBSERVER never saw the peer respawn -- the peer's wreck sat forever. The wreck
appears incidentally via rising damage-zone replication -> MechDeathHandler::Tick
-> BTRemakeMechModel (one-way). The un-wreck+warp ran master-only in Mech::Reset.
Fix (reuses the existing damage channel, no stream-framing change): Mech::Reset
also ForceUpdate(DamageZoneUpdateModelFlag) so healed zones cross; Tick handles
the FALLING edge -- on a ReplicantInstance whose zone heals from destroyed, call
BTRebuildMechModel + BTStartWarpEffect once (wasWrecked latch). This is the port
analog of the binary's type-0 graphic-state -> ResetPose un-wreck hook
(Mech::ReadUpdateRecord case 0); the full Mech::WriteUpdateRecord death record
(type 6) is deferred (would touch the netcode framing).
Smoke-verified headless (2-node): victim respawns intact + warp; observer logs
"replicant un-wrecked + warp" at the peer's spawn point; no crash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
d07dd0bf4e |
Respawn visuals: warp was in the wrong render pass; render never un-wrecked
Two render bugs behind "warp never appears" + "respawned mech stays a sunk wreck": 1. Warp invisible: the tsphere was drawn in PASS_ALPHABLEND, but DrawMesh skips any op whose alphaTest != (pass==PASS_ALPHABLEND) (L4D3D.cpp:1045) and tsphere's op is opaque -> every op skipped, nothing drawn. Flag the sphere's ops for the alpha pass, and draw it as an explicit translucent-blue ADDITIVE glow (state saved+restored) so it reads as a warp shimmer, not an opaque ball or (as shipped) nothing. Tint via BT_WARP_COLOR. 2. Render stays wrecked on respawn: SwapToWreck hides the body + hangs a sinking dbr hulk with render_tree.wrecked a one-way latch; Mech::Reset healed the sim but nothing reversed the render, so the reborn mech kept rendering as the sunk hulk. Add RebuildMechRenderables (the heal direction of RemakeEntity): drop the hulk/debris, clear wrecked, restore every body segment to its now-intact mesh. Mech::Reset calls it via BTRebuildMechModel. Smoke-verified: tsphere loads with its op alpha-flagged; on each respawn "[BTrender] respawn: rebuilt intact model (12 segs restored, hulk dropped)"; no crash. Both are local render (the respawning node's own screen); remote-observer replication of the warp+un-wreck rides the deferred WriteUpdateRecord death path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
247e51e1e1 |
Reset-based respawn: reuse+heal the mech, not create-a-new-one (task #52)
The respawn glitches (2 mechs, on-fire respawn, camera-inside, can't-control, wreck-never-disappears) all traced to one architectural divergence: our respawn SEVERED playerVehicle on death and CREATED a new mech, leaving the old as a permanent wreck and building a duplicate viewpoint whose old render tree was never torn down. The authentic engine (FUN_0049fb74 + RPPlayer) REUSES the same mech entity: on respawn Mech::Reset heals it and moves it in place. Implemented faithfully, adapted to our layout (the 1995 raw offsets map to different 2007 engine fields, so reset the equivalent named members, not the offsets): - Mech::Reset (real, was a reposition-only stub): reposition + kill dead-reckon (projectedOrigin/projectedVelocity/updateVelocity + our relocated gait accumulators) so the replicant stops lerping to the death spot; clear the death latch (movementMode=1, graphicAlarm=0); Heal every damage zone (new Mech__DamageZone::Heal: full structure, intact skin); DeathReset (vtable+0x28) every subsystem; ForceUpdate to broadcast. - btplayer.cpp: VehicleDead no longer severs playerVehicle; the respawn re-post gates on the mech still being dead; DropZoneReply resets the EXISTING mech in place (heal+move) instead of creating a new one, then fires the warp. Warp moved to the shared placement (initial drop-in + respawn). Verified 2-node: mech entity ID stays 3:22 across 3 deaths (reused, not a new 3:32); each Reset logs alive=1, 20 zones healed, 33 subsystems reset; A sees ONE mech (no wreck+new pair). Warp fires each respawn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f0535356bc |
Warp: decouple from player SimulationState (fix camera/targeting regressions)
Pulsing the player's SimulationState (DropZoneAcquired->Translocated) to trigger the translocation sphere regressed everything else that dial drives in our reconstruction -- the camera flipped to inside-view, targeting/firing gated off, shadow pass glitched. That dial is load-bearing; co-opting it was wrong. Replace with a self-contained render one-shot: btplayer.cpp respawn calls BTStartWarpEffect(dropZoneOrigin); the effect plays its own collapse->expand and touches nothing but the render. No SetSimulationState, no DropZoneLocation write, no per-entity renderable walk (the tree's BTTranslocationRenderable objects are now inert). Scale capped 30/40 (authentic 100/150) so the sphere -- centred on your own reinsertion -- doesn't envelop the camera; tunable via BT_WARP_SCALE. Smoke-verified 2-node: warp fires on each respawn (collapse 31->5, expand ->35), repeats, respawn cycle intact, no crash. Camera/targeting restored (state pulse gone). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
63c1c5a460 |
Implement the translocation sphere -- the "blue warp" respawn effect (task #52)
BTTranslocationRenderable was a no-op stub; now reconstructed over the engine's POVTranslocateRenderable: loads tsphere.bgf and runs a collapse-on-arrival / expand-on-reveal sphere (scale 100->1 over 1.3s, then 1->150 over 1.0s, rotating) keyed on the player's SimulationState dial. Drawn direct from the render loop by BTDrawTranslocationSpheres (beside BTDrawBeams, PASS_ALPHABLEND) -- the same accommodation the beams/reticle use. The asset loads by FILENAME (tsphere.bgf), not through the RES table -- which is why every resource-name search missed the effect for three rounds. Trigger wiring: btl4vid.cpp MakeEntityRenderables builds the sphere for the LOCAL player (the authentic wiring builds it only for replicants + a POV fade for self, but peer player-attribute replication isn't wired -- on a replicant SimulationState/ DropZoneLocation read uninitialised). btplayer.cpp pulses SimulationState DropZoneAcquired->VehicleTranslocated at respawn (a 1.4s flip timer stands in for the engine's +1s drop-zone re-post) and writes the respawn origin into the DropZoneLocation attribute. Verified 2-node (BT_TLOC_LOG): on respawn the sphere collapses (~100->5) then expands (->150) at the valid drop-zone origin, deduped to <=2 active, respawn cycle un-regressed, no render errors. Visual appearance (colour/size) still needs a live look; the authentic see-others'-spheres path needs player replication. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
83b3f31957 |
Whirlwind respawn: authentic death->drop-zone->recreate cycle (task #52)
Death transition (mech4) now dispatches VehicleDead(-1) to the owning player; BTPlayer::VehicleDeadMessageHandler restructured to the authentic @004c05c4 three-way branch: -1 = death bookkeeping + sever playerVehicle (wreck stays) + 5s re-post; >=0 = engine drop-zone hunt -> DropZoneReply -> CreatePlayerVehicle (new mech); -2 = the acquire probe. Guarded on a DropZones group so a zone-less mission stays dead instead of aborting. Three latent bugs the respawn path exposed, all fixed: - IsMechDestroyed latched on graphicAlarm>=9 alone; a later leg hit on the wreck rewrites the alarm to 4/3, un-latching -> the death transition re-ran (double kill/score, abort). Now latches on movementMode 2||9. - Score handlers dereferenced the severed playerVehicle during the dead window; guarded. - The console score flush routed a NetworkClient::Message through the player's Entity::Dispatch, which stamps entityID past the smaller struct -> /RTC1 stack overflow. Sent via application->SendMessage instead. - Renderer LoadMission re-entry (per viewpoint-make) re-read the env INI; its light block Fail'd on stale sceneLightCount. Reset it in DPLReadEnvironment so the respawn's second read is clean. Verified 2-node self-drive: B killed repeatedly by A respawns each time (wreck stays), ticks + reloads + takes fresh damage on the new mech, no abort/hang across multiple death->respawn cycles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
e430e474cb |
Death gate: a destroyed mech takes no pilot input (ghost-drive fix)
User report from 2-window play: after dying, the pilot could still steer the wreck around invisibly. Zero throttle/turn/fire and the forced harnesses at the gBTDrive source when IsMechDestroyed() -- deliberately INPUT-only, so the gait/animation machinery keeps running and the death collapse plays out. Also recorded in the KB: the killer's screen shows the wreck SWAP but not the death SINK (needs the Mech-level death/knockdown update records, disasm preserved from task #51), and the respawn-cycle scouting (type-12 intro scripts are binary ref-records; adrop; DropZone + Mech::Reset machinery) for the recovery-whirlwind reconstruction (task #52). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
29035028fd |
Cross-pod beams: replicate emitter discharge via subsystem update records
User report: lasers only visible on the window firing them. The peer's replicant emitters never learned the master fired. THE AUTHENTIC PIPE (decomp-verified): - FUN_0041c350 (the "beam keepalive" ServiceDischarge/ContinueDischarge call) does TWO things: queue the LOCAL deferred beam-effect callback (@0x4bac0c) on the app+0x34 manager -- our per-weapon render walk already plays that role -- and set the subsystem DIRTY bit, which maps to the 2007 engine's updateModel / ForceUpdate(). - Replication rides SUBSYSTEM UPDATE RECORDS inside the mech's update message: the roster walk already hands the entity's stream to every subsystem's PerformAndWatch; Simulation::WriteSimulationUpdate serializes each requested updateModel bit; Entity::UpdateMessageHandler routes received records by subsystemID to the subsystem's ReadUpdateRecord. All engine machinery -- the missing pieces were the Emitter's serialize/apply pair + the triggers. CORRECTIONS to the dormant task-33-era transcriptions (never exercised -- nothing ever set updateModel -- so the latent misreads never surfaced): - The weapon-family VTABLE SLOT MAP was swapped: slot 6 = ReadUpdateRecord, 7 = WriteUpdateRecord, 9 = TakeDamage (evidence: Mech hierarchy symmetry + body semantics; @004ba568 resolves an EntityID at rec+0x30 through the entity index -- record semantics, not Damage). Renamed across MechWeapon / Emitter / ProjectileWeapon; the real Emitter::TakeDamage @004bafc8 is undecoded (inherits MechWeapon for now). - Emitter/MechWeapon Write: `*record = 0x38/0x18` is the record LENGTH, not recordID; rec+0x30 is the TARGET's EntityID (GetEntityID()), not a colour -- the old `CopyColor(targetEntity+0x184)` was also a databinding trap. - OVERRIDE-SIGNATURE TRAP: the decls used each class's own shadowing UpdateRecord typedef as the param type, silently NOT overriding the engine virtual (the base ran instead; nothing would ever have serialized). Base-typed params (Simulation__UpdateRecord*), casts inside. - Emitter::ReadUpdateRecord reconstructed (@004ba568): target EntityID resolve (drop unknown non-null targets), MechWeapon alarm apply chain, beam fields. - ServiceDischarge/ContinueDischarge: ForceUpdate() per keepalive tick + one final record at beam end (turns the peer's beam off). - Mech::DrawWeaponBeams extracted from the player-only drive block so the walk runs for REPLICANTS (+ per-mech gun-port cache -- the process-wide statics would have served the player's segment pointers as the replicant's muzzles). VERIFIED 2-node: A fires 57 volleys -> 225 emitter records -> B applies all 225 -> B draws 414 beams (PPC blue / laser red, from A's replicant's own gun ports). Solo un-regressed (150 beams, kill chain, no crash). Also preserved: the full Mech::WriteUpdateRecord @0x4a0c2c recovery (reference/decomp/mech_writeupdate_004a0c2c.disasm.txt) with all 9 record types decoded (pose/alarm/leg-state+heat with the body-channel write-through re-sync, knockdown, death, impact, movementMode) -- transcription deferred; it replicates remote knockdown/death/heat and was not needed for beams. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a110041a70 |
Fix "can't turn in MP": pilotArray[1] overrun stomped controlMode
User report from the first live 2-window session: throttle worked, A/D turn
did nothing. [mppr] showed the input REACHING the mapper (stickX=-1) but
turnDemand=0 and controlMode = pointer garbage (73583232 / 217478632 --
different per instance), from frame 1, MP only (solo mode=0).
Hunted with a cdb hardware write-breakpoint (ba w4 armed at ctor time on
&controlMode, compiled offset +0x128 from the [mode-ctor] probe). Caught the
writers red-handed: MechControlsMapper::BuildPilotArray / FillPilotArray.
ROOT CAUSE -- a shrunk-span array: the binary reserves a FIXED 10-slot pilot
block @0x15C..0x183 ((pilotArrayBuilt@0x184 - 0x15C)/4 = 10); the recon
declared `Pilot *pilotArray[1]` ("variable length"), so every write past
slot 0 stomps the members declared after it. In MP, FillPilotArray wrote the
PEER's Player pointer into slot 1 = over controlMode -> the turn shaping
dispatched on pointer garbage -> turnDemand 0. MASKED in solo: the zero-loop
overrun wrote 0 there, and 0 == BasicMode. This was ALSO the real cause of
task #48's "turnDemand zeroes out in -net" observation (worked around then
with the direct goto-turn; attribution corrected in the KB).
Fix: pilotArray sized to the binary's own 10-slot span; ctor zeroes all
slots; BuildPilotArray clamps pilotCount to capacity (local + 9 remotes; an
8-pod game fits). Diagnostics kept (BT_MODE_LOG ctor probe; [mppr] line now
prints mapper/&mode).
VERIFIED: MP session holds mode=0 throughout, turnDemand flows nonzero, the
mech turns (heading -1.33) and closes on the peer; solo kill chain intact.
KB: new "shrunk-span array" gotcha (reconstruction-gotchas 5) with the cdb
ba-w4 recipe; multiplayer.md task-#48 note corrected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
d09cda6c36 |
MP: replicant gait animation -- peer mechs walk instead of sliding (task #50)
A peer's replicant mech slid around as a frozen statue: DeadReckon moved localOrigin but nothing animated the legs. Reconstruct the replicated-motion display loop: after DeadReckon, derive a SIGNED speed demand from the replicated velocity (the master publishes worldLinearVelocity in every update record, Mover::WriteUpdateRecord MOVER.cpp:759; the replicant stores it in updateVelocity.linearMotion :712; sign = forward dot against the -Z facing), write it to controlsMapper->speedDemand (the LEG channel's live demand source), and advance the LEG state machine for its JOINT writes only -- travel stays with DeadReckon, so position always follows the master and the clip cadence matches the replicated speed. Two replicant-only initialization holes found live (the master's equivalents are primed per-frame in the PLAYER-only drive block): - globalTimeScale/idleStrideScale read 0 -> clip advance time dt*0, the leg machine engaged (state 11) but the clip froze (legCycle stuck 0); - reverseSpeedMax2 (the run-cycle rise-clamp @0x7a0; LoadLocomotionClips does NOT set it) read debug-heap garbage -> legCycleSpeed clamped to -4.3e8 on entering run state 13. Primed both; legCycleSpeed sane-banded once. VERIFIED 2-node (BT_GOTO driver + BT_REPL_LOG observer): the replicant runs the full gait lifecycle -- stand -> walk(5,7) -> run(10,12), legCycle 7.5->34 tracking the master's speed, wind-down(8), Standing(0) when the master stops. Solo un-regressed (goto->aim->kill chain intact, no crash). projectedVelocity source-swap (the binary publishes the BODY channel's smoothed projection instead of the actual travel velocity) evaluated and DEFERRED: it risks a verified working replication feed for a smoothing nuance; recorded in multiplayer.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b6fe686b22 |
Shadow: authentic terrain tilt + decal draw order (feet layer correctly)
User report: the ground shadow painted OVER the mech's feet. Root cause: the
flat quad + big depth-bias (-0.004 ~= 2-4 world units) workaround for slope
burial -- a bias big enough to beat the TERRAIN also beat the FEET in the
depth test, and the shadow drew in the late alpha-blend pass (after the mech),
so z-arbitration was the only layering control.
Fix, four co-dependent parts (user-verified live: feet on top, opaque,
survives inclines/declines):
- TILT (mech.cpp UpdateShadowJoint): apply the SKL's own contract for
jointshadow ("apply terrain angle to pitch and roll") -- quad up aligned to
the surface normal via an orthonormal basis fed to the ENGINE's own
LinearMatrix->EulerAngles conversion. Hand-derived Euler signs are exactly
how the prior tilt attempts "dug into the hillside"; the engine conversion
is convention-proof. ~35 deg cliff-guard cap.
- SAMPLER (mech4.cpp): surface gradient from the collision probes PLUS
BTVisualGroundLift per probe -- the quad hugs the VISIBLE terrain, whose
lift varies across a slope; world->local rotation by the TRUE yaw from
localToWorld, not the drift-prone gDriveHeading mirror (the task-#48 bug
class). BT_SHADOW_LOG traces the normals.
- DRAW ORDER (new PASS_SHADOW, l4d3d.h/L4VIDRND/L4VIDEO): shadows draw
between the STATIC terrain and the DYNAMIC opaque bodies -- the classic
decal order. The mech, drawn after, z-passes over the shadow: the bias
can never paint the shadow over the feet, structurally.
- BIAS pairing (L4D3D.cpp): tilt on (default) -> decal epsilon -0.0008;
BT_SHADOW_TILT=0 flat fallback -> the old -0.004. BT_SHADOW_BIAS overrides.
KB (locomotion.md): the four-part arrangement recorded as co-dependent, with
the gait-desync-symptom diagnostic hint retained.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
d4e139a0d9 |
Gait v5: display+travel from the LEG channel (kills the stutter class)
User-reported recurrence of "legs stutter and lose sync". Live [sync] showed
advSum/legSum drifting 5->92 during walk-up churn, then locking. Reproduced
headlessly (BT_FORCE_OSC throttle feathering + BT_FORCE_TURN + the new
per-frame BT_SYNC_LOG probe) and root-caused in two parts:
- SEED: the bring-up turn-in-place trigger arms only the LEG channel, so the
walk re-entry lands one frame apart -> a persistent clip phase offset.
- AMPLIFIER: with offset clips the two end-of-clip callbacks fire on different
frames; a demand change landing between them picks DIFFERENT next states
(one channel winds down, the other keeps walking) -> opposite-phase churn
(observed bs=6 ls=7 -- body on walk-R, leg on walk-L).
Under v4 (display+travel = BODY) the out-of-phase LEG pose showed through on
every frame the body didn't write joints (Standing/wind-down) = the visible
stutter/pop.
DISASM GROUND TRUTH (neither Advance fn has a static decomp caller; found by
byte-scanning the CODE section for e8 calls):
- master perf (0x4a9b5c gap) -> AdvanceLegAnimation @0x4aa399 (air @0x4aa388)
-> -dist/dt into localVelocity (+0x1cc): the LEG drives LOCAL travel and
(writing last) the displayed pose;
- IntegrateMotion (0x4ab1c8, body advance @0x4ab312, caller FUN_004ab430 =
the projected-origin updater) -> -dist/dt into projectedVelocity (+0x2a0):
the BODY is the dead-reckoning PROJECTOR -- locally invisible.
v4's reading of FUN_004ab430 as the travel source was wrong.
FIX: advance body FIRST (projection; its writes get overwritten), leg LAST
(displayed pose); travel = legAdv under the two-channel split. Display ==
travel through the LEG by construction -- body-channel drift can no longer
become visible, structurally (not by input-symmetry luck).
Verified: trn+feathering repro clean; straight drive locked (adv==proj);
solo goto=enemy kill chain intact; MP 2-node drive-to-range cross-pod kill
intact (241 hits, DESTROYED); no crashes. KB corrected (locomotion.md roles
+ P3_LOCOMOTION.md v5 entry). New gated instruments: BT_SYNC_LOG,
BT_FORCE_OSC.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
9353d59eb9 |
Fix drive-to-range stall: BT_GOTO self-drives + steers correctly cross-net
The BT_GOTO beeline never actually closed on a target -- three separate faults: 1. It was gated behind `if(gBTDrive.forced)`, and `forced` was set ONLY by BT_AUTODRIVE -- a chicken-and-egg where the block that would drive was gated on the drive already being on. So BT_GOTO alone just turned in place (the "stall"). Fix: BT_GOTO enables forced mode itself AND supplies its own forward throttle (a beeline must drive, not only steer), cutting the throttle inside a stop radius (weapon range for "enemy", so it holds + shoots). 2. Steering used the gDriveHeading SCALAR MIRROR (seeded to 0), but MP pilots spawn facing a non-zero heading -> it steered the WRONG way. Fix: heading error from the mech's ACTUAL forward (localToWorld's -Z axis), signed angle to the target. 3. The goto turn was routed through controlsMapper->turnDemand, which zeroes out in -net mode (the mapper key-bridge only shapes the local viewpoint mech there) -- the heading froze and A drove straight past B. Fix: apply the goto turn to the orientation integration DIRECTLY (if gBTGotoActive, turn= gBTGotoTurn), after the mapper read. Verified 2-node one-box: A with BT_GOTO="enemy" ALONE (no autodrive) drives dist 1673->231m, holds at weapon range, mechPicks=59, autofire -> B hdlr=192, DESTROYED -- a fully automated human-style cross-pod KILL via the real beam path (not the FORCE_DMG hook). Solo un-regressed (plain autodrive still drives; goto="enemy" resolves the enemy, mechPicks=59, no crash). BT_GOTO_LOG traces the beeline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0dfbb97dcc |
Fix torso-pitch aim ray: gyro-stabilized boresight from the mech torso
The aim/pick ray was the active RENDER EYE's view direction, which made a
mech unable to aim at a distant enemy:
(a) it inherited the mech BODY pitch -- on a slope the body tilts ~8 deg to
conform to terrain, so the boresight pointed that far DOWN and the pick
ray hit the ground ~50-100m short of the target (groundPicks, never
mechPicks); and
(b) in CHASE view (the default) it was the elevated behind-the-shoulder
camera, so the ray started over the mech's shoulder and flew high.
Fix (L4VIDRND DPLEyeRenderable::Execute, the BTSetAimCamera publish):
- LEVEL the boresight: drop the view-direction pitch and rebuild an upright
basis (world +Y up) -- the guns fire along the mech's gyro-stabilized
HORIZONTAL heading, not the terrain-pitched body. Reticle X still carries
the torso twist (BTTwistToReticleX); reticle Y any elevation.
- ANCHOR the ray origin at the mech's TORSO (myEntity->localToWorld
translation + ~5) instead of the render eye, so it is correct in BOTH the
chase (default) and cockpit views.
Verified end-to-end: solo, enemy 120m ahead, BOTH chase and cockpit view ->
[target] "MECH under boresight ... mechPicks=59 groundPicks=0" -> beam ->
*** DESTROYED. Also proved PickRayHit resolves a REPLICANT when aimed at it
(hit=1 at 757m, 2-node run).
mech4: BT_GOTO="enemy" test aid now chases the nearest peer of ANY instance
(solo dummy OR replicant), not just replicants.
Still open (locomotion, not aim/MP): BT_GOTO/BT_AUTODRIVE stall short of the
enemy, so an AUTOMATED 2-node kill via the real beam isn't demonstrated yet
(the FORCE_DMG hook still is). A human can now aim correctly in the default
view. Solo un-regressed; build clean; KB validator clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
bac45851ad |
MP task #48 (partial): -net aim+drive already work; localize the real gap
Re-measured interactive -net combat on a 2-node run. The KB's "aim ray dead / drive dead in -net" was STALE: A's aim ray is live (noRay=0, the active eye publishes the aim camera) and A drives (BT_AUTODRIVE/BT_GOTO move it). The real blocker to a human cross-pod kill via the beam path is NOT the netcode: - Boresight inherits the mech BODY pitch. The aim ray = the active eye's view dir; on sloped terrain the body pitches to conform and the boresight points ~8 deg down ([EYE] up=(-.03,.99,-.14) == [pick] ldir.y=-0.145 every frame), so at range the ray hits terrain ~50-100m short of the enemy (mechPicks=0). - Drive/goto stalls ~700m short before closing to pick/weapon range. - PickRayHit vs a replicant is thus unconfirmed, but the replicant localToWorld IS correct (lstart magnitude == true A->B distance; the DeadReckon + localToWorld=localOrigin block already tracks the replicated position). Change: add a gated BT_GOTO="enemy" test mode (beeline toward the nearest live replicant) -- MP spawn coords vary per run so a fixed "x z" can't reliably face the peer. KB (multiplayer.md) updated with the corrected findings + the real remaining work (torso-pitch/level-boresight aim ray + drive-to-range). Solo un-regressed; btl4.exe builds; KB validator clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
54b6663b4d |
MP task #47 SOLVED: cross-pod damage + kill (validate the master mech)
Full cross-pod KILL now works: instance A (host 2) shoots B's mech (host 3) dead over the network; B runs the death transition on its own screen. Verified on a 2-node one-box run (BT_MP_FORCE_DMG on A): B's mech takes ~36 remote hits of 12 and is DESTROYED, with wreck swap + explosion. ROOT CAUSE (one bug): B's own MASTER mech was INVALID. Entity::Receive(Event*) does `if(!IsValid()) event->Defer()` -- so a cross-pod-delivered TakeDamage that arrived, resolved to B's real mech, and Posted, was DEFERRED forever; the handler never ran. The reconstructed Mech::Make force-set ValidFlag only for replicants (the P6 bring-up hack for the partial MakeReady handshake); a locally-created master never got validated. FIX (one line): Mech::Make now sets ValidFlag for the master too -- the reconstructed ctor builds the mech synchronously, so it's safe. Nothing else changed: targeting, the virtual victim->Dispatch, the replicant reroute, the wire, receive, and id resolution were ALL already correct. Two prior root causes were BOTH mis-diagnoses from the WRONG messageID: Entity::TakeDamageMessageID is 18, not 21. Every earlier probe filtered msgID==21, never matched a real TakeDamage, and mis-correlated the stray 21 hits' 3:19 / classID-48 against the real 3:22 hit -- inventing (a) a "cross-TU Entity layout divergence" (disproven: offsetof identical in both TUs, sizeof 444, entityID@0x17C) and (b) a "localID corrupted on the wire". Both FALSE; the id is correct end-to-end (3:22 = B's master). - game/reconstructed/mech.cpp: validate master + replicant at Make. - game/reconstructed/mech4.cpp: BT_MP_FORCE_DMG cleaned to a concise gated 2-node cross-pod reproducer (real virtual Dispatch path). - context/multiplayer.md, reconstruction-gotchas.md: corrected root cause + the validity-defers-cross-pod gotcha. Engine diagnostics reverted to clean. Solo un-regressed (the solo enemy was already force-valid; the change only ADDS ValidFlag). btl4.exe builds; full mission loop clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
04a2049ce2 |
MP task #47: DISPROVE the Entity-layout root cause; correct the record
The prior commit ( |
||
|
|
c78662a0f1 |
MP cross-pod damage ROOT-CAUSED: EntityID localID corrupted by hostID on the wire (task #47)
Traced the dispatched-message delivery end to end with BT_MP_NET / BT_MP_FORCE_DMG. Everything works except one wire step: - A's Entity::Dispatch reroutes the replicant's TakeDamage (application->SendMessage(ownerID=3, EntityManager, msg)); POST-Dispatch the message carries entityID=3:22 (host:local) -- the replicant's own ID, matching B's master (GetEntityID()==3:22). VERIFIED sent. - On B the message ARRIVES, GetEntityPointer finds an entity, Posts it, the event drains (ProcessEventTask = ProcessOneEvent(0)), Event::Process runs, Receive finds+calls a handler. VERIFIED the full deliver chain. - BUG: B receives entityID=3:19, NOT 3:22 -- the localID dropped by exactly the hostID (3) between A's send and B's receive. So GetEntityPointer(3:19) returns the WRONG entity (classID 48, not the mech 0xBB9), whose base handler ignores the unaimed hit -> 0 damage. Auto-replicated UPDATE records (msgID 18) arrive with the correct 3:22 and find the mech, so the corruption is specific to the dispatched- message wire path/direction. Next: the host-relative EntityID (de)serialization on the dispatched-msg path (RoutePacket / packet EntityID encoding) -- diff vs the update path which translates correctly. Diagnostics retained (all BT_MP_NET-gated, off in solo -- verified: solo 22 hits, 0 probe noise). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a9c3e96f20 |
MP combat step 1 (target any peer) DONE; step 2 (cross-pod damage) localised (task #46)
STEP 1 -- target any peer mech: DONE + verified. A live-mech registry (BTRegisterMech/BTDeregisterMech from the Mech ctor/dtor) collects every mech -- player, dummy, and peer replicants. The boresight world-pick walks BTGetTargetCandidates (all mechs != shooter, closest ray hit) and the whole fire/damage block retargets from the solo gEnemyMech to the picked hotTarget; missile/projectile validation generalised via BTIsRegisteredMech. Verified one-box: instance A enumerates B's mech as a live ReplicantInstance (20 zones, ownerID=3). Solo un-regressed (pick->damage->kill clean, 28 hits + DESTROYED). STEP 2 -- cross-pod damage: dispatch + engine reroute are CORRECT and invoked with a valid owner, but the dispatched message doesn't reach the master. Entity::Dispatch (ENTITY.cpp:244) reroutes a replicant's msg via application->SendMessage(ownerID,...); BT_MP_FORCE_DMG proves A dispatches at B's replicant with ownerID=3, yet B takes 0 STEP-6 damage. Corrects the KB's [T3] "Dispatch already reroutes": the reroute FIRES; the break is downstream in the transport/delivery of a dispatched entity-message (update records flow fine -- net-rx works). Next MP task = that wire delivery. Diagnostics BT_MP_LOG / BT_MP_FORCE_DMG retained. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fa88f74c68 |
HUD aspect correction: square reticle units at any window shape (task #44)
User: "it doesn't scale with our screen resolution?" -- correct. The HUD draws into the fixed 800x600 backbuffer and the present stretches it into the client area: on non-4:3 windows everything distorted (circles -> ellipses, the bottom tape widened vs the ladder, positions drifted outward). - BTGetPresentAspect (L4VIDEO, from gWindowAspect; 4:3 fallback). - dpl2d x-unit = (bbW/2)/presentAspect (== bbH/2 at 4:3 -- unchanged there), circles pre-squished in bb space so the stretch restores round. - The reticle<->NDC conversions (BTGetAimRay / BTProjectToReticle / BTProjectHotBox / BTTwistToReticleX) use the PRESENT aspect; the mouse map (BTClientToReticle) is now pure client-relative. - Placement itself is authentic and stays put: the binary clusters the instruments around the boresight (ladder x=0.35 half-heights, spans +-0.25) -- NOT at the display edges. - Crash en route: the BTGetPresentAspect extern declared INSIDE dpl2d's anonymous namespace = a different, unresolved symbol -> /FORCE garbage call (landed in MissileLauncher::DefaultData). Moved to global scope; gotcha noted in the comment. Verified 4:3 regression: aimed lock + zone hits + no crash; hud-geom MapX/MapY unchanged at 4:3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e13e8af44b |
dpl2d OpenLines = SEPARATE SEGMENT PAIRS + pip spacing 0.03 (user screenshot)
User screenshot showed three artifacts, all one root cause: my dpl2d
executor drew OpenLines/CloseLines vertex runs as a connected LINESTRIP.
Every ctor use of OpenLines is SEGMENT PAIRS (4 crosshair arms, one pair
per ladder tick, the compass stem, the lock ring's 4 ticks, threat
marks) -- the strip joined ticks into zigzags ("squiggly lines" on both
ladders), hung diagonals off the crosshair arms, and drew chords across
the lock ring. Now D3DPT_LINELIST (one line per pair).
Also PIP_SPACING: _DAT_004cdce8 is a DOUBLE = 0.03 (read from the exe);
my 0.01 guess overlapped the 0.028-wide pips (user: "pips are
overlapping"). Reference-screenshot pairs sit side-by-side, matching.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
3430f87305 |
HUD colors corrected vs the binary ctor (validated by a period screenshot)
A period pod screenshot from players who know the game flagged the overlay. Stroke-by-stroke re-read of the ctor (@4546-4587) against it: - The range caret triangle is GREEN width 1 (ctor @4550-4551 sets SetLineWidth(1)+SetColor(0,0.75,0) AFTER the yellow bar's CallList) -- my transcription left it yellow/2. Same for the bottom bowtie carets (@4569-4570). Both fixed; the caret polylines also get the binary's explicit 4th closing point (@4558). - The screenshot otherwise CONFIRMS our layout (yellow range bar, green caret, colored pip dots, tick tape + green bowtie, rotating compass, mid-ladder range with no lock = the world-pick terrain range). - Its crosshair is ~2.5x taller with arrowhead arms -- NOT our binary's +-0.04..0.16 program: a different pod revision. Ours stays [T1] to BTL4OPT.EXE 4.10. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2250b01c6b |
Fire-channel pulses moved out of the enemy block (post-kill firing froze)
User report: after killing the enemy, wandering around, weapons won't fire. Not a lock requirement -- the trigger pulse generator (the 1,0,1,0 edge feed for CheckFireEdge) sat inside `if (gEnemyMech != 0)`. Once the wreck buried and gEnemyMech nulled, the channels froze at their last value -> no rising edges -> no fire, even with a valid terrain pick in the target slot. Latent since the world-pick model made no-enemy firing meaningful (task #41); the weapon-group split kept the pulses in the same wrong place. The pulses now run unconditionally (before the enemy block); the weapon's own HasActiveTarget gate remains the only fire arbiter -- terrain downrange fires at the scenery, a true sky shot (nothing within 1200) authentically refuses. Verified: BT_AIM=0.5,0.3 (terrain-only picks, enemy never aimed at) -> sustained fire, 123 laser + 82 PPC beam samples. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
14c0dd06f5 |
Three-trigger keyboard weapon groups: 1=lasers 2=PPCs 3=missiles (task #43)
Interim pod-like grouping ahead of the authentic ConfigureMappables/ ChooseButton mapper channels (decomp-anchored in open-questions): - Three fire channels like three pod buttons: gBTWeaponTrigger (lasers, key 1 or SPACE), gBTPPCTrigger (PPCs, key 2), gBTMissileTrigger (missiles, key 3 or CTRL). Keyboard only per user (mouse buttons removed). - EmitterSimulation picks its channel by classID (PPC 3028 vs Emitter 3016); BT_AUTOFIRE holds all three. - The BT_BEAM_LOG sampler modulus 30 aliased to ONE weapon under the new synchronized 5-beam volleys (5 | 30) -> 31 (coprime). Verified: all 5 energy weapons fire (31 samples each), aimed hits land, kill completes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4e7561714e |
HUD: per-weapon pip data dump + the real authored ranges recorded
[hud] pip diagnostic logs each registered pip's classID/PipPosition/ WeaponRange/PipExtendedRange/PipColor. The live dump corrects the "everything is 500m" assumption: BLH lasers red @500 (x3, ext=1), missiles amber @800 (x2), PPCs blue @900 (x2) -- the pips sit at their weapons' max-range marks on the range ladder (caret below a pip = that weapon reaches), so 7 weapons read as 3 weapon-system groups. KB + comments corrected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5038d64382 |
Damage range gate: the authentic per-weapon effectiveRange (was a 100u stand-in)
User report: lock at spawn range (~120) but no damage until closing in. The bring-up damage gate used kWeaponRange=100 -- a placeholder from before the weapon reconstruction -- while the authored BLH weapon range is 500 m. The binary's rule (FireWeapon @004bace8:7758 [T1]): damage applies when dist <= the weapon's EFFECTIVE range = (1 - host-zone damage) x authored WeaponRange -- the per-weapon targetWithinRange flag UpdateTargeting computes each frame. The gate (and the [target] log's IN WEAPON RANGE annotation) now reads that flag across the weapon roster; kWeaponRange removed. Verified: stationary at spawn range 120 -> IN WEAPON RANGE + 34 aimed zone hits (was 0 until closing under 100). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cb110310ba |
Range caret: the authentic 500 m/s slide (HudSimulation :5652-5670)
The right-ladder caret reads range-to-pick, and with the world-pick
model the pick is usually the ground downrange -- so walking (cockpit
bob + turning sweeping the boresight across near/far terrain) made the
caret teleport frame to frame ("going all over the place", user
report). The binary rate-limits the DISPLAYED range: shown +=
clamp(true - shown, +-dt*500) -- a 500 m/s slide, applied in
HudSimulation, including toward the no-target 1200 default
(_DAT_004b7ed0 confirmed 0 = the abs() idiom). Implemented in mech4's
HUD feed where dt is in scope.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
305c928923 |
Death effects dispatch at the victim's death transition (task #42)
User report: enemy takes hits, starts smoking, then beams pass through -- no more damage, no death. Root cause: the death-effects dispatch (blhdead explosion chain + wreck swap + KILL score) lived in the SHOOTER's laser fire block kill check. A missile killing blow lands frames later in BTUpdateProjectiles -- outside that block -- and after task #41 the boresight pick skips a dead mech, so the fire block never ran against it again: the death chain was silently skipped, leaving an internally-dead, smoking, standing, invulnerable mech. Fix: the dispatch moved to the VICTIM's own once-per-death transition (UpdateDeathState's movementMode->9 moment) -- fires exactly once for ANY kill source (laser, missile, collision). Killer id for the Explosion message = lastInflictingID (the task-#31 bookkeeping). The BT_ENABLE_TEARDOWN cdb harness was removed with the old block (findings preserved in docs/HARD_PROBLEMS.md). Verified headless (75s soak, missile-heavy kill): destroyed -> 'blhdead' id=22 -> wreck swap 'blhdbr.bgf' -> smoke re-arm -> sink -> buried INERT; post-death picks fall through to terrain; 0 crashes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e30a61a62f |
World-pick targeting: 0x388 = whatever is downrange -- mech, terrain, or nothing (task #41)
User video evidence (lasers firing at nothing) + the colleague's
torso-locked report + the binary all reconcile into one model:
The boresight pick hits the WORLD, and the target slot mech+0x388 holds
whatever entity is downrange:
- enemy mech under the boresight -> aimed target (hull point -> STEP-6
zone under the boresight, hotbox + lock ring, damage);
- else the TERRAIN downrange (BTGroundRayHit to 1200) -> the beam and
missiles fire at the scenery ("firing at nothing", as in the pod
videos), range caret reads the ground distance (authentic :5639), no
damage;
- else (sky) -> no target, and the weapon's own double 0x388 gate
refuses the discharge.
Binary evidence for non-mech targets: HudSimulation :5620 explicitly
handles a target WITHOUT damage zones (target->0x120 == 0) -- dead code
if only mechs were ever targeted. The pick is automatic every frame
(0x388: 11 reads / 0 direct stores in CODE).
Implementation: gBTTerrainEntity captured in MakeEntityRenderables
(Terrain::GetClassDerivations); mech4 pick = enemy PickRayHit else
ground ray -> terrain entity + ground point; damage/hotbox/lock only
for the mech pick; the task-40 enemy auto-converge REMOVED (facing away
now fires at the scenery, not magically at the enemy -- the user's
complaint). Verified all three states headless (BT_AIM 0,0 / .6,.5 /
0,-.8): aimed zone hits / terrain beams + 0 damage / no discharge.
Fourth (and evidence-complete) targeting model iteration; KB updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
60ed54e008 |
Auto-target fire model: firing needs a target, NOT a manual lock/pinpoint (task #40)
Colleague: you fire without a lock, at "nothing." Double-checked the binary: - The weapon fire path is DOUBLY gated on mech+0x388 != 0: EmitterSimulation (FUN_004baa88:7689) only calls FireWeapon with a target, and FireWeapon (FUN_004bace8:7727) wraps its whole body -- including beamFlag(+0x46c)=1 -- in the same check. So no target => no beam, literally (cannot fire into truly empty space). [T1] - BUT a capstone scan of the entire CODE section finds 11 READS of +0x388 and ZERO direct stores: the target is written INDIRECTLY (a message/selector), i.e. AUTO-acquired -- there is no manual lock to fire. The spinning-ring LOCK (HudSimulation 5619-5634) is a separate, stricter state. So the colleague is right that no lock is needed; my port was wrong to gate firing on a pinpoint boresight-on-hull pick. Fix: mech+0x388 = the enemy whenever it is ALIVE (auto-target); firing needs only that. A pinpoint hull hit upgrades the shot to aimed-zone damage + the lock ring; off-hull with the enemy present, the beam converges on centre mass (body hits). Third correction from over-reading the RP-shared Reticle struct (after sticky-lock and mouse-cursor). Verified: BT_AIM="0.5 0.2" (off-hull) -> fires + lands damage (was 0 before); BT_AIM="0 0" (centred) -> HOT-aimed zone hits. KB swept; checkctx CLEAN. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a8a56c57c9 |
Targeting correction: reticle = TORSO BORESIGHT, not a free mouse cursor (task #39)
Colleague flagged that targeting is torso-locked with no fine cursor. Correct: the pod stick's yaw drove the TORSO TWIST (MechControlsMapper -> HUD::SetFreeAimSlew(stick_x) @cockpit+0x28C, gated on torso-horizontal-enabled; hud.hpp:167) and reticlePosition @HUD+0x1FC is COMPUTED by HudSimulation from the mech pose quaternion + target geometry, zeroed to centre with no target (part_013.c:5680) -- never a free-floating cursor. The engine Reticle struct is general (shared with Red Planet); BT drives it from the torso boresight. Fix: - Removed the mouse-cursor slew (a mis-sourced stand-in from the RP-shared struct). The crosshair is now the torso boresight: BTTwistToReticleX(torsoTwist) = tan(twist) projected through the live per-axis projection. Dead-centre on the fixed-torso BLH (TorsoHorizontalEnabled=0); you aim by steering the whole mech. - BT_AIM="x y" retained as the headless test harness. Verified: face-to-face spawn -> HOT lock + aimed zone hits (36); BT_FORCE_TURN circling -> 31 no-target vs 1 HOT (steering off-target drops the lock, as it must). KB swept (combat-damage / gauges-hud / open-questions); checkctx CLEAN. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
24ea7f13af |
HUD ironing (task #38): authentic Lock producer + simple-X mode
- LOCK rules transcribed from the exported HudSimulation (part_013.c:5619-5634): lock requires a target AND your own HUD host zone damage < 0.75 (_DAT_004b7ec4 -- a shot-up targeting computer drops lock) AND the targeted zone damage < 1.0 (_DAT_004b7ec8; whole-mech target checks zone 0 -- a wreck's dead zone can't re-lock). Box and ring are now separate signals like the binary: gBTHudLockState 1 = target held (hotbox draws), 2 = LOCKED (+ the spinning ring). - SIMPLE-X mode: the ctor's [0x99] minimal-reticle list transcribed (@4689-4705: green +-0.02..0.08 cross on the aim translate); Draw switches master <-> simple X on the PrimaryHudOn element bit (0x20), completing the recovered Execute's state-list logic. - Canopy diagnosis sharpened (open-questions): blx_cop is the torso segment's inside-skeleton mesh enclosing the eye; the black box = the skeleton render branch missing its texture + PUNCH cutout (the canopy windows are punch texels). A rendering-branch task; stays hidden by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
18d49491b7 |
Reticle Execute @004cdcf0 RECOVERED: every HUD instrument now live (task #37)
The one un-exported gap in the reticle chain, read via capstone (tools/disas2.py; the annotated disasm preserved at reference/decomp/reticle_execute_004cdcf0.disasm.txt). Draw() is now a transcription of the real per-frame logic, and the HUD attr-table names (hud.hpp ids 4/5/6/8/A/B/C/D) are CONFIRMED by their Execute usage: - Range ladder: BAR from ladder-top to the caret + caret translate. - The bottom 21-tick tape is the TORSO-TWIST indicator (NOT heading): deflection = -/+(span/2) x (RotationOfTorsoHorizontal / twist limit), attrs 4/5/6. Fixed-torso BLH reads centred -- authentic static. - The circle-with-stem is the COMPASS (attr 0xD, rad->deg rotation) at (botX, botY - 3*tickMajor - 0.03), with the THREAT trail (attr 0xC) in its rotated frame: 0.05-unit attack-direction marks, fresh < 2s red, expiring at 6s. Port feed: player TakeDamage pushes the impact direction (dormant vs the passive test dummy). - Pips (composed into subB6): hidden when destroyed (attr 1 == 1), LIT when the fire cycle is LOADED (attr 0x1c == 2; port source rechargeLevel >= 1) else the dark charging ring; filtered by the weapon-GROUP bits (weaponMode & elementMask&0xF). Range plays NO part (the stored TargetWithinRange slots are never read by Execute). - Lock ring: subB9 at frame centre SPINNING 4 deg/frame while locked ([0x9d] is the spin matrix, not a heading list). - Target HOTBOX: a rectangle hugging the projected extents (x+-4 around the top-centre hotbox point, +1/-11.5 vertical; baked K=2.8145 -- the port projects through the live per-axis projection), switching to the edge arrows past +-1.6 or behind (BTProjectHotBox, L4VIDEO). - Reticle state Off/On + PrimaryHudOn full-HUD/simple-X switch, aim translate on slew move, 3D marker + PNAME player-name mesh identified (chain still deferred). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cf9f56044e |
Aim coordinates: route all conversions through the true NDC (fixes the
sideways lock offset) The windowed present stretches the fixed 800x600 backbuffer into the client area while the projection's aspect follows the CLIENT (resize rebuild) -- so the dpl2d/reticle frame, the client frame and NDC only coincide for a backbuffer-shaped window. The mouse mapping and the square-frame ray shortcut (cx = rx*tanHalfFov) were exact at screen centre and drifted outward with a side-flipped sign: aiming left of the enemy locked as if right of it (user report). - BTSetAimProjection now publishes BOTH proj scales (P11 carries the aspect) + the backbuffer size (the dpl2d frame). NOTE: GetViewport at the loop top holds the PREVIOUS frame's last pass viewport (gauge passes shrink it) -> use the renderer's own GetWidth/GetHeight. - BTGetAimRay / BTProjectToReticle convert reticle <-> NDC per axis (ndc_x = rx * vh/vw; camera x = ndc_x/P11) -- exact for any window shape and FOV. - New BTClientToReticle undoes the present stretch for the mouse (client px -> viewport px -> reticle coords); mech4 uses it instead of the raw client-height mapping. - 1Hz aim telemetry in the [target] log (hits/noRay/noPick): verified 100% pick hit rate once the projection is live (the pre-visibility mission-load seconds report noRay -- that was the "flicker"). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9666dc5b5e |
Targeting: lock mirrors the pick per frame (sticky designation removed)
The sticky lock made aim irrelevant after the first acquisition (the crosshair starts on the face-to-face enemy, so it locked at spawn and never let go -- played as auto-targeting). The binary treats no-target as a frequent live state (fire path re-checks 0x388!=0 at every step, part_013.c:7689/7727; HUD range feed has a permanent no-target default, :5636), so the target slots now mirror the reticle pick each frame: locked while the crosshair is ON the mech, no lock otherwise. Keeping the crosshair on the enemy is the gunnery. Verified headless: BT_AIM="0 0" -> HOT + aimed zone hits; BT_AIM="0.6 0.4" -> no lock, zero damage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d78bde066d |
Authentic target acquisition LIVE: reticle slew + pick-ray lock + aimed zone damage (task #36)
The engine Reticle model (MUNGA/RETICLE.h [T0]) reconstructed end to end: - Mech::targetReticle is a real Reticle member bound to the TargetReticle attribute (0x1d), per the RP VTV analog (VTV.h targetReticle). - Crosshair slew: mouse -> client rect -> reticle coords (the pod stick free-aim channel's dev-box stand-in); BT_AIM="x y" pins it headless. LMB fires lasers / RMB missiles (alongside SPACE/CTRL). - Pick ray: the ACTIVE eye publishes pos + LookAtRH basis (BTSetAimCamera, L4VIDRND view-write site) + the render loop publishes proj._22; BTGetAimRay builds the world ray, Mech::PickRayHit slab-tests it against the collision template's ExtentBox via the engine's BoundingBox::HitBy (local frame; clips the Line at entry) -> world hull point. - Designation: the mech under the crosshair designates (sticky; re-hover refreshes; cleared when the target leaves the roster at burial); the entity target slots 0x37c/0x388/0x38c feed the whole weapon path. - Aimed fire: while HOT the impact point is the PICKED hull point -> the STEP-6 cylinder lookup resolves the zone under the crosshair (verified: center-aim -> head-band zone 13 dominant). Off-crosshair the sticky designation converges on center mass. - HUD: the aim group draws at the slewed position ([0x9a] translate, contained by push/pop); the designator ring tracks the target's projected point (subB9 hot / subB8 designated, BTProjectToReticle); edge arrows when off-screen/behind. - AUTHENTIC gating: no fire arc exists in the binary (FireWeapon fires whenever HasActiveTarget, part_013.c:7758) -> BT_FIRE_ARC is now an explicit OPT-IN presentation clamp; the hardwired gEnemyMech lock and the projectile path's gEnemyMech fallback are removed. - Fixed en route: every renderable rebuild stomped mCamera back to the chase eye (start-inside silently lost the cockpit camera; the aim feed exposed it). BTL4VideoRenderer::mViewInside persists the chosen view. Verified headless: BT_AIM="0 0" -> HOT lock, pick hits the hull face at exact range, aimed zones resolve; BT_AIM="0.8 0.3" -> no lock, zero damage, zero missile launches; kill chain completes to wreck + smoke. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
48b17750e5 |
HUD reticle + weapon pips LIVE: dpl2d 2D display-list port (task #35)
- dpl2d API fully recovered from the binary recorders (@487f34-488630): opcode model (points/lines/polyline/circle/color/width/matrix/push-pop), CallList = INLINE include (state persists to caller), centered coordinate frame (unit = half viewport height). game/reconstructed/dpl2d.cpp rework. - BTReticleRenderable ctor @004cc40c transcribed with the authentic calibration (originX .35, originY .25, scaleY .5, 0..1200m right range ladder, bottom heading tape, FUN_004cd938 tick ladders, lock rings, turn arrows); range caret slides from the live target range fed by the mech4 targeting step (BTSetHudTargetRange). - Weapon pips: the binary gate is IsDerivedFrom(0x511830 = MechWeapon::ClassDerivations) [T1: part_014.c:5386 hard-aborts on missing weapon attrs; part_012 counts + roster ORs capabilityFlags@+0x334] so ALL 7 BLH weapons register (3 lasers + 2 PPCs + 2 MissileLaunchers). Pip A (lit, authored PipColor) on TargetWithinRange, else dark ring B. - AddWeapon @004cdac0 store map corrected to the verified order (part_014.c:4827-4837); both state attrs are literally named "SimulationState" (strings @51d526/51d577) -> weapon simulationState. - Mech roster this[0x1ef] renamed poweredSubsystems -> weaponRoster (0x511830 is MechWeapon, not PoweredSubsystem=0x50f4bc); derivation-tag table added to context/decomp-reference.md. - Draw hook BTDrawReticle after the 3D scene, cockpit view only. Binary Execute @004cdcf0 is an un-exported gap -> Draw dynamics [T3], tracked in context/open-questions.md with the blx_cop canopy + PNAME pip meshes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1cd8ca1a80 |
HUD phase 1: full dpl2d API recovered + opcode-faithful 2D layer rework
Every dpl2d_ recorder in the binary self-identifies via its debug-name string
(part_010.c 0x487f34..0x4888c0) -- the complete API + opcode map is recorded
in phases/phase-02-dpl2d-reticle.md: point sets (2/3), closed polylines (4/5),
open line strips (6/7), AddPoint (8), AddCircle (9), SetColor (0xF),
Set/ConcatMatrix (0x10/0x11, 2x3 affine), Push/PopState (0x12/0x13),
SetLineWidth (0x15), CallDisplayList (nested glyphs), FullScreenClipRegion.
dpl2d.cpp reworked to that model: command-stream recorder (open-primitive
vertex runs, state commands, nested list calls) + a recursive ExecuteList
rasteriser (XYZRHW points/strips/loops/circles, 2x3 transform + state stacks,
save/restore). CORRECTION: the old "PushMatrix/MoveTo/PopMatrix" trio was a
misreading of OpenPolypoint/AddPoint/ClosePolypoint ("draw a point") -- kept
as aliases mapping to the true semantics.
Reticle findings recorded: the ctor (@004cc40c) builds ~15 display lists (the
dotted-cross reticle, tick ladders via FUN_004cd938, side arrows, arcs) plus a
3D marker chain and the PNAME1-8.bgf pip meshes (all ship). Next: transcribe
ctor+Execute, wire the 1996 caller's AddWeapon arguments, hook the draw.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
56f02b2176 |
Cockpit view live: V toggle, camera gate, real EyepointRotation, inside skeleton
- V toggles the authentic cockpit eyepoint <-> the chase camera. Both eyes coexist; DPLEyeRenderable::Execute now writes the VIEW only when it IS mCamera (unconditional writes let the last-executed eye stomp the toggle). - EyepointRotation is a real zeroed EulerAngles member on Mech (was bound to the shared junk attrPad -- the eye composes this attribute into the view EVERY frame, so the cockpit camera was rotated by garbage: the canted horizon, then the black screen). The eye-slew systems write it later. - The cockpit eye mounts at the eyepoint's REST position with a clean upright forward basis on the tree root (the live joint chain fed it the site tilt + torso pose; authentic pitch/yaw is the deferred gyro eye chain). - The inside view swaps the player to the INSIDE skeleton mesh set (SkeletonType_A): 19 body segments hide, exactly one mesh remains -- blx_cop.bgf, the authentic cockpit canopy shell around the eyepoint. It currently renders as a black enclosure (the black-screen report), so it is HIDDEN pending its interior/punch-material rendering (BT_INSIDE_COCKPIT=1 shows it for that work). Damage gstates respected in both directions; RemakeEntity keys off the DISPLAYED skeleton. - Dev: BT_START_INSIDE=1 starts in the cockpit view. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c2f70f6348 |
Beams CUT on a destroyed target (the real FUN_0049fb54, was a mislabeled stub)
Answers "can I keep targeting the dead mech?": the lock isn't cleared by anything we've found, but the binary REFUSES to sustain energy beams on a destroyed mech: ServiceDischarge (@004ba8d0) and ContinueDischarge (@004baa20) check the beam's target EVERY discharge frame -- IsDerivedFrom(Mech) then Mech::IsDestroyed (FUN_0049fb54 = movementMode 2||9) -- and kill the beam the moment it dies. EmitterSimulation (@004baa88) applies the same check to the OWNER (a dead mech's own weapons drop everything). Our recon had FUN_0049fb54 as a mislabeled "cockpit/HUD query" no-op stub called with NULL, and never set the beam's target (0x474) at fire -- all three authentic gates were dead. Now: FireWeapon stashes the owner's target entity; both discharge paths + the owner gate run the real check (BTMechDestroyed). Verified: 40 beam samples before the kill, 0 after -- lasers flash-fail on the wreck instead of cutting it like butter (heat is still spent per the binary's FireWeapon, which fires blind; only the DISCHARGE checks the corpse). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
31626a3b97 |
Beams at the models' NATURAL width, per-weapon tube models (de-cartooned)
The "cartoonish" beams: the port drew the ermlaser tube INFLATED -- a 3.0x glow + 0.9x core two-layer hack, 13x the authored radius. Parsed the beam models: ERMLASER/MLASER 0.22u radius, SLASER 0.11, LLASER 0.32, PPC 0.62 -- thin pencil beams with an authored per-class size progression; the PPC bolt is genuinely ~3x fatter with its own 62-vert shape. Now: ONE draw per beam at natural model scale (width multiplier 1.0), using the weapon's OWN tube model (ermlaser for lasers, ppc.bgf for PPCs; the s/m/llaser tubes load for other mechs' loadouts). BTPushBeamKind carries the model selector; BTPushBeam keeps its signature (kind 0). Tint = 40+215x the authored PipColor; the thin natural tubes stay under additive saturation so the scrolling grit reads without a hand-dimmed core layer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2036e7da51 |
Per-weapon beams from the REAL Emitter sim state (authentic per-mech fire)
The visible beams now come from each weapon's own live state instead of the hardcoded single-look stagger: FireWeapon arms beamFlag + dischargeTimer (the weapon's authored DischargeTime), ServiceDischarge ends the window, recharge gates cadence -- so volley-vs-stagger patterns, cadence and colours all emerge from each mech's real loadout (BLH: 3 lasers + 2 PPCs). Per weapon: live muzzle (its own mount; gun-port ordinal fallback when the mount doesn't resolve -- GetMuzzlePoint returns the feet otherwise), the fire's stored world endpoint, and the authored PipColor (lasers red 1,0,0 / PPC blue 0,0,1 straight from the .SUB data). PPC draws as a thicker, brighter bolt. Filter is by EXACT classID (Emitter 3016 / PPC 3028) -- the derivation check matched shared recon-stub chains (a Sensor and the MissileLauncher passed and drew garbage beams from misinterpreted offsets). The target slot (MECH_TARGET_POS) now carries the TORSO aim point (all consumers -- beam endpoints, missiles, reticle -- want center-mass, not the ground between the feet); the projectile fallback lift moved to match. Accessors: Emitter::BeamOn/BeamEndpoint, MechWeapon::MuzzlePoint/PipColor. BT_BEAM_LOG=1 samples the live per-weapon beam state. Verified: [beam] PPC mz=(...,4.49,...) end=(...,7,...) rgb=(0,0,1) -- gun-port muzzles, torso endpoints, authored colours; kill chain intact (10 hits). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a834866fcd |
Enemy spawn faces the player (was copying the player's pose -> back turned)
The dummy spawned 120u along the player's forward with the SAME orientation, so both mechs faced the same way and the player stared at its back (also why first shots read as rear hits). Flip the enemy's yaw 180 deg about Y (engine convention: Z basis = (sin y, 0, cos y) -> yaw = atan2(z.x, z.z) + pi) so the two spawn face to face. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |