Add imported skins to dropdown and auto-select

This commit is contained in:
Brian Beck 2025-01-28 22:41:05 -08:00
parent 997f9226f8
commit 044d9886fb
13 changed files with 151 additions and 26 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
self.__BUILD_MANIFEST=function(s){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[s,"static/chunks/e21e5bbe-b28e0079b469d4e8.js","static/chunks/ebc70433-4eccd1cb3af29a3e.js","static/chunks/6eb5140f-31a2b2da7903b885.js","static/chunks/85d7bc83-1ca530d7d3f44153.js","static/chunks/3a17f596-9aeae038dfa51955.js","static/chunks/f580fadb-2911e2fbf64aae5a.js","static/chunks/515-13ff0773d41722ae.js","static/chunks/pages/index-7060ed19c3a48d4d.js"],"/_error":["static/chunks/pages/_error-54b9fcf45cb5bc62.js"],"/gallery":[s,"static/chunks/737a5600-aea383aaa2061cc6.js","static/chunks/918-3c6747f76df39072.js","static/css/922e89893536f2f9.css","static/chunks/pages/gallery-af1406fdc1af13f5.js"],sortedPages:["/","/_app","/_error","/gallery"]}}("static/chunks/cb355538-dbf1c108320fc6a1.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB(); self.__BUILD_MANIFEST=function(s){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[s,"static/chunks/e21e5bbe-b28e0079b469d4e8.js","static/chunks/ebc70433-4eccd1cb3af29a3e.js","static/chunks/6eb5140f-31a2b2da7903b885.js","static/chunks/85d7bc83-1ca530d7d3f44153.js","static/chunks/3a17f596-9aeae038dfa51955.js","static/chunks/f580fadb-2911e2fbf64aae5a.js","static/chunks/515-13ff0773d41722ae.js","static/chunks/pages/index-5b5a7028e5a18507.js"],"/_error":["static/chunks/pages/_error-54b9fcf45cb5bc62.js"],"/gallery":[s,"static/chunks/737a5600-aea383aaa2061cc6.js","static/chunks/918-3c6747f76df39072.js","static/css/922e89893536f2f9.css","static/chunks/pages/gallery-af1406fdc1af13f5.js"],sortedPages:["/","/_app","/_error","/gallery"]}}("static/chunks/cb355538-dbf1c108320fc6a1.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,13 +1,29 @@
import { ReactNode, useEffect, useMemo, useState } from "react"; import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import getConfig from "next/config"; import getConfig from "next/config";
import useSettings from "./useSettings"; import useSettings from "./useSettings";
import { WarriorContext } from "./useWarrior"; import { WarriorContext } from "./useWarrior";
import type { MaterialDefinition } from "./Material"; import type { MaterialDefinition } from "./Material";
import type { Skin } from "./importUtils";
const { publicRuntimeConfig } = getConfig(); const { publicRuntimeConfig } = getConfig();
const { materials, modelDefaults } = publicRuntimeConfig; const { materials, modelDefaults } = publicRuntimeConfig;
const baseSkinPath = `https://exogen.github.io/t2-skins/skins`; const baseSkinPath = `https://exogen.github.io/t2-skins/skins`;
let IMPORTED_SKINS: Map<string, Map<string | null, Skin>> = new Map();
function mergeNewImportedSkins(newSkins: typeof IMPORTED_SKINS) {
const newImportedSkins = new Map(IMPORTED_SKINS.entries());
newSkins.forEach((newSkinsByName, modelName) => {
const skinsByName =
newImportedSkins.get(modelName) ?? new Map<string | null, Skin>();
newSkinsByName.forEach((skin, skinName) => {
skinsByName.set(skinName, skin);
});
newImportedSkins.set(modelName, skinsByName);
});
IMPORTED_SKINS = newImportedSkins;
}
function getFrameNames(frameZeroFile: string, frameCount: number) { function getFrameNames(frameZeroFile: string, frameCount: number) {
if (frameCount < 2) { if (frameCount < 2) {
return [frameZeroFile]; return [frameZeroFile];
@ -36,6 +52,17 @@ export function getSkinImageUrls({
selectedSkinType: string | null; selectedSkinType: string | null;
}): Record<string, string[]> { }): Record<string, string[]> {
const materialDefs = materials[actualModel]; const materialDefs = materials[actualModel];
if (selectedSkin && selectedSkinType === "import") {
const skinsByName = IMPORTED_SKINS.get(actualModel);
if (skinsByName) {
const key = selectedSkin === "__untitled__" ? null : selectedSkin;
const skin = skinsByName.get(key);
if (skin && skin.isComplete) {
return Object.fromEntries(skin.materials);
}
}
return {};
}
switch (selectedModelType) { switch (selectedModelType) {
case "player": case "player":
switch (selectedSkinType) { switch (selectedSkinType) {
@ -146,7 +173,12 @@ export default function WarriorProvider({ children }: { children: ReactNode }) {
actualModel, actualModel,
selectedAnimation selectedAnimation
); );
const [importedSkins, setImportedSkins] = useState(() => new Map()); const [importedSkins, setImportedSkins] = useState(IMPORTED_SKINS);
const addImportedSkins = useCallback((newSkins: typeof IMPORTED_SKINS) => {
mergeNewImportedSkins(newSkins);
setImportedSkins(IMPORTED_SKINS);
}, []);
const [skinImageUrls, setSkinImageUrls] = useState<Record<string, string[]>>( const [skinImageUrls, setSkinImageUrls] = useState<Record<string, string[]>>(
() => () =>
@ -193,7 +225,7 @@ export default function WarriorProvider({ children }: { children: ReactNode }) {
slowModeEnabled, slowModeEnabled,
setSlowModeEnabled, setSlowModeEnabled,
importedSkins, importedSkins,
setImportedSkins, addImportedSkins,
}; };
}, [ }, [
selectedModel, selectedModel,
@ -215,7 +247,7 @@ export default function WarriorProvider({ children }: { children: ReactNode }) {
defaultSkinImageUrls, defaultSkinImageUrls,
slowModeEnabled, slowModeEnabled,
importedSkins, importedSkins,
setImportedSkins, addImportedSkins,
]); ]);
useEffect(() => { useEffect(() => {

View file

@ -4,7 +4,7 @@ import { FaFolderOpen } from "react-icons/fa";
import { BsFillGrid3X3GapFill } from "react-icons/bs"; import { BsFillGrid3X3GapFill } from "react-icons/bs";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import useTools from "./useTools"; import useTools from "./useTools";
import { importMultipleFilesToModels } from "./importUtils"; import { importMultipleFilesToModels, modelToModelType } from "./importUtils";
const { publicRuntimeConfig } = getConfig(); const { publicRuntimeConfig } = getConfig();
const { defaultSkins, modelDefaults /*materials*/ } = publicRuntimeConfig; const { defaultSkins, modelDefaults /*materials*/ } = publicRuntimeConfig;
@ -12,6 +12,8 @@ const { defaultSkins, modelDefaults /*materials*/ } = publicRuntimeConfig;
const baseManifestPath = `https://exogen.github.io/t2-skins`; const baseManifestPath = `https://exogen.github.io/t2-skins`;
const defaultCustomSkins = {}; const defaultCustomSkins = {};
const emptyMap = new Map();
export default function WarriorSelector() { export default function WarriorSelector() {
const { const {
selectedModel, selectedModel,
@ -23,10 +25,9 @@ export default function WarriorSelector() {
setSelectedSkinType, setSelectedSkinType,
actualModel, actualModel,
setSelectedAnimation, setSelectedAnimation,
setSkinImageUrls,
setAnimationPaused, setAnimationPaused,
// importedSkins, importedSkins,
// setImportedSkins, addImportedSkins,
} = useWarrior(); } = useWarrior();
const { /*selectedMaterialIndex,*/ setSelectedMaterialIndex } = useTools(); const { /*selectedMaterialIndex,*/ setSelectedMaterialIndex } = useTools();
// const materialDefs = materials[actualModel]; // const materialDefs = materials[actualModel];
@ -40,6 +41,12 @@ export default function WarriorSelector() {
); );
const fileInputRef = useRef<HTMLInputElement | null>(null); const fileInputRef = useRef<HTMLInputElement | null>(null);
const importedSkinsForModel = importedSkins.get(actualModel) ?? emptyMap;
const selectableImportedSkins = Array.from(
importedSkinsForModel.values()
).filter((skin) => skin.isComplete);
useEffect(() => { useEffect(() => {
const controller = new AbortController(); const controller = new AbortController();
const signal = controller.signal; const signal = controller.signal;
@ -193,6 +200,20 @@ export default function WarriorSelector() {
); );
})} })}
</optgroup> </optgroup>
{selectableImportedSkins.length ? (
<optgroup label="Imported Skins" data-skin-type="import">
{selectableImportedSkins.map((skin) => {
return (
<option
key={`import/${skin.name ?? "__untitled__"}`}
value={`import/${skin.name ?? "__untitled__"}`}
>
{skin.name || "Untitled Imported Skin"}
</option>
);
})}
</optgroup>
) : null}
{newSkins[actualModel]?.length ? ( {newSkins[actualModel]?.length ? (
<optgroup label="New Skins ✨" data-skin-type="custom"> <optgroup label="New Skins ✨" data-skin-type="custom">
{newSkins[actualModel]?.map((name: string) => { {newSkins[actualModel]?.map((name: string) => {
@ -229,6 +250,20 @@ export default function WarriorSelector() {
<option value={modelDefaults[actualModel]}>Default</option> <option value={modelDefaults[actualModel]}>Default</option>
</optgroup> </optgroup>
) : null} ) : null}
{selectableImportedSkins.length ? (
<optgroup label="Imported Skins" data-skin-type="import">
{selectableImportedSkins.map((skin) => {
return (
<option
key={`import/${skin.name ?? "__untitled__"}`}
value={`import/${skin.name ?? "__untitled__"}`}
>
{skin.name || "Untitled Imported Skin"}
</option>
);
})}
</optgroup>
) : null}
{customSkins[actualModel]?.length ? ( {customSkins[actualModel]?.length ? (
<optgroup label="Custom Skins" data-skin-type="custom"> <optgroup label="Custom Skins" data-skin-type="custom">
{customSkins[actualModel].map((name: string) => ( {customSkins[actualModel].map((name: string) => (
@ -259,16 +294,34 @@ export default function WarriorSelector() {
const foundModels = await importMultipleFilesToModels( const foundModels = await importMultipleFilesToModels(
event.target.files ?? [] event.target.files ?? []
); );
const selectedModelSkins = foundModels.get(actualModel); addImportedSkins(foundModels);
if (selectedModelSkins) { const currentModelSkins = foundModels.get(actualModel);
const skins = Array.from(selectedModelSkins.values()); if (currentModelSkins) {
for (const skin of skins) { const completeSkins = Array.from(
currentModelSkins.values()
).filter((skin) => skin.isComplete);
if (completeSkins.length) {
const skin = completeSkins[0];
setSelectedSkinType("import");
setSelectedSkinSection("import");
setSelectedSkin(skin.name ?? "__untitled__");
setSelectedMaterialIndex(0);
setSelectedAnimation(null);
return;
}
}
for (const [modelName, skinsByName] of Array.from(
foundModels.entries()
)) {
for (const skin of Array.from(skinsByName.values())) {
if (skin.isComplete) { if (skin.isComplete) {
setSelectedSkin(null); setSelectedModel(modelName);
setSelectedSkinSection(null); setSelectedModelType(modelToModelType(modelName));
setSkinImageUrls( setSelectedSkinType("import");
Object.fromEntries(skin.materials.entries()) setSelectedSkinSection("import");
); setSelectedSkin(skin.name ?? "__untitled__");
setSelectedMaterialIndex(0);
setSelectedAnimation(null);
break; break;
} }
} }

View file

@ -22,6 +22,43 @@ export function clearImportedSkins() {
const ignoreFilePattern = /^(\.|__MACOSX)/; const ignoreFilePattern = /^(\.|__MACOSX)/;
export function modelToModelType(modelName: string) {
switch (modelName) {
case "lmale":
case "mmale":
case "hmale":
case "lfemale":
case "mfemale":
case "hfemale":
case "lbioderm":
case "mbioderm":
case "hbioderm":
return "player";
case "disc":
case "chaingun":
case "grenade_launcher":
case "sniper":
case "plasmathrower":
case "energy":
case "shocklance":
case "elf":
case "missile":
case "mortar":
case "repair":
case "targeting":
return "weapon";
case "vehicle_grav_scout":
case "vehicle_grav_tank":
case "vehicle_land_mpbbase":
case "vehicle_air_scout":
case "vehicle_air_bomber":
case "vehicle_air_hapc":
return "vehicle";
default:
throw new Error("Unknown model");
}
}
export async function readZipFile(inputFile: File) { export async function readZipFile(inputFile: File) {
const content = await JSZip.loadAsync(inputFile); const content = await JSZip.loadAsync(inputFile);
const skins = await Promise.all( const skins = await Promise.all(
@ -175,7 +212,7 @@ function pathToModels(path: string, skinName: string | null = null) {
return null; return null;
} }
type Skin = { export type Skin = {
name: string | null; name: string | null;
isComplete: null | boolean; isComplete: null | boolean;
materials: Map<string, string[]>; materials: Map<string, string[]>;

View file

@ -1,4 +1,5 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import { Skin } from "./importUtils";
type WarriorContextValue = { type WarriorContextValue = {
actualModel: string; actualModel: string;
@ -28,6 +29,8 @@ type WarriorContextValue = {
setSelectedModelType: (selectedModelType: string) => void; setSelectedModelType: (selectedModelType: string) => void;
slowModeEnabled: boolean; slowModeEnabled: boolean;
setSlowModeEnabled: (slowModeEnabled: boolean) => void; setSlowModeEnabled: (slowModeEnabled: boolean) => void;
importedSkins: Map<string, Map<string | null, Skin>>;
addImportedSkins: (newSkins: Map<string, Map<string | null, Skin>>) => void;
}; };
const WarriorContext = React.createContext<WarriorContextValue | null>(null); const WarriorContext = React.createContext<WarriorContextValue | null>(null);