177 lines
4.7 KiB
TypeScript
177 lines
4.7 KiB
TypeScript
import {
|
|
buildBodyPath,
|
|
buildMedievalAtlasSpec,
|
|
buildRaceAssetPath,
|
|
clampMedievalAtlasFrame,
|
|
getMedievalAtlasOptions,
|
|
getMedievalPoseOptions,
|
|
MEDIEVAL_BODY_COLORS,
|
|
type MedievalAtlasSourceType,
|
|
type MedievalNpcVisualOverride,
|
|
type MedievalRace,
|
|
} from '../data/medievalNpcVisuals';
|
|
import type { Encounter } from '../types';
|
|
import { type NpcLayoutConfig, type NpcLayoutPart } from './npcVisualShared';
|
|
|
|
export type GearSourceType = 'none' | MedievalAtlasSourceType;
|
|
|
|
export type EditableNpcVisualState = {
|
|
race: MedievalRace;
|
|
bodyColor: string;
|
|
headIndex: number;
|
|
hairColorIndex: number;
|
|
hairStyleFrame: number;
|
|
facialHairEnabled: boolean;
|
|
facialHairColorIndex: number;
|
|
facialHairStyleFrame: number;
|
|
headgearType: GearSourceType;
|
|
headgearFile: string;
|
|
headgearFrame: number;
|
|
mainHandType: GearSourceType;
|
|
mainHandFile: string;
|
|
mainHandFrame: number;
|
|
offHandType: GearSourceType;
|
|
offHandFile: string;
|
|
offHandFrame: number;
|
|
};
|
|
|
|
export type EditorNpcOption = {
|
|
encounter: Encounter;
|
|
sceneNames: string[];
|
|
};
|
|
|
|
const NPC_LAYOUT_PARTS: NpcLayoutPart[] = [
|
|
'body',
|
|
'head',
|
|
'facialHair',
|
|
'hair',
|
|
'headgear',
|
|
'hand',
|
|
'mainHand',
|
|
'offHand',
|
|
];
|
|
|
|
export function sanitizeFrameSelection(
|
|
type: GearSourceType,
|
|
file: string,
|
|
frame: number,
|
|
usage: 'headgear' | 'mainHand' | 'offHand',
|
|
) {
|
|
if (type === 'none' || !file) return 0;
|
|
const poseOptions = getMedievalPoseOptions(type, file, usage);
|
|
if (poseOptions.length === 0) return 0;
|
|
if (poseOptions.some(option => option.value === frame)) {
|
|
return clampMedievalAtlasFrame(type, file, frame);
|
|
}
|
|
const firstOption = poseOptions[0];
|
|
return firstOption ? firstOption.value : 0;
|
|
}
|
|
|
|
export function getDefaultFileForType(type: GearSourceType) {
|
|
if (type === 'none') return '';
|
|
return getMedievalAtlasOptions(type)[0]?.file ?? '';
|
|
}
|
|
|
|
export function getDefaultFrameForSelection(
|
|
type: GearSourceType,
|
|
file: string,
|
|
usage: 'headgear' | 'mainHand' | 'offHand',
|
|
) {
|
|
if (type === 'none' || !file) return 0;
|
|
return getMedievalPoseOptions(type, file, usage)[0]?.value ?? 0;
|
|
}
|
|
|
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
export function isNpcLayoutConfig(value: unknown): value is NpcLayoutConfig {
|
|
return (
|
|
isRecord(value)
|
|
&& NPC_LAYOUT_PARTS.every(part => {
|
|
const coordinate = value[part];
|
|
return (
|
|
isRecord(coordinate)
|
|
&& typeof coordinate.x === 'number'
|
|
&& Number.isFinite(coordinate.x)
|
|
&& typeof coordinate.y === 'number'
|
|
&& Number.isFinite(coordinate.y)
|
|
);
|
|
})
|
|
);
|
|
}
|
|
|
|
export function buildOverrideFromEditorState(
|
|
state: EditableNpcVisualState,
|
|
): MedievalNpcVisualOverride {
|
|
return {
|
|
race: state.race,
|
|
bodySrc: buildBodyPath(
|
|
state.bodyColor as (typeof MEDIEVAL_BODY_COLORS)[number],
|
|
),
|
|
headSrc: buildRaceAssetPath(state.race, 'head', state.headIndex),
|
|
hairSrc: buildRaceAssetPath(state.race, 'hair', state.hairColorIndex),
|
|
handSrc: buildRaceAssetPath(state.race, 'hand', 1),
|
|
facialHairSrc: state.facialHairEnabled
|
|
? buildRaceAssetPath(state.race, 'facialHair', state.facialHairColorIndex)
|
|
: undefined,
|
|
headgear:
|
|
state.headgearType === 'none'
|
|
? undefined
|
|
: buildMedievalAtlasSpec(
|
|
state.headgearType,
|
|
state.headgearFile,
|
|
sanitizeFrameSelection(
|
|
state.headgearType,
|
|
state.headgearFile,
|
|
state.headgearFrame,
|
|
'headgear',
|
|
),
|
|
),
|
|
mainHand:
|
|
state.mainHandType === 'none'
|
|
? undefined
|
|
: buildMedievalAtlasSpec(
|
|
state.mainHandType,
|
|
state.mainHandFile,
|
|
sanitizeFrameSelection(
|
|
state.mainHandType,
|
|
state.mainHandFile,
|
|
state.mainHandFrame,
|
|
'mainHand',
|
|
),
|
|
),
|
|
offHand:
|
|
state.offHandType === 'none'
|
|
? undefined
|
|
: buildMedievalAtlasSpec(
|
|
state.offHandType,
|
|
state.offHandFile,
|
|
sanitizeFrameSelection(
|
|
state.offHandType,
|
|
state.offHandFile,
|
|
state.offHandFrame,
|
|
'offHand',
|
|
),
|
|
),
|
|
bodyFrames: [0, 1, 2, 3],
|
|
headFrame: 0,
|
|
hairFrame: state.hairStyleFrame,
|
|
handFrame: 0,
|
|
facialHairFrame: state.facialHairEnabled
|
|
? state.facialHairStyleFrame
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
export function buildNpcVisualSavePayload(
|
|
overrideMap: Record<string, MedievalNpcVisualOverride>,
|
|
npcId: string,
|
|
editorState: EditableNpcVisualState,
|
|
) {
|
|
return {
|
|
...overrideMap,
|
|
[npcId]: buildOverrideFromEditorState(editorState),
|
|
};
|
|
}
|