Skip to content

Commit 4716003

Browse files
committed
fix: restore minigun carry and isolate item previews
1 parent 442bd92 commit 4716003

4 files changed

Lines changed: 60 additions & 7 deletions

File tree

src/game/builder.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
22
import { Box3, BoxGeometry, Matrix4, Mesh, Vector3 } from 'three'
33
import { FULL_MASK, type PlacedPiece, useBoots } from '../store'
44
import {
5+
builderPreviewActive,
56
builderDebug,
67
cancelPieceClad,
78
CELLS,
@@ -98,6 +99,14 @@ function placed(
9899
return { id: nextId++, piece, position: [x, y, z], yaw, mask }
99100
}
100101

102+
describe('builder preview ownership', () => {
103+
test('the wall ghost yields while catalog furniture is being placed or moved', () => {
104+
expect(builderPreviewActive('builder', false)).toBe(true)
105+
expect(builderPreviewActive('builder', true)).toBe(false)
106+
expect(builderPreviewActive('minigun', false)).toBe(false)
107+
})
108+
})
109+
101110
// --- Slot-locked ghost: grid targeting × piece-slots authority ---------------
102111

103112
/** Open world: nothing occupied, everything supported (pure grid checks). */

src/game/builder.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,6 +1566,12 @@ function applyEditExitTransform(id: number): void {
15661566
sfx.place()
15671567
}
15681568

1569+
/** The item catalog and builder share LMB and world-preview space. Once an
1570+
* item ghost is active it owns both, even if the hammer is still selected. */
1571+
export function builderPreviewActive(weapon: string, itemPreview: boolean): boolean {
1572+
return weapon === 'builder' && !itemPreview
1573+
}
1574+
15691575
export function Builder() {
15701576
const ghostRef = useRef<Group>(null)
15711577
const [ghost, setGhost] = useState<GhostState | null>(null)
@@ -1633,10 +1639,14 @@ export function Builder() {
16331639
if (!session) return
16341640
placeCooldown.current -= dt
16351641

1636-
if (!active) {
1642+
// Catalog placement owns the world preview completely. In particular,
1643+
// moving furniture with L while the builder is the underlying weapon must
1644+
// not leave a wall ghost between the player and the furniture ghost.
1645+
if (!builderPreviewActive(weapon, itemGhostActive())) {
16371646
if (ghost) setGhost(null)
16381647
if (edit) {
1639-
// Weapon-switch closes the edit too — same exit-time confirm.
1648+
// A weapon switch or catalog placement closes edit mode with the same
1649+
// exit-time confirmation.
16401650
applyEditExitTransform(edit.id)
16411651
setEdit(null)
16421652
}

src/game/remote-players.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import {
7373
rosterNames,
7474
SPEAK_SCALE_X,
7575
twoHanded,
76+
usesPosedGripHands,
7677
} from './remote-players'
7778
import { presenceDebug, wrapAngle } from './presence'
7879
import { gripToShoulder, leftGripFor } from './hand-grips'
@@ -517,6 +518,21 @@ describe('articulation — gait, airborne, slump', () => {
517518
expect(refs.head.current.rotation.y).toBeCloseTo(0.2, 12)
518519
})
519520

521+
test('a heavy weapon can keep native hands when no posed replacement is mounted', () => {
522+
const mk = () => ({ current: new Group() })
523+
const refs = {
524+
torso: mk(), head: mk(), armL: mk(), armR: mk(), legL: mk(), legR: mk(),
525+
headDetail: mk(), bodyDetail: mk(), handL: mk(), handR: mk(),
526+
fistL: { current: null }, fistR: { current: null },
527+
}
528+
const a = createArticulation()
529+
a.gripR = 1
530+
a.gripL = 1
531+
applyArticulation(refs, a)
532+
expect(refs.handR.current.scale.x).toBe(1)
533+
expect(refs.handL.current.scale.x).toBe(1)
534+
})
535+
520536
test('placeRoot lifts by the bob and sways along the body\'s own +x, whatever the yaw', () => {
521537
const root = new Group()
522538
const a = createArticulation()
@@ -1143,6 +1159,11 @@ describe('avatar hands — the first-person grip table on the third-person arms'
11431159
for (const w of ['knife', 'hammer', 'builder', 'paint', '', 'laser-of-the-future']) expect(twoHanded(w)).toBe(false)
11441160
})
11451161

1162+
test('minigun retains its centered native-hand carry; the knife gets its exact posed grip', () => {
1163+
expect(usesPosedGripHands('minigun')).toBe(false)
1164+
expect(usesPosedGripHands('knife')).toBe(true)
1165+
})
1166+
11461167
test('the speaking halo encloses the 0.72 × 0.18 tag with a margin and its inner edge clears the tag', () => {
11471168
const outerX = 0.19 * SPEAK_SCALE_X
11481169
const innerX = 0.16 * SPEAK_SCALE_X

src/game/remote-players.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -824,7 +824,9 @@ export function applyArticulation(refs: AvatarRigRefs, a: AvatarArticulation): v
824824
fist: { current: Group | null } | undefined,
825825
grip: number,
826826
) => {
827-
const gripping = grip > 0.5
827+
// A native hand is hidden only when a mounted replacement can take its
828+
// place. Some heavy weapons deliberately use the model hands instead.
829+
const gripping = grip > 0.5 && fist?.current != null
828830
// Switch on one threshold: the replacement never overlaps a visible
829831
// native hand, including during weapon-change interpolation.
830832
if (bone?.current) bone.current.scale.setScalar(gripping ? HAND_COLLAPSE : 1)
@@ -1711,6 +1713,14 @@ export function twoHanded(weapon: string): boolean {
17111713
return grip !== 'none' && GRIPS[grip].foregrip > 0 && leftGripFor(weapon) !== null
17121714
}
17131715

1716+
/** The minigun reads better with the Pascaline model's native hands: its
1717+
* broad two-arm carry already lands both wrists on the grips, while the small
1718+
* procedural hands make the heavy body look offset to one side. Thin tools
1719+
* such as the knife still need a posed hand wrapped around their handle. */
1720+
export function usesPosedGripHands(weapon: string): boolean {
1721+
return weapon !== 'minigun'
1722+
}
1723+
17141724
/**
17151725
* The held weapon, in THE ARM FRAME: hanging −y from the shoulder, barrel down
17161726
* the arm (weapon-models contract: grip at the origin, barrel down −Z), muzzle
@@ -1737,6 +1747,7 @@ function HeldWeapon({
17371747
const muzzle = refs.fx ? remoteMuzzle(weapon) : null
17381748
const hold = holdFor(weapon)
17391749
const support = twoHanded(weapon) ? hold.left : null
1750+
const posedHands = usesPosedGripHands(weapon)
17401751
const qR = useMemo(() => gripQuaternion(hold.right, 'R', new Quaternion()), [hold])
17411752
const qL = useMemo(() => (support ? gripQuaternion(support, 'L', new Quaternion()) : null), [support])
17421753
if (!Weapon) return null
@@ -1771,10 +1782,12 @@ function HeldWeapon({
17711782
/>
17721783
</group>
17731784
) : null}
1774-
<group position={[r[0], r[1], r[2]]} quaternion={qR} ref={refs.fistR} visible={false}>
1775-
<HandMesh pose={hold.right.pose} side="R" scale={AVATAR_GRIP_HAND_SCALE} />
1776-
</group>
1777-
{support && qL ? (
1785+
{posedHands ? (
1786+
<group position={[r[0], r[1], r[2]]} quaternion={qR} ref={refs.fistR} visible={false}>
1787+
<HandMesh pose={hold.right.pose} side="R" scale={AVATAR_GRIP_HAND_SCALE} />
1788+
</group>
1789+
) : null}
1790+
{posedHands && support && qL ? (
17781791
<group
17791792
position={[support.position[0], support.position[1], support.position[2]]}
17801793
quaternion={qL}

0 commit comments

Comments
 (0)