mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +02:00
feat: upgrade tankopediaEphemeral
This commit is contained in:
1 parent
b6c3fdc4e9
commit
ac29d1ead0
28 files changed
+165
-168
No files matched your search
@@ -2,24 +2,49 @@ import { produce } from 'immer';
|
|||||||
import { merge } from 'lodash-es';
|
import { merge } from 'lodash-es';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export class Varuna<Type> {
|
interface ProviderProps<Arguments> {
|
||||||
|
args: Arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Varuna<Type, Arguments = void> {
|
||||||
private listeners = new Set<(state: Type) => void>();
|
private listeners = new Set<(state: Type) => void>();
|
||||||
public initial: Type;
|
|
||||||
|
private initialized = false;
|
||||||
|
private _initial?: Type;
|
||||||
|
private _state?: Type;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public state: Type,
|
private creator: Type | ((...args: Arguments[]) => Type),
|
||||||
persistence?: string,
|
private persistence?: string,
|
||||||
) {
|
) {
|
||||||
if (persistence) {
|
if (typeof creator !== 'function') this.initialize(creator);
|
||||||
const dehydrated = localStorage.getItem(persistence);
|
}
|
||||||
|
|
||||||
|
Provider({ args }: ProviderProps<Arguments>) {
|
||||||
|
if (typeof this.creator !== 'function') {
|
||||||
|
throw new Error('Provider must be used with a function creator');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialize((this.creator as (...args: Arguments[]) => Type)(args));
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize(initial: Type) {
|
||||||
|
let data = initial;
|
||||||
|
|
||||||
|
if (this.persistence) {
|
||||||
|
const dehydrated = localStorage.getItem(this.persistence);
|
||||||
|
|
||||||
if (dehydrated) {
|
if (dehydrated) {
|
||||||
const rehydrated = JSON.parse(dehydrated) as unknown;
|
const rehydrated = JSON.parse(dehydrated) as unknown;
|
||||||
this.state = merge(this.state, rehydrated);
|
data = merge(data, rehydrated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.initial = this.state;
|
this.initialized = true;
|
||||||
|
this._initial = data;
|
||||||
|
this._state = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private dispatch() {
|
private dispatch() {
|
||||||
@@ -46,11 +71,26 @@ export class Varuna<Type> {
|
|||||||
this.set(produce(this.state, mutator));
|
this.set(produce(this.state, mutator));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private assertInitialized() {
|
||||||
|
if (!this.initialized) throw new Error('State used before initialization');
|
||||||
|
}
|
||||||
|
|
||||||
|
get state() {
|
||||||
|
this.assertInitialized();
|
||||||
|
return this._state as Type;
|
||||||
|
}
|
||||||
|
|
||||||
set(state: Type) {
|
set(state: Type) {
|
||||||
this.state = state;
|
this.assertInitialized();
|
||||||
|
this._state = state;
|
||||||
this.dispatch();
|
this.dispatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get initial() {
|
||||||
|
this.assertInitialized();
|
||||||
|
return this._initial as Type;
|
||||||
|
}
|
||||||
|
|
||||||
use<Slice = Type>(
|
use<Slice = Type>(
|
||||||
slicer: (state: Type) => Slice = (state) => state as unknown as Slice,
|
slicer: (state: Type) => Slice = (state) => state as unknown as Slice,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -21,11 +21,9 @@ export function ArmorPlateDisplay() {
|
|||||||
const highlightArmor = TankopediaEphemeral.use(
|
const highlightArmor = TankopediaEphemeral.use(
|
||||||
(state) => state.highlightArmor,
|
(state) => state.highlightArmor,
|
||||||
);
|
);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
|
|
||||||
if (highlightArmor === undefined) return null;
|
if (highlightArmor === undefined) return null;
|
||||||
|
|
||||||
@@ -95,7 +93,7 @@ export function ArmorPlateDisplay() {
|
|||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
input.current?.blur();
|
input.current?.blur();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
const { protagonist } = duelStore.getState();
|
const { protagonist } = duelStore.getState();
|
||||||
const tank = draft.model;
|
const tank = draft.model;
|
||||||
@@ -119,7 +117,7 @@ export function ArmorPlateDisplay() {
|
|||||||
});
|
});
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === 'Escape') {
|
||||||
input.current?.blur();
|
input.current?.blur();
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -137,7 +135,7 @@ export function ArmorPlateDisplay() {
|
|||||||
mt="2"
|
mt="2"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
if (!highlightArmor.editingPlate) return;
|
if (!highlightArmor.editingPlate) return;
|
||||||
|
|
||||||
const index = resolveArmorIndex(highlightArmor.name);
|
const index = resolveArmorIndex(highlightArmor.name);
|
||||||
@@ -145,8 +143,7 @@ export function ArmorPlateDisplay() {
|
|||||||
if (index === undefined) return;
|
if (index === undefined) return;
|
||||||
|
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
const initialState =
|
const initialState = TankopediaEphemeral.initial;
|
||||||
tankopediaEphemeralStore.getInitialState();
|
|
||||||
const { protagonist } = duelStore.getState();
|
const { protagonist } = duelStore.getState();
|
||||||
const tank = draft.model;
|
const tank = draft.model;
|
||||||
const initialTank = initialState.model;
|
const initialTank = initialState.model;
|
||||||
|
|||||||
+3
-7
@@ -43,7 +43,6 @@ export function PrimaryArmorSceneComponent({
|
|||||||
}: PrimaryArmorSceneComponentProps) {
|
}: PrimaryArmorSceneComponentProps) {
|
||||||
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
vertexShader,
|
vertexShader,
|
||||||
@@ -82,7 +81,7 @@ export function PrimaryArmorSceneComponent({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleShellChange() {
|
function handleShellChange() {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
|
|
||||||
material.uniforms.caliber.value = shell.caliber;
|
material.uniforms.caliber.value = shell.caliber;
|
||||||
@@ -136,7 +135,7 @@ export function PrimaryArmorSceneComponent({
|
|||||||
noInvalidate = false,
|
noInvalidate = false,
|
||||||
) {
|
) {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
const penetration = shell.penetration.near;
|
const penetration = shell.penetration.near;
|
||||||
const hasCalibratedShells = hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
@@ -168,10 +167,7 @@ export function PrimaryArmorSceneComponent({
|
|||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
||||||
tankopediaEphemeralStore.subscribe(
|
TankopediaEphemeral.on((state) => state.customShell, handleShellChange),
|
||||||
(state) => state.customShell,
|
|
||||||
handleShellChange,
|
|
||||||
),
|
|
||||||
tankopediaPersistentStore.subscribe(
|
tankopediaPersistentStore.subscribe(
|
||||||
(state) => state.greenPenetration,
|
(state) => state.greenPenetration,
|
||||||
handleGreenPenetrationChange,
|
handleGreenPenetrationChange,
|
||||||
|
|||||||
+3
-7
@@ -29,7 +29,6 @@ export function SpacedArmorSubExternal({
|
|||||||
variant,
|
variant,
|
||||||
clip,
|
clip,
|
||||||
}: SpacedArmorSubExternalProps) {
|
}: SpacedArmorSubExternalProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
@@ -50,7 +49,7 @@ export function SpacedArmorSubExternal({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleShellChange() {
|
function handleShellChange() {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
material.uniforms.penetration.value = shell.penetration.near;
|
material.uniforms.penetration.value = shell.penetration.near;
|
||||||
}
|
}
|
||||||
@@ -67,7 +66,7 @@ export function SpacedArmorSubExternal({
|
|||||||
}
|
}
|
||||||
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
const penetration = shell.penetration.near;
|
const penetration = shell.penetration.near;
|
||||||
const hasCalibratedShells = hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
@@ -91,10 +90,7 @@ export function SpacedArmorSubExternal({
|
|||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
||||||
tankopediaEphemeralStore.subscribe(
|
TankopediaEphemeral.on((state) => state.customShell, handleShellChange),
|
||||||
(state) => state.customShell,
|
|
||||||
handleShellChange,
|
|
||||||
),
|
|
||||||
duelStore.subscribe(
|
duelStore.subscribe(
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
handleProtagonistEquipmentChange,
|
handleProtagonistEquipmentChange,
|
||||||
|
|||||||
+3
-7
@@ -30,7 +30,6 @@ export function SpacedArmorSubSpaced({
|
|||||||
node,
|
node,
|
||||||
thickness,
|
thickness,
|
||||||
}: SpacedArmorSubSpacedProps) {
|
}: SpacedArmorSubSpacedProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
@@ -52,7 +51,7 @@ export function SpacedArmorSubSpaced({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleShellChange() {
|
function handleShellChange() {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
|
|
||||||
material.uniforms.penetration.value = shell.penetration.near;
|
material.uniforms.penetration.value = shell.penetration.near;
|
||||||
@@ -77,7 +76,7 @@ export function SpacedArmorSubSpaced({
|
|||||||
}
|
}
|
||||||
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
const duel = duelStore.getState();
|
const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
const penetration = shell.penetration.near;
|
const penetration = shell.penetration.near;
|
||||||
const hasCalibratedShells = hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
@@ -101,10 +100,7 @@ export function SpacedArmorSubSpaced({
|
|||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
||||||
tankopediaEphemeralStore.subscribe(
|
TankopediaEphemeral.on((state) => state.customShell, handleShellChange),
|
||||||
(state) => state.customShell,
|
|
||||||
handleShellChange,
|
|
||||||
),
|
|
||||||
duelStore.subscribe(
|
duelStore.subscribe(
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
handleProtagonistEquipmentChange,
|
handleProtagonistEquipmentChange,
|
||||||
|
|||||||
+4
-3
@@ -76,7 +76,6 @@ export function SpacedArmorSceneComponent({
|
|||||||
clip,
|
clip,
|
||||||
...props
|
...props
|
||||||
}: SpacedArmorSceneComponentProps) {
|
}: SpacedArmorSceneComponentProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
|
|
||||||
@@ -88,7 +87,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
remainingPenetrationInput?: number,
|
remainingPenetrationInput?: number,
|
||||||
) => {
|
) => {
|
||||||
const { antagonist, protagonist } = duelStore.getState();
|
const { antagonist, protagonist } = duelStore.getState();
|
||||||
const { customShell } = tankopediaEphemeralStore.getState();
|
const { customShell } = TankopediaEphemeral.state;
|
||||||
const shell = customShell ?? antagonist.shell;
|
const shell = customShell ?? antagonist.shell;
|
||||||
const cameraNormal = camera.position.clone().sub(point).normalize();
|
const cameraNormal = camera.position.clone().sub(point).normalize();
|
||||||
const shot: Shot = {
|
const shot: Shot = {
|
||||||
@@ -391,7 +390,9 @@ export function SpacedArmorSceneComponent({
|
|||||||
|
|
||||||
const shot = shoot(event.point, event.intersections, true)!;
|
const shot = shoot(event.point, event.intersections, true)!;
|
||||||
|
|
||||||
tankopediaEphemeralStore.setState({ shot });
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
|
draft.shot = shot;
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
||||||
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
||||||
const canvas = useThree((state) => state.gl.domElement);
|
const canvas = useThree((state) => state.gl.domElement);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const maskOrigin =
|
const maskOrigin =
|
||||||
gunModelDefinition.mask === undefined
|
gunModelDefinition.mask === undefined
|
||||||
@@ -155,10 +154,10 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
@@ -194,7 +193,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
@@ -245,10 +244,10 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
@@ -287,7 +286,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
@@ -327,10 +326,10 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
@@ -368,7 +367,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ export function StaticArmorSceneComponent({
|
|||||||
onPointerDown,
|
onPointerDown,
|
||||||
...props
|
...props
|
||||||
}: StaticArmorSceneComponentProps) {
|
}: StaticArmorSceneComponentProps) {
|
||||||
const mutateTankopediaEphemeralStore = TankopediaEphemeral.useMutation();
|
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const x = thickness / thicknessRange.value;
|
const x = thickness / thicknessRange.value;
|
||||||
const xClamped = clamp(x, 0, 1);
|
const xClamped = clamp(x, 0, 1);
|
||||||
@@ -166,7 +164,7 @@ export function StaticArmorSceneComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
tankopediaEphemeralStore.subscribe(
|
TankopediaEphemeral.on(
|
||||||
(state) => state.highlightArmor?.name,
|
(state) => state.highlightArmor?.name,
|
||||||
handleHighlightArmor,
|
handleHighlightArmor,
|
||||||
),
|
),
|
||||||
@@ -238,7 +236,7 @@ export function StaticArmorSceneComponent({
|
|||||||
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const { editStatic } = tankopediaEphemeralStore.getState();
|
const { editStatic } = TankopediaEphemeral.state;
|
||||||
|
|
||||||
const bounds = new Box3().setFromObject(event.object);
|
const bounds = new Box3().setFromObject(event.object);
|
||||||
const point = bounds.min
|
const point = bounds.min
|
||||||
@@ -259,7 +257,7 @@ export function StaticArmorSceneComponent({
|
|||||||
const thicknessAngled =
|
const thicknessAngled =
|
||||||
thickness / Math.sin(Math.PI / 2 - angle);
|
thickness / Math.sin(Math.PI / 2 - angle);
|
||||||
|
|
||||||
mutateTankopediaEphemeralStore((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.highlightArmor = {
|
draft.highlightArmor = {
|
||||||
type: props.type,
|
type: props.type,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -18,13 +18,12 @@ export function GuessBackground() {
|
|||||||
const guessState = GuessEphemeral.use((state) => state.guessState);
|
const guessState = GuessEphemeral.use((state) => state.guessState);
|
||||||
const tank = GuessEphemeral.use((state) => state.tank);
|
const tank = GuessEphemeral.use((state) => state.tank);
|
||||||
const mutateDuel = Duel.useMutation();
|
const mutateDuel = Duel.useMutation();
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mutateDuel((state) => {
|
mutateDuel((state) => {
|
||||||
state.protagonist = tankToDuelMember(tank, provisionDefinitions);
|
state.protagonist = tankToDuelMember(tank, provisionDefinitions);
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((state) => {
|
TankopediaEphemeral.mutate((state) => {
|
||||||
state.model = modelDefinitions.models[tank.id];
|
state.model = modelDefinitions.models[tank.id];
|
||||||
});
|
});
|
||||||
}, [tank]);
|
}, [tank]);
|
||||||
|
|||||||
+1
-2
@@ -13,7 +13,6 @@ export function CompareOptions(props: FlexProps) {
|
|||||||
(state) => state.relativeAgainst,
|
(state) => state.relativeAgainst,
|
||||||
);
|
);
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,7 +32,7 @@ export function CompareOptions(props: FlexProps) {
|
|||||||
size="1"
|
size="1"
|
||||||
value={`${relativeAgainst}`}
|
value={`${relativeAgainst}`}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.relativeAgainst = Number(value) as TankopediaRelativeAgainst;
|
draft.relativeAgainst = Number(value) as TankopediaRelativeAgainst;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
+2
-3
@@ -10,7 +10,6 @@ export function StatSearch(props: FlexProps) {
|
|||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const count = useRef<HTMLDivElement>(null);
|
const count = useRef<HTMLDivElement>(null);
|
||||||
const [index, setIndex] = useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
const search = useCallback(() => {
|
const search = useCallback(() => {
|
||||||
@@ -34,14 +33,14 @@ export function StatSearch(props: FlexProps) {
|
|||||||
count.current.textContent = '';
|
count.current.textContent = '';
|
||||||
|
|
||||||
setSearching(false);
|
setSearching(false);
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.statSearch = undefined;
|
draft.statSearch = undefined;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSearching(false);
|
setSearching(false);
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.statSearch = input.current!.value;
|
draft.statSearch = input.current!.value;
|
||||||
});
|
});
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|||||||
+3
-4
@@ -14,7 +14,6 @@ import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
|||||||
|
|
||||||
export function Skills() {
|
export function Skills() {
|
||||||
const skillLevels = TankopediaEphemeral.use((state) => state.skills);
|
const skillLevels = TankopediaEphemeral.use((state) => state.skills);
|
||||||
const mutateTankopediaTemporary = TankopediaEphemeral.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,7 +45,7 @@ export function Skills() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaTemporary((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
Object.keys(draft.skills).forEach((skill) => {
|
Object.keys(draft.skills).forEach((skill) => {
|
||||||
draft.skills[skill] = 0;
|
draft.skills[skill] = 0;
|
||||||
});
|
});
|
||||||
@@ -58,7 +57,7 @@ export function Skills() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaTemporary((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
Object.keys(draft.skills).forEach((skill) => {
|
Object.keys(draft.skills).forEach((skill) => {
|
||||||
draft.skills[skill] = 7;
|
draft.skills[skill] = 7;
|
||||||
});
|
});
|
||||||
@@ -72,7 +71,7 @@ export function Skills() {
|
|||||||
<CrewSkillManager
|
<CrewSkillManager
|
||||||
skillLevels={skillLevels}
|
skillLevels={skillLevels}
|
||||||
onChange={(skills) => {
|
onChange={(skills) => {
|
||||||
mutateTankopediaTemporary((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.skills = skills;
|
draft.skills = skills;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
+8
-9
@@ -20,7 +20,6 @@ export function CustomShellButton() {
|
|||||||
const customShell = TankopediaEphemeral.use((state) => state.customShell);
|
const customShell = TankopediaEphemeral.use((state) => state.customShell);
|
||||||
const gun = Duel.use((state) => state.protagonist.gun);
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
const antagonistShell = Duel.use((state) => state.antagonist.shell);
|
const antagonistShell = Duel.use((state) => state.antagonist.shell);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -28,7 +27,7 @@ export function CustomShellButton() {
|
|||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open || customShell) return;
|
if (!open || customShell) return;
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.customShell = antagonistShell;
|
draft.customShell = antagonistShell;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -97,7 +96,7 @@ export function CustomShellButton() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.customShell = modified;
|
draft.customShell = modified;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -134,7 +133,7 @@ export function CustomShellButton() {
|
|||||||
defaultValue={customShell.caliber}
|
defaultValue={customShell.caliber}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.customShell!.caliber = event.target.valueAsNumber;
|
draft.customShell!.caliber = event.target.valueAsNumber;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -170,7 +169,7 @@ export function CustomShellButton() {
|
|||||||
defaultValue={customShell.penetration.near}
|
defaultValue={customShell.penetration.near}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.customShell!.penetration.near =
|
draft.customShell!.penetration.near =
|
||||||
event.target.valueAsNumber;
|
event.target.valueAsNumber;
|
||||||
});
|
});
|
||||||
@@ -226,7 +225,7 @@ export function CustomShellButton() {
|
|||||||
onValueChange={([value]) => {
|
onValueChange={([value]) => {
|
||||||
console.log('asd');
|
console.log('asd');
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.customShell!.normalization = value;
|
draft.customShell!.normalization = value;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -255,7 +254,7 @@ export function CustomShellButton() {
|
|||||||
step={Number.EPSILON}
|
step={Number.EPSILON}
|
||||||
defaultValue={[customShell.ricochet ?? 90]}
|
defaultValue={[customShell.ricochet ?? 90]}
|
||||||
onValueChange={throttle(([value]) => {
|
onValueChange={throttle(([value]) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.customShell!.ricochet = value;
|
draft.customShell!.ricochet = value;
|
||||||
});
|
});
|
||||||
}, 1000)}
|
}, 1000)}
|
||||||
@@ -274,7 +273,7 @@ export function CustomShellButton() {
|
|||||||
defaultValue={customShell.explosion_radius ?? 0}
|
defaultValue={customShell.explosion_radius ?? 0}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.customShell!.explosion_radius =
|
draft.customShell!.explosion_radius =
|
||||||
event.target.valueAsNumber;
|
event.target.valueAsNumber;
|
||||||
});
|
});
|
||||||
@@ -310,7 +309,7 @@ export function CustomShellButton() {
|
|||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.customShell = antagonistShell;
|
draft.customShell = antagonistShell;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
+1
-2
@@ -7,7 +7,6 @@ import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemera
|
|||||||
export function DynamicArmorSwitcher() {
|
export function DynamicArmorSwitcher() {
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const mutateDuel = Duel.useMutation();
|
const mutateDuel = Duel.useMutation();
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const isDynamicArmorActive = Duel.use((state) =>
|
const isDynamicArmorActive = Duel.use((state) =>
|
||||||
state.protagonist.consumables.includes(73),
|
state.protagonist.consumables.includes(73),
|
||||||
);
|
);
|
||||||
@@ -33,7 +32,7 @@ export function DynamicArmorSwitcher() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
+6
-8
@@ -26,9 +26,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
(state) => state.showPrimaryArmor,
|
(state) => state.showPrimaryArmor,
|
||||||
);
|
);
|
||||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const editStatic = TankopediaEphemeral.use((state) => state.editStatic);
|
const editStatic = TankopediaEphemeral.use((state) => state.editStatic);
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -55,7 +53,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.showPrimaryArmor = !draft.showPrimaryArmor;
|
draft.showPrimaryArmor = !draft.showPrimaryArmor;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -96,7 +94,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.showSpacedArmor = !draft.showSpacedArmor;
|
draft.showSpacedArmor = !draft.showSpacedArmor;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -135,7 +133,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.showExternalModules = !draft.showExternalModules;
|
draft.showExternalModules = !draft.showExternalModules;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -168,7 +166,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
gap="2"
|
gap="2"
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.editStatic = !draft.editStatic;
|
draft.editStatic = !draft.editStatic;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
@@ -186,9 +184,9 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
size="1"
|
size="1"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.editStatic = false;
|
draft.editStatic = false;
|
||||||
draft.model = tankopediaEphemeralStore.getInitialState().model;
|
draft.model = TankopediaEphemeral.initial.model;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
const hasEnhancedArmor = useEquipment(110);
|
const hasEnhancedArmor = useEquipment(110);
|
||||||
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
|
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
|
||||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const { strings, unwrap } = useLocale();
|
const { strings, unwrap } = useLocale();
|
||||||
const revealed = TankopediaEphemeral.use((state) => state.revealed);
|
const revealed = TankopediaEphemeral.use((state) => state.revealed);
|
||||||
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||||
@@ -196,7 +195,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
mutateDuel((draft) => {
|
mutateDuel((draft) => {
|
||||||
draft.antagonist.shell = thisShell;
|
draft.antagonist.shell = thisShell;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.customShell = undefined;
|
draft.customShell = undefined;
|
||||||
});
|
});
|
||||||
@@ -235,7 +234,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
? 0
|
? 0
|
||||||
: 1;
|
: 1;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -260,7 +259,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
? 0
|
? 0
|
||||||
: -1;
|
: -1;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -367,7 +366,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
variant="classic"
|
variant="classic"
|
||||||
value={`${disturbed ? display : -1}`}
|
value={`${disturbed ? display : -1}`}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.disturbed = true;
|
draft.disturbed = true;
|
||||||
draft.display = Number(value);
|
draft.display = Number(value);
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-2
@@ -14,14 +14,13 @@ export function Title() {
|
|||||||
? '2rem'
|
? '2rem'
|
||||||
: `min(16vh, ${125 / name.length}vw)`
|
: `min(16vh, ${125 / name.length}vw)`
|
||||||
: `min(12vh, ${75 / name.length}vw)`;
|
: `min(12vh, ${75 / name.length}vw)`;
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.revealed = true;
|
draft.revealed = true;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
+3
-5
@@ -32,9 +32,7 @@ export function Controls({
|
|||||||
zoomable = true,
|
zoomable = true,
|
||||||
enableRotate = true,
|
enableRotate = true,
|
||||||
}: ControlsProps) {
|
}: ControlsProps) {
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const camera = useThree((state) => state.camera as PerspectiveCamera);
|
const camera = useThree((state) => state.camera as PerspectiveCamera);
|
||||||
const canvas = useThree((state) => state.gl.domElement);
|
const canvas = useThree((state) => state.gl.domElement);
|
||||||
const orbitControls = useRef<OrbitControlsClass>(null);
|
const orbitControls = useRef<OrbitControlsClass>(null);
|
||||||
@@ -102,7 +100,7 @@ export function Controls({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
|
const unsubscribeTankopediaEphemeral = TankopediaEphemeral.on(
|
||||||
(state) => state.controlsEnabled,
|
(state) => state.controlsEnabled,
|
||||||
(enabled) => {
|
(enabled) => {
|
||||||
if (orbitControls.current) orbitControls.current.enabled = enabled;
|
if (orbitControls.current) orbitControls.current.enabled = enabled;
|
||||||
@@ -222,7 +220,7 @@ export function Controls({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function disturb() {
|
function disturb() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.disturbed = true;
|
draft.disturbed = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -268,7 +266,7 @@ export function Controls({
|
|||||||
enableZoom={zoomable}
|
enableZoom={zoomable}
|
||||||
zoomSpeed={zoomable ? undefined : 0}
|
zoomSpeed={zoomable ? undefined : 0}
|
||||||
ref={orbitControls}
|
ref={orbitControls}
|
||||||
enabled={tankopediaEphemeralStore.getState().controlsEnabled}
|
enabled={TankopediaEphemeral.state.controlsEnabled}
|
||||||
enableDamping={false}
|
enableDamping={false}
|
||||||
autoRotateSpeed={1 / 4}
|
autoRotateSpeed={1 / 4}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+1
-2
@@ -6,10 +6,9 @@ import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemera
|
|||||||
|
|
||||||
export function InitialFogReveal() {
|
export function InitialFogReveal() {
|
||||||
const fog = useThree((state) => state.scene.fog as Fog | null);
|
const fog = useThree((state) => state.scene.fog as Fog | null);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.revealed = true;
|
draft.revealed = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+9
-12
@@ -16,14 +16,12 @@ import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapp
|
|||||||
export function TankModel() {
|
export function TankModel() {
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
const protagonist = Duel.use((draft) => draft.protagonist);
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const track = Duel.use((state) => state.protagonist.track);
|
const track = Duel.use((state) => state.protagonist.track);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
const canvas = useThree((state) => state.gl.domElement);
|
const canvas = useThree((state) => state.gl.domElement);
|
||||||
const hullContainer = useRef<Group>(null);
|
const hullContainer = useRef<Group>(null);
|
||||||
const turretContainer = useRef<Group>(null);
|
const turretContainer = useRef<Group>(null);
|
||||||
const gunContainer = useRef<Group>(null);
|
const gunContainer = useRef<Group>(null);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const turretModelDefinition =
|
const turretModelDefinition =
|
||||||
tankModelDefinition.turrets[protagonist.turret.id];
|
tankModelDefinition.turrets[protagonist.turret.id];
|
||||||
@@ -63,13 +61,12 @@ export function TankModel() {
|
|||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
if (
|
if (
|
||||||
isTrack &&
|
isTrack &&
|
||||||
tankopediaEphemeralStore.getState().display ===
|
TankopediaEphemeral.state.display === TankopediaDisplay.Model
|
||||||
TankopediaDisplay.Model
|
|
||||||
) {
|
) {
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,7 +85,7 @@ export function TankModel() {
|
|||||||
translateTexture(deltaX + deltaY);
|
translateTexture(deltaX + deltaY);
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,10 +127,10 @@ export function TankModel() {
|
|||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
@@ -169,7 +166,7 @@ export function TankModel() {
|
|||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
@@ -213,10 +210,10 @@ export function TankModel() {
|
|||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
@@ -255,7 +252,7 @@ export function TankModel() {
|
|||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
|
|||||||
+1
-2
@@ -22,7 +22,6 @@ const skeletonMaterial = new ShaderMaterial({
|
|||||||
|
|
||||||
export function TransitionSkeleton() {
|
export function TransitionSkeleton() {
|
||||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
const protagonist = Duel.use((draft) => draft.protagonist);
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const track = Duel.use((state) => state.protagonist.track);
|
const track = Duel.use((state) => state.protagonist.track);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
const hullContainer = useRef<Group>(null);
|
const hullContainer = useRef<Group>(null);
|
||||||
@@ -34,7 +33,7 @@ export function TransitionSkeleton() {
|
|||||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||||
const { gltf } = useModel(protagonist.tank.id);
|
const { gltf } = useModel(protagonist.tank.id);
|
||||||
const nodes = Object.values(gltf.nodes);
|
const nodes = Object.values(gltf.nodes);
|
||||||
const revealed = tankopediaEphemeralStore((state) => state.revealed);
|
const revealed = TankopediaEphemeral.use((state) => state.revealed);
|
||||||
|
|
||||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -49,7 +49,6 @@ export const fogAnimationTime = 2.5;
|
|||||||
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||||
({ thicknessRange, naked }, ref) => {
|
({ thicknessRange, naked }, ref) => {
|
||||||
const fog = useRef(new Fog('black', forNear1, fogFar1));
|
const fog = useRef(new Fog('black', forNear1, fogFar1));
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const canvas = useRef<HTMLCanvasElement>(null);
|
const canvas = useRef<HTMLCanvasElement>(null);
|
||||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||||
@@ -177,13 +176,13 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (display !== TankopediaDisplay.DynamicArmor) {
|
if (display !== TankopediaDisplay.DynamicArmor) {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (display !== TankopediaDisplay.StaticArmor) {
|
if (display !== TankopediaDisplay.StaticArmor) {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -201,7 +200,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
|||||||
shadows="soft"
|
shadows="soft"
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
onPointerMissed={() => {
|
onPointerMissed={() => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
}, [protagonist]);
|
}, [protagonist]);
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const mutateDuel = Duel.useMutation();
|
const mutateDuel = Duel.useMutation();
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disturbed) {
|
if (disturbed) {
|
||||||
@@ -55,7 +54,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
mutateDuel((draft) => {
|
mutateDuel((draft) => {
|
||||||
draft.antagonist.shell = shells[index];
|
draft.antagonist.shell = shells[index];
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.customShell = undefined;
|
draft.customShell = undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
const { locale, strings } = useLocale();
|
const { locale, strings } = useLocale();
|
||||||
const master = Duel.use((state) => state.protagonist.tank);
|
const master = Duel.use((state) => state.protagonist.tank);
|
||||||
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier);
|
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
const lines = useMemo(() => {
|
const lines = useMemo(() => {
|
||||||
function extend(line: Line): Line[] {
|
function extend(line: Line): Line[] {
|
||||||
@@ -148,7 +147,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
TankopediaEphemeral.mutate((draft) => {
|
||||||
draft.xpMultiplier = multiplier;
|
draft.xpMultiplier = multiplier;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -43,11 +43,12 @@ function Container() {
|
|||||||
const model = modelDefinitions.models[tank.id];
|
const model = modelDefinitions.models[tank.id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TankopediaEphemeral.Provider data={model}>
|
<>
|
||||||
|
<TankopediaEphemeral.Provider args={model} />
|
||||||
<Duel.Provider data={{ tank, model, provisionDefinitions }}>
|
<Duel.Provider data={{ tank, model, provisionDefinitions }}>
|
||||||
<Content />
|
<Content />
|
||||||
</Duel.Provider>
|
</Duel.Provider>
|
||||||
</TankopediaEphemeral.Provider>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,28 +41,27 @@ export function Page({ id, skeleton, locale, guide }: PageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LocaleProvider locale={locale}>
|
<LocaleProvider locale={locale}>
|
||||||
<TankopediaEphemeral.Provider data={model}>
|
<TankopediaEphemeral.Provider args={model} />
|
||||||
<App.Provider>
|
<App.Provider>
|
||||||
<TankopediaPersistent.Provider>
|
<TankopediaPersistent.Provider>
|
||||||
<Duel.Provider
|
<Duel.Provider
|
||||||
data={{ tank, model, provisionDefinitions: provisionDefinitions }}
|
data={{ tank, model, provisionDefinitions: provisionDefinitions }}
|
||||||
>
|
>
|
||||||
<PageWrapper p="0" maxWidth="unset" color="purple" gap="9" pb="9">
|
<PageWrapper p="0" maxWidth="unset" color="purple" gap="9" pb="9">
|
||||||
<HeroSection skeleton={skeleton} />
|
<HeroSection skeleton={skeleton} />
|
||||||
<MetaSection />
|
<MetaSection />
|
||||||
<CalloutsSection />
|
<CalloutsSection />
|
||||||
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
|
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
|
||||||
<TechTreeSection skeleton={skeleton} />
|
<TechTreeSection skeleton={skeleton} />
|
||||||
)}
|
)}
|
||||||
<CharacteristicsSection skeleton={skeleton} />
|
<CharacteristicsSection skeleton={skeleton} />
|
||||||
<GameModeSection />
|
<GameModeSection />
|
||||||
{guide && <GuideSection guide={guide} />}
|
{guide && <GuideSection guide={guide} />}
|
||||||
<VideoSection skeleton={skeleton} />
|
<VideoSection skeleton={skeleton} />
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
</Duel.Provider>
|
</Duel.Provider>
|
||||||
</TankopediaPersistent.Provider>
|
</TankopediaPersistent.Provider>
|
||||||
</App.Provider>
|
</App.Provider>
|
||||||
</TankopediaEphemeral.Provider>
|
|
||||||
</LocaleProvider>
|
</LocaleProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -28,9 +28,8 @@ export function Page({ id }: PageProps) {
|
|||||||
<App.Provider>
|
<App.Provider>
|
||||||
<TankopediaPersistent.Provider>
|
<TankopediaPersistent.Provider>
|
||||||
<Duel.Provider data={{ provisionDefinitions, model, tank }}>
|
<Duel.Provider data={{ provisionDefinitions, model, tank }}>
|
||||||
<TankopediaEphemeral.Provider data={model}>
|
<TankopediaEphemeral.Provider args={model} />
|
||||||
<Content />
|
<Content />
|
||||||
</TankopediaEphemeral.Provider>
|
|
||||||
</Duel.Provider>
|
</Duel.Provider>
|
||||||
</TankopediaPersistent.Provider>
|
</TankopediaPersistent.Provider>
|
||||||
</App.Provider>
|
</App.Provider>
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import {
|
|||||||
type ModelDefinition,
|
type ModelDefinition,
|
||||||
} from '@blitzkit/core';
|
} from '@blitzkit/core';
|
||||||
import type { Vector3 } from 'three';
|
import type { Vector3 } from 'three';
|
||||||
import { create } from 'zustand';
|
import { Varuna } from 'varuna';
|
||||||
import { subscribeWithSelector } from 'zustand/middleware';
|
|
||||||
import type { ArmorType } from '../components/Armor/components/SpacedArmorScene';
|
import type { ArmorType } from '../components/Armor/components/SpacedArmorScene';
|
||||||
import type { ExternalModuleVariant } from '../components/Armor/components/SpacedArmorSceneComponent';
|
import type { ExternalModuleVariant } from '../components/Armor/components/SpacedArmorSceneComponent';
|
||||||
import type { XP_MULTIPLIERS } from '../components/Tankopedia/TechTreeSection';
|
import type { XP_MULTIPLIERS } from '../components/Tankopedia/TechTreeSection';
|
||||||
import { awaitableSkillDefinitions } from '../core/awaitables/skillDefinitions';
|
import { awaitableSkillDefinitions } from '../core/awaitables/skillDefinitions';
|
||||||
import { createContextualStore } from '../core/zustand/createContextualStore';
|
|
||||||
import { TankopediaDisplay } from './tankopediaPersistent/constants';
|
import { TankopediaDisplay } from './tankopediaPersistent/constants';
|
||||||
|
|
||||||
export interface ShotLayerBase {
|
export interface ShotLayerBase {
|
||||||
@@ -109,20 +107,17 @@ interface TankopediaEphemeral {
|
|||||||
|
|
||||||
const skillDefinitions = await awaitableSkillDefinitions;
|
const skillDefinitions = await awaitableSkillDefinitions;
|
||||||
|
|
||||||
export const TankopediaEphemeral = createContextualStore(
|
export const TankopediaEphemeral = new Varuna<
|
||||||
(model: ModelDefinition) => {
|
TankopediaEphemeral,
|
||||||
return create<TankopediaEphemeral>()(
|
ModelDefinition
|
||||||
subscribeWithSelector<TankopediaEphemeral>(() => ({
|
>((model) => ({
|
||||||
disturbed: false,
|
disturbed: false,
|
||||||
revealed: false,
|
revealed: false,
|
||||||
relativeAgainst: TankopediaRelativeAgainst.Class,
|
relativeAgainst: TankopediaRelativeAgainst.Class,
|
||||||
editStatic: false,
|
editStatic: false,
|
||||||
skills: createDefaultSkills(skillDefinitions),
|
skills: createDefaultSkills(skillDefinitions),
|
||||||
model,
|
model,
|
||||||
controlsEnabled: true,
|
controlsEnabled: true,
|
||||||
xpMultiplier: 1,
|
xpMultiplier: 1,
|
||||||
display: TankopediaDisplay.Model,
|
display: TankopediaDisplay.Model,
|
||||||
})),
|
}));
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Reference in new issue
Block a user