mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-04 00:10:38 +01:00
Fix: Unify Tankopedia model, camouflage and attachment loaders
- Tankopedia state uses `loaders: { model, skin, attachment }` with `requested` and `loaded`
- useSkin swaps the camouflage halfway through the lighting transition for both cached and downloaded skins
- Remove the leftover "Unreleased" camouflage name handling
This commit is contained in:
1 parent
523fee5010
commit
c772d47a07
8 files changed
+74
-83
No files matched your search
+3
-1
@@ -44,7 +44,9 @@ export function GunFlexibilityVisualizer({
|
|||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
const gun = Duel.use((state) => state.protagonist.gun);
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
const modelRequested = Tankopedia.use(
|
||||||
|
(state) => state.loaders.model.requested,
|
||||||
|
);
|
||||||
|
|
||||||
const tankModel = modelDefinition.models[tank.id];
|
const tankModel = modelDefinition.models[tank.id];
|
||||||
const turretModel = tankModel.turrets[turret.id];
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
|
|||||||
+19
-27
@@ -1,5 +1,5 @@
|
|||||||
import { alias } from "@blitzkit/core";
|
import { alias } from "@blitzkit/core";
|
||||||
import { Flex, Text, Tooltip } from "@radix-ui/themes";
|
import { Flex, Tooltip } from "@radix-ui/themes";
|
||||||
import { uniqBy } from "lodash-es";
|
import { uniqBy } from "lodash-es";
|
||||||
import { SKIN_NUMBER } from "../../../../../../constants/skinNumber";
|
import { SKIN_NUMBER } from "../../../../../../constants/skinNumber";
|
||||||
import { api } from "../../../../../../core/blitzkit/api";
|
import { api } from "../../../../../../core/blitzkit/api";
|
||||||
@@ -13,17 +13,18 @@ const camouflageDefinitions = await api.camouflageDefinitions();
|
|||||||
|
|
||||||
export function SkinSwitcher() {
|
export function SkinSwitcher() {
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const skin = Tankopedia.use((state) => state.requestedSkin);
|
const skin = Tankopedia.use((state) => state.loaders.skin.requested);
|
||||||
const attachment = Tankopedia.use((state) => state.requestedAttachment);
|
const attachment = Tankopedia.use(
|
||||||
|
(state) => state.loaders.attachment.requested,
|
||||||
|
);
|
||||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||||
const { unwrap, strings } = useLocale();
|
const { unwrap } = useLocale();
|
||||||
const skins = uniqBy(
|
const skins = uniqBy(
|
||||||
Object.values(camouflageDefinitions.camouflages).filter(
|
Object.values(camouflageDefinitions.camouflages).filter(
|
||||||
(camouflage) => camouflage.tank_id === tank.id,
|
(camouflage) => camouflage.tank_id === tank.id,
|
||||||
),
|
),
|
||||||
(camouflage) =>
|
(camouflage) => unwrap(camouflage.name!),
|
||||||
(camouflage.name && unwrap(camouflage.name)) || camouflage.id,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// attachments aren't camouflages so they get their own group
|
// attachments aren't camouflages so they get their own group
|
||||||
@@ -54,26 +55,16 @@ export function SkinSwitcher() {
|
|||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<Flex key={group[0].id} direction="column">
|
<Flex key={group[0].id} direction="column">
|
||||||
{group.map((camouflage, index) => {
|
{group.map((camouflage, index) => {
|
||||||
const rawName = camouflage.name && unwrap(camouflage.name);
|
const name = unwrap(camouflage.name!).replace(
|
||||||
const name = rawName
|
"%(camo_num)",
|
||||||
? rawName.replace("%(camo_num)", SKIN_NUMBER)
|
SKIN_NUMBER,
|
||||||
: strings.website.tools.tankopedia.sandbox.skins.unreleased;
|
);
|
||||||
const top = index === 0 ? "var(--radius-full)" : "0";
|
const top = index === 0 ? "var(--radius-full)" : "0";
|
||||||
const bottom =
|
const bottom =
|
||||||
index === group.length - 1 ? "var(--radius-full)" : "0";
|
index === group.length - 1 ? "var(--radius-full)" : "0";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip key={camouflage.id} content={name} side="left">
|
||||||
key={camouflage.id}
|
|
||||||
content={
|
|
||||||
rawName ? (
|
|
||||||
name
|
|
||||||
) : (
|
|
||||||
<Text style={{ color: "var(--orange-9)" }}>{name}</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
side="left"
|
|
||||||
>
|
|
||||||
<SidebarIconButton
|
<SidebarIconButton
|
||||||
selected={
|
selected={
|
||||||
(camouflage.attachment ? attachment : skin) ===
|
(camouflage.attachment ? attachment : skin) ===
|
||||||
@@ -92,13 +83,14 @@ export function SkinSwitcher() {
|
|||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const slot = camouflage.attachment
|
|
||||||
? "requestedAttachment"
|
|
||||||
: "requestedSkin";
|
|
||||||
|
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft[slot] =
|
const loader =
|
||||||
draft[slot] === camouflage.id
|
draft.loaders[
|
||||||
|
camouflage.attachment ? "attachment" : "skin"
|
||||||
|
];
|
||||||
|
|
||||||
|
loader.requested =
|
||||||
|
loader.requested === camouflage.id
|
||||||
? undefined
|
? undefined
|
||||||
: camouflage.id;
|
: camouflage.id;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
const revealed = Tankopedia.use((state) => state.revealed);
|
const revealed = Tankopedia.use((state) => state.revealed);
|
||||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||||
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
||||||
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
const modelRequested = Tankopedia.use(
|
||||||
|
(state) => state.loaders.model.requested,
|
||||||
|
);
|
||||||
const equalize = Duel.use((state) => state.equalize);
|
const equalize = Duel.use((state) => state.equalize);
|
||||||
const equalizer = Duel.use((state) => state.antagonist.tank.equalizer);
|
const equalizer = Duel.use((state) => state.antagonist.tank.equalizer);
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -13,12 +13,14 @@ const camouflageDefinitions = await api.camouflageDefinitions();
|
|||||||
|
|
||||||
export function Title({ skeleton }: MaybeSkeletonComponentProps) {
|
export function Title({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const { unwrap, strings } = useLocale();
|
const { unwrap, strings } = useLocale();
|
||||||
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
const modelRequested = Tankopedia.use(
|
||||||
|
(state) => state.loaders.model.requested,
|
||||||
|
);
|
||||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||||
const revealed = Tankopedia.use((state) => state.revealed);
|
const revealed = Tankopedia.use((state) => state.revealed);
|
||||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||||
const hasRevealed = useRef(false);
|
const hasRevealed = useRef(false);
|
||||||
const skin = Tankopedia.use((state) => state.requestedSkin);
|
const skin = Tankopedia.use((state) => state.loaders.skin.requested);
|
||||||
const camouflage =
|
const camouflage =
|
||||||
skin === undefined ? undefined : camouflageDefinitions.camouflages[skin];
|
skin === undefined ? undefined : camouflageDefinitions.camouflages[skin];
|
||||||
const tankName = unwrap(protagonist.name!);
|
const tankName = unwrap(protagonist.name!);
|
||||||
@@ -27,9 +29,7 @@ export function Title({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
camouflage
|
camouflage
|
||||||
? camouflage.tank_name
|
? camouflage.tank_name
|
||||||
? unwrap(camouflage.tank_name)
|
? unwrap(camouflage.tank_name)
|
||||||
: camouflage.name
|
: `${tankName} ${unwrap(camouflage.name!)}`
|
||||||
? `${tankName} ${unwrap(camouflage.name)}`
|
|
||||||
: tankName
|
|
||||||
: tankName
|
: tankName
|
||||||
).replace("%(camo_num)", SKIN_NUMBER);
|
).replace("%(camo_num)", SKIN_NUMBER);
|
||||||
const fontSize = revealed
|
const fontSize = revealed
|
||||||
@@ -109,7 +109,7 @@ export function Title({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
onPointerDown={(event) => event.stopPropagation()}
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.modelRequested = true;
|
draft.loaders.model.requested = true;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+3
-13
@@ -49,8 +49,8 @@ export function Lighting({ hasPbr }: Props) {
|
|||||||
const revealed = Tankopedia.use((state) => state.revealed);
|
const revealed = Tankopedia.use((state) => state.revealed);
|
||||||
const skinPending = Tankopedia.use(
|
const skinPending = Tankopedia.use(
|
||||||
(state) =>
|
(state) =>
|
||||||
state.loadedSkin !== state.skin ||
|
state.loaders.skin.requested !== state.loaders.skin.loaded ||
|
||||||
state.loadedAttachment !== state.attachment,
|
state.loaders.attachment.requested !== state.loaders.attachment.loaded,
|
||||||
);
|
);
|
||||||
|
|
||||||
const animationTime = useRef(REVEAL_ANIMATION_TIME);
|
const animationTime = useRef(REVEAL_ANIMATION_TIME);
|
||||||
@@ -65,6 +65,7 @@ export function Lighting({ hasPbr }: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.revealed = true;
|
draft.revealed = true;
|
||||||
|
draft.loaders.model.loaded = true;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -246,17 +247,6 @@ function Animator({
|
|||||||
draft.display = requestedDisplay;
|
draft.display = requestedDisplay;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
x >= 1 &&
|
|
||||||
(Tankopedia.state.skin !== Tankopedia.state.loadedSkin ||
|
|
||||||
Tankopedia.state.attachment !== Tankopedia.state.loadedAttachment)
|
|
||||||
) {
|
|
||||||
Tankopedia.mutate((draft) => {
|
|
||||||
draft.skin = draft.loadedSkin;
|
|
||||||
draft.attachment = draft.loadedAttachment;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
const canvas = useRef<HTMLCanvasElement>(null!);
|
const canvas = useRef<HTMLCanvasElement>(null!);
|
||||||
const isFullScreen = useFullScreen();
|
const isFullScreen = useFullScreen();
|
||||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||||
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
const modelRequested = Tankopedia.use(
|
||||||
|
(state) => state.loaders.model.requested,
|
||||||
|
);
|
||||||
const thicknessRange = useMemo(() => {
|
const thicknessRange = useMemo(() => {
|
||||||
const entries = Object.values(tankDefinitions.tanks);
|
const entries = Object.values(tankDefinitions.tanks);
|
||||||
const filtered = entries.filter(
|
const filtered = entries.filter(
|
||||||
|
|||||||
@@ -35,36 +35,28 @@ function skinPath(id: number, skin: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// attachments load the same way as camouflages and can be shown with one
|
// attachments load the same way as camouflages and can be shown with one
|
||||||
const slots = {
|
export function useSkin(id: number, loader: "skin" | "attachment" = "skin") {
|
||||||
skin: { requested: "requestedSkin", loaded: "loadedSkin" },
|
const requested = Tankopedia.use((state) => state.loaders[loader].requested);
|
||||||
attachment: { requested: "requestedAttachment", loaded: "loadedAttachment" },
|
const loaded = Tankopedia.use((state) => state.loaders[loader].loaded);
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function useSkin(id: number, slot: keyof typeof slots = "skin") {
|
|
||||||
const { requested, loaded } = slots[slot];
|
|
||||||
const requestedSkin = Tankopedia.use((state) => state[requested]);
|
|
||||||
const skin = Tankopedia.use((state) => state[slot]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const path =
|
if (requested === Tankopedia.state.loaders[loader].loaded) return;
|
||||||
requestedSkin === undefined ? undefined : skinPath(id, requestedSkin);
|
|
||||||
|
const path = requested === undefined ? undefined : skinPath(id, requested);
|
||||||
|
const cached = path === undefined || loadedSkins.has(path);
|
||||||
let settled = false;
|
let settled = false;
|
||||||
|
|
||||||
if (path === undefined || loadedSkins.has(path)) {
|
if (!cached) {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft[loaded] = requestedSkin;
|
draft.revealed = false;
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
|
if (!skins.has(path)) skins.set(path, loadSkin(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
Tankopedia.mutate((draft) => {
|
// swaps halfway through the lighting transition
|
||||||
draft.revealed = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!skins.has(path)) skins.set(path, loadSkin(path));
|
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
skins.get(path),
|
path && skins.get(path),
|
||||||
new Promise((resolve) =>
|
new Promise((resolve) =>
|
||||||
setTimeout(resolve, TRANSITION_ANIMATION_TIME * 1e3),
|
setTimeout(resolve, TRANSITION_ANIMATION_TIME * 1e3),
|
||||||
),
|
),
|
||||||
@@ -73,9 +65,8 @@ export function useSkin(id: number, slot: keyof typeof slots = "skin") {
|
|||||||
|
|
||||||
settled = true;
|
settled = true;
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft[loaded] = requestedSkin;
|
draft.loaders[loader].loaded = requested;
|
||||||
draft[slot] = requestedSkin;
|
if (!cached) draft.revealed = true;
|
||||||
draft.revealed = true;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,11 +74,16 @@ export function useSkin(id: number, slot: keyof typeof slots = "skin") {
|
|||||||
if (settled) return;
|
if (settled) return;
|
||||||
|
|
||||||
settled = true;
|
settled = true;
|
||||||
|
|
||||||
|
if (cached) return;
|
||||||
|
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.revealed = true;
|
draft.revealed = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}, [id, slot, requestedSkin]);
|
}, [id, loader, requested]);
|
||||||
|
|
||||||
return skin === undefined ? undefined : loadedSkins.get(skinPath(id, skin));
|
return loaded === undefined
|
||||||
|
? undefined
|
||||||
|
: loadedSkins.get(skinPath(id, loaded));
|
||||||
}
|
}
|
||||||
@@ -78,10 +78,19 @@ export enum TankopediaRelativeAgainst {
|
|||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LoaderState<Type> {
|
||||||
|
requested: Type;
|
||||||
|
loaded: Type;
|
||||||
|
}
|
||||||
|
|
||||||
interface Tankopedia {
|
interface Tankopedia {
|
||||||
disturbed: boolean;
|
disturbed: boolean;
|
||||||
revealed: boolean;
|
revealed: boolean;
|
||||||
modelRequested: boolean;
|
loaders: {
|
||||||
|
model: LoaderState<boolean>;
|
||||||
|
skin: LoaderState<number | undefined>;
|
||||||
|
attachment: LoaderState<number | undefined>;
|
||||||
|
};
|
||||||
shot?: Shot;
|
shot?: Shot;
|
||||||
skills: Record<string, number>;
|
skills: Record<string, number>;
|
||||||
relativeAgainst: TankopediaRelativeAgainst;
|
relativeAgainst: TankopediaRelativeAgainst;
|
||||||
@@ -103,12 +112,6 @@ interface Tankopedia {
|
|||||||
);
|
);
|
||||||
xpMultiplier: (typeof XP_MULTIPLIERS)[number];
|
xpMultiplier: (typeof XP_MULTIPLIERS)[number];
|
||||||
customShell?: ShellDefinition;
|
customShell?: ShellDefinition;
|
||||||
requestedSkin?: number;
|
|
||||||
loadedSkin?: number;
|
|
||||||
skin?: number;
|
|
||||||
requestedAttachment?: number;
|
|
||||||
loadedAttachment?: number;
|
|
||||||
attachment?: number;
|
|
||||||
requestedDisplay: TankopediaDisplay;
|
requestedDisplay: TankopediaDisplay;
|
||||||
display: TankopediaDisplay;
|
display: TankopediaDisplay;
|
||||||
statSearch?: string;
|
statSearch?: string;
|
||||||
@@ -119,7 +122,11 @@ const skillDefinitions = await api.skillDefinitions();
|
|||||||
export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
|
export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
|
||||||
disturbed: false,
|
disturbed: false,
|
||||||
revealed: false,
|
revealed: false,
|
||||||
modelRequested: App.state.autoLoadModels,
|
loaders: {
|
||||||
|
model: { requested: App.state.autoLoadModels, loaded: false },
|
||||||
|
skin: { requested: undefined, loaded: undefined },
|
||||||
|
attachment: { requested: undefined, loaded: undefined },
|
||||||
|
},
|
||||||
relativeAgainst: TankopediaRelativeAgainst.Class,
|
relativeAgainst: TankopediaRelativeAgainst.Class,
|
||||||
editStatic: false,
|
editStatic: false,
|
||||||
skills: createDefaultSkills(skillDefinitions),
|
skills: createDefaultSkills(skillDefinitions),
|
||||||
|
|||||||
Reference in new issue
Block a user