Skip to content

Commit 5269ad2

Browse files
authored
feat: Add spectator mode entity spectating (#369)
1 parent f126f56 commit 5269ad2

14 files changed

+150
-53
lines changed

renderer/viewer/lib/basePlayerState.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,27 @@ export const getInitialPlayerState = () => proxy({
4747
shouldHideHand: false,
4848
heldItemMain: undefined as HandItemBlock | undefined,
4949
heldItemOff: undefined as HandItemBlock | undefined,
50+
51+
cameraSpectatingEntity: undefined as number | undefined,
52+
})
53+
54+
export const getPlayerStateUtils = (reactive: PlayerStateReactive) => ({
55+
isSpectator () {
56+
return reactive.gameMode === 'spectator'
57+
},
58+
isSpectatingEntity () {
59+
return reactive.cameraSpectatingEntity !== undefined && reactive.gameMode === 'spectator'
60+
}
5061
})
5162

5263
export const getInitialPlayerStateRenderer = () => ({
5364
reactive: getInitialPlayerState()
5465
})
5566

5667
export type PlayerStateReactive = ReturnType<typeof getInitialPlayerState>
68+
export type PlayerStateUtils = ReturnType<typeof getPlayerStateUtils>
5769

58-
export interface PlayerStateRenderer {
59-
reactive: PlayerStateReactive
60-
}
70+
export type PlayerStateRenderer = PlayerStateReactive
6171

6272
export const getItemSelector = (playerState: PlayerStateRenderer, specificProperties: ItemSpecificContextProperties, item?: import('prismarine-item').Item) => {
6373
return {

renderer/viewer/lib/worldrendererCommon.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAsset
1616
import { chunkPos } from './simpleUtils'
1717
import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
1818
import { WorldDataEmitterWorker } from './worldDataEmitter'
19-
import { PlayerStateRenderer } from './basePlayerState'
19+
import { getPlayerStateUtils, PlayerStateReactive, PlayerStateRenderer, PlayerStateUtils } from './basePlayerState'
2020
import { MesherLogReader } from './mesherlogReader'
2121
import { setSkinsConfig } from './utils/skins'
2222

@@ -156,7 +156,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
156156
abstract changeBackgroundColor (color: [number, number, number]): void
157157

158158
worldRendererConfig: WorldRendererConfig
159-
playerState: PlayerStateRenderer
159+
playerStateReactive: PlayerStateReactive
160+
playerStateUtils: PlayerStateUtils
160161
reactiveState: RendererReactiveState
161162
mesherLogReader: MesherLogReader | undefined
162163
forceCallFromMesherReplayer = false
@@ -191,7 +192,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
191192
constructor (public readonly resourcesManager: ResourcesManagerTransferred, public displayOptions: DisplayWorldOptions, public initOptions: GraphicsInitOptions) {
192193
this.snapshotInitialValues()
193194
this.worldRendererConfig = displayOptions.inWorldRenderingConfig
194-
this.playerState = displayOptions.playerState
195+
this.playerStateReactive = displayOptions.playerStateReactive
196+
this.playerStateUtils = getPlayerStateUtils(this.playerStateReactive)
195197
this.reactiveState = displayOptions.rendererState
196198
// this.mesherLogReader = new MesherLogReader(this)
197199
this.renderUpdateEmitter.on('update', () => {
@@ -304,11 +306,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
304306
}
305307
}
306308

307-
onReactivePlayerStateUpdated<T extends keyof typeof this.displayOptions.playerState.reactive>(key: T, callback: (value: typeof this.displayOptions.playerState.reactive[T]) => void, initial = true) {
309+
onReactivePlayerStateUpdated<T extends keyof PlayerStateReactive>(key: T, callback: (value: PlayerStateReactive[T]) => void, initial = true) {
308310
if (initial) {
309-
callback(this.displayOptions.playerState.reactive[key])
311+
callback(this.playerStateReactive[key])
310312
}
311-
subscribeKey(this.displayOptions.playerState.reactive, key, callback)
313+
subscribeKey(this.playerStateReactive, key, callback)
312314
}
313315

314316
onReactiveConfigUpdated<T extends keyof typeof this.worldRendererConfig>(key: T, callback: (value: typeof this.worldRendererConfig[T]) => void) {

renderer/viewer/three/cameraShake.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export class CameraShake {
2121
this.update()
2222
}
2323

24+
getBaseRotation () {
25+
return { pitch: this.basePitch, yaw: this.baseYaw }
26+
}
27+
2428
shakeFromDamage (yaw?: number) {
2529
// Add roll animation
2630
const startRoll = this.rollAngle
@@ -35,6 +39,11 @@ export class CameraShake {
3539
}
3640

3741
update () {
42+
if (this.worldRenderer.playerStateUtils.isSpectatingEntity()) {
43+
// Remove any shaking when spectating
44+
this.rollAngle = 0
45+
this.rollAnimation = undefined
46+
}
3847
// Update roll animation
3948
if (this.rollAnimation) {
4049
const now = performance.now()

renderer/viewer/three/entities.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -870,9 +870,7 @@ export class Entities {
870870

871871
const meta = getGeneralEntitiesMetadata(entity)
872872

873-
//@ts-expect-error
874-
// set visibility
875-
const isInvisible = entity.metadata?.[0] & 0x20
873+
const isInvisible = ((entity.metadata?.[0] ?? 0) as unknown as number) & 0x20 || (this.worldRenderer.playerStateReactive.cameraSpectatingEntity === entity.id && this.worldRenderer.playerStateUtils.isSpectator())
876874
for (const child of mesh!.children ?? []) {
877875
if (child.name !== 'nametag') {
878876
child.visible = !isInvisible

renderer/viewer/three/holdingBlock.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,10 @@ export default class HoldingBlock {
116116
offHandModeLegacy = false
117117

118118
swingAnimator: HandSwingAnimator | undefined
119-
playerState: PlayerStateRenderer
120119
config: WorldRendererConfig
121120

122121
constructor (public worldRenderer: WorldRendererThree, public offHand = false) {
123122
this.initCameraGroup()
124-
this.playerState = worldRenderer.displayOptions.playerState
125123
this.worldRenderer.onReactivePlayerStateUpdated('heldItemMain', () => {
126124
if (!this.offHand) {
127125
this.updateItem()
@@ -146,9 +144,9 @@ export default class HoldingBlock {
146144
// now watch over the player skin
147145
watchProperty(
148146
async () => {
149-
return getMyHand(this.playerState.reactive.playerSkin, this.playerState.reactive.onlineMode ? this.playerState.reactive.username : undefined)
147+
return getMyHand(this.worldRenderer.playerStateReactive.playerSkin, this.worldRenderer.playerStateReactive.onlineMode ? this.worldRenderer.playerStateReactive.username : undefined)
150148
},
151-
this.playerState.reactive,
149+
this.worldRenderer.playerStateReactive,
152150
'playerSkin',
153151
(newHand) => {
154152
if (newHand) {
@@ -167,7 +165,7 @@ export default class HoldingBlock {
167165

168166
updateItem () {
169167
if (!this.ready) return
170-
const item = this.offHand ? this.playerState.reactive.heldItemOff : this.playerState.reactive.heldItemMain
168+
const item = this.offHand ? this.worldRenderer.playerStateReactive.heldItemOff : this.worldRenderer.playerStateReactive.heldItemMain
171169
if (item) {
172170
void this.setNewItem(item)
173171
} else if (this.offHand) {
@@ -357,8 +355,8 @@ export default class HoldingBlock {
357355
itemId: handItem.id,
358356
}, {
359357
'minecraft:display_context': 'firstperson',
360-
'minecraft:use_duration': this.playerState.reactive.itemUsageTicks,
361-
'minecraft:using_item': !!this.playerState.reactive.itemUsageTicks,
358+
'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks,
359+
'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks,
362360
}, this.lastItemModelName)
363361
if (result) {
364362
const { mesh: itemMesh, isBlock, modelName } = result
@@ -475,7 +473,7 @@ export default class HoldingBlock {
475473
this.swingAnimator = new HandSwingAnimator(this.holdingBlockInnerGroup)
476474
this.swingAnimator.type = result.type
477475
if (this.config.viewBobbing) {
478-
this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.playerState)
476+
this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.worldRenderer.playerStateReactive)
479477
}
480478
}
481479

@@ -710,7 +708,7 @@ class HandIdleAnimator {
710708

711709
// Check for state changes from player state
712710
if (this.playerState) {
713-
const newState = this.playerState.reactive.movementState
711+
const newState = this.playerState.movementState
714712
if (newState !== this.targetState) {
715713
this.setState(newState)
716714
}

renderer/viewer/three/panorama.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export class PanoramaRenderer {
197197
version,
198198
worldView,
199199
inWorldRenderingConfig: defaultWorldRendererConfig,
200-
playerState: getInitialPlayerStateRenderer(),
200+
playerStateReactive: getInitialPlayerStateRenderer().reactive,
201201
rendererState: getDefaultRendererState().reactive,
202202
nonReactiveState: getDefaultRendererState().nonReactive
203203
}

renderer/viewer/three/world/cursorBlock.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,14 @@ export class CursorBlock {
6161
this.blockBreakMesh.name = 'blockBreakMesh'
6262
this.worldRenderer.scene.add(this.blockBreakMesh)
6363

64-
subscribeKey(this.worldRenderer.playerState.reactive, 'gameMode', () => {
64+
this.worldRenderer.onReactivePlayerStateUpdated('gameMode', () => {
6565
this.updateLineMaterial()
6666
})
67-
68-
this.updateLineMaterial()
6967
}
7068

7169
// Update functions
7270
updateLineMaterial () {
73-
const inCreative = this.worldRenderer.displayOptions.playerState.reactive.gameMode === 'creative'
71+
const inCreative = this.worldRenderer.playerStateReactive.gameMode === 'creative'
7472
const pixelRatio = this.worldRenderer.renderer.getPixelRatio()
7573

7674
this.cursorLineMaterial = new LineMaterial({

renderer/viewer/three/worldrendererThree.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ export class WorldRendererThree extends WorldRendererCommon {
6969
}
7070
fountains: Fountain[] = []
7171

72+
private currentPosTween?: tweenJs.Tween<THREE.Vector3>
73+
private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }>
74+
7275
get tilesRendered () {
7376
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
7477
}
@@ -150,7 +153,7 @@ export class WorldRendererThree extends WorldRendererCommon {
150153
override watchReactivePlayerState () {
151154
super.watchReactivePlayerState()
152155
this.onReactivePlayerStateUpdated('inWater', (value) => {
153-
this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.displayOptions.playerState.reactive.waterBreathing ? 100 : 20) : null
156+
this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.playerStateReactive.waterBreathing ? 100 : 20) : null
154157
})
155158
this.onReactivePlayerStateUpdated('ambientLight', (value) => {
156159
if (!value) return
@@ -238,7 +241,7 @@ export class WorldRendererThree extends WorldRendererCommon {
238241
}
239242

240243
getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) {
241-
return getItemUv(item, specificProps, this.resourcesManager, this.playerState)
244+
return getItemUv(item, specificProps, this.resourcesManager, this.playerStateReactive)
242245
}
243246

244247
async demoModel () {
@@ -430,7 +433,7 @@ export class WorldRendererThree extends WorldRendererCommon {
430433
}
431434

432435
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
433-
const yOffset = this.displayOptions.playerState.reactive.eyeHeight
436+
const yOffset = this.playerStateReactive.eyeHeight
434437

435438
this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
436439
this.media.tryIntersectMedia()
@@ -448,10 +451,28 @@ export class WorldRendererThree extends WorldRendererCommon {
448451
pos.y -= this.camera.position.y // Fix Y position of camera in world
449452
}
450453

451-
new tweenJs.Tween(this.cameraObject.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start()
454+
this.currentPosTween?.stop()
455+
this.currentPosTween = new tweenJs.Tween(this.cameraObject.position).to({ x: pos.x, y: pos.y, z: pos.z }, this.playerStateUtils.isSpectatingEntity() ? 150 : 50).start()
452456
// this.freeFlyState.position = pos
453457
}
454-
this.cameraShake.setBaseRotation(pitch, yaw)
458+
459+
if (this.playerStateUtils.isSpectatingEntity()) {
460+
const rotation = this.cameraShake.getBaseRotation()
461+
// wrap in the correct direction
462+
let yawOffset = 0
463+
const halfPi = Math.PI / 2
464+
if (rotation.yaw < halfPi && yaw > Math.PI + halfPi) {
465+
yawOffset = -Math.PI * 2
466+
} else if (yaw < halfPi && rotation.yaw > Math.PI + halfPi) {
467+
yawOffset = Math.PI * 2
468+
}
469+
this.currentRotTween?.stop()
470+
this.currentRotTween = new tweenJs.Tween(rotation).to({ pitch, yaw: yaw + yawOffset }, 100)
471+
.onUpdate(params => this.cameraShake.setBaseRotation(params.pitch, params.yaw - yawOffset)).start()
472+
} else {
473+
this.currentRotTween?.stop()
474+
this.cameraShake.setBaseRotation(pitch, yaw)
475+
}
455476
}
456477

457478
debugChunksVisibilityOverride () {
@@ -508,7 +529,7 @@ export class WorldRendererThree extends WorldRendererCommon {
508529
const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
509530
this.renderer.render(this.scene, cam)
510531

511-
if (this.displayOptions.inWorldRenderingConfig.showHand && this.playerState.reactive.gameMode !== 'spectator' /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) {
532+
if (this.displayOptions.inWorldRenderingConfig.showHand && this.playerStateReactive.gameMode !== 'spectator' /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) {
512533
this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
513534
this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
514535
}

src/appViewer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { WorldDataEmitter, WorldDataEmitterWorker } from 'renderer/viewer/lib/worldDataEmitter'
2-
import { getInitialPlayerState, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState'
2+
import { getInitialPlayerState, PlayerStateRenderer, PlayerStateReactive } from 'renderer/viewer/lib/basePlayerState'
33
import { subscribeKey } from 'valtio/utils'
44
import { defaultWorldRendererConfig, WorldRendererConfig } from 'renderer/viewer/lib/worldrendererCommon'
55
import { Vec3 } from 'vec3'
@@ -67,7 +67,7 @@ export interface DisplayWorldOptions {
6767
version: string
6868
worldView: WorldDataEmitterWorker
6969
inWorldRenderingConfig: WorldRendererConfig
70-
playerState: PlayerStateRenderer
70+
playerStateReactive: PlayerStateReactive
7171
rendererState: RendererReactiveState
7272
nonReactiveState: NonReactiveState
7373
}
@@ -180,7 +180,7 @@ export class AppViewer {
180180
this.worldView!.listenToBot(bot)
181181
}
182182

183-
async startWorld (world, renderDistance: number, playerStateSend: PlayerStateRenderer = this.playerState) {
183+
async startWorld (world, renderDistance: number, playerStateSend: PlayerStateRenderer = this.playerState.reactive) {
184184
if (this.currentDisplay === 'world') throw new Error('World already started')
185185
this.currentDisplay = 'world'
186186
const startPosition = bot.entity?.position ?? new Vec3(0, 64, 0)
@@ -192,7 +192,7 @@ export class AppViewer {
192192
version: this.resourcesManager.currentConfig!.version,
193193
worldView: this.worldView,
194194
inWorldRenderingConfig: this.inWorldRenderingConfig,
195-
playerState: playerStateSend,
195+
playerStateReactive: playerStateSend,
196196
rendererState: this.rendererState,
197197
nonReactiveState: this.nonReactiveState
198198
}

src/cameraRotationControls.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function onCameraMove (e: MouseEvent | CameraMoveEvent) {
1818
if (!isGameActive(true)) return
1919
if (e.type === 'mousemove' && !document.pointerLockElement) return
2020
e.stopPropagation?.()
21+
if (appViewer.playerState.utils.isSpectatingEntity()) return
2122
const now = performance.now()
2223
// todo: limit camera movement for now to avoid unexpected jumps
2324
if (now - lastMouseMove < 4 && !options.preciseMouseInput) return
@@ -32,7 +33,6 @@ export function onCameraMove (e: MouseEvent | CameraMoveEvent) {
3233
updateMotion()
3334
}
3435

35-
3636
export const moveCameraRawHandler = ({ x, y }: { x: number; y: number }) => {
3737
const maxPitch = 0.5 * Math.PI
3838
const minPitch = -0.5 * Math.PI

0 commit comments

Comments
 (0)