fix(art): 将 NPC 渲染文件的基础寻址从 objects 修正为 monsters
修正 Deckard Cain、Jerhyn 等城镇 NPC 的基础美术路径。通过从 objectEntry 下传的 baseIsMonsters 布尔标识,使特定 Object 的 DCC 及 COF 查表过程自动从默认的 `data\global\objects` 切换到 `data\global\monsters`, 彻底解决打包器因跨域失效而丢弃图集的情况。
This commit is contained in:
parent
11e0688930
commit
11439256bf
|
|
@ -483,7 +483,7 @@ export function resolveDs1Object(
|
|||
token,
|
||||
mode: entry.mode,
|
||||
artless: token.trim() === '',
|
||||
kind: 'object',
|
||||
kind: entry.baseIsMonsters ? 'npc' : 'object',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -511,8 +511,8 @@ export interface ResolvedDs1Object {
|
|||
readonly mode: string
|
||||
/** True when no token could be found: drawing nothing is then correct. */
|
||||
readonly artless: boolean
|
||||
/** `object` for a real object, `monster` for a DS1 monster spawn point. */
|
||||
readonly kind: 'object' | 'monster'
|
||||
/** `object` for a real object, `monster` for a DS1 monster spawn point, `npc` for named NPC. */
|
||||
readonly kind: 'object' | 'monster' | 'npc'
|
||||
}
|
||||
|
||||
/** What {@link resolveObjectArt} is asked to resolve. */
|
||||
|
|
@ -536,6 +536,8 @@ export interface ObjectArtRequest {
|
|||
readonly row: { name: string; token: string; subClass: number; mode: number; hp: number } | null
|
||||
/** Every member name in the mounted archives, for name-only resolution. */
|
||||
readonly members: readonly string[]
|
||||
/** True when the object is effectively an NPC (`baseIsMonsters`), resolving in `monsters/` not `objects/`. */
|
||||
readonly baseIsMonsters?: boolean
|
||||
}
|
||||
|
||||
/** The art the engine would use for one DS1 object. */
|
||||
|
|
@ -563,10 +565,11 @@ export interface ObjectArt {
|
|||
* @param modeIndex - animation mode index, 0..7.
|
||||
* @returns the lower-cased member name.
|
||||
*/
|
||||
export function objectCofMember(token: string, modeIndex: number): string {
|
||||
export function objectCofMember(token: string, modeIndex: number, baseIsMonsters = false): string {
|
||||
const name = token.trim().toLowerCase()
|
||||
const mode = modeToken(modeIndex).toLowerCase()
|
||||
return `${OBJECT_ROOT}${name}\\cof\\${name}${mode}${OBJECT_WEAPON}.cof`
|
||||
const root = baseIsMonsters ? 'data\\global\\monsters\\' : OBJECT_ROOT
|
||||
return `${root}${name}\\cof\\${name}${mode}${OBJECT_WEAPON}.cof`
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -580,13 +583,15 @@ export function objectCofMember(token: string, modeIndex: number): string {
|
|||
* @param token - `Objects.txt` `Token`, any case.
|
||||
* @param component - component directory, e.g. `tr`.
|
||||
* @param modeIndex - animation mode index, 0..7.
|
||||
* @param baseIsMonsters - true when the base directory is `monsters/` not `objects/`.
|
||||
* @returns the lower-cased `.dcc` member name.
|
||||
*/
|
||||
export function objectSpriteMember(token: string, component: string, modeIndex: number): string {
|
||||
export function objectSpriteMember(token: string, component: string, modeIndex: number, baseIsMonsters = false): string {
|
||||
const name = token.trim().toLowerCase()
|
||||
const part = component.toLowerCase()
|
||||
const mode = modeToken(modeIndex).toLowerCase()
|
||||
return `${OBJECT_ROOT}${name}\\${part}\\${name}${part}${OBJECT_ARMOR_CLASS}${mode}${OBJECT_WEAPON}.dcc`
|
||||
const root = baseIsMonsters ? 'data\\global\\monsters\\' : OBJECT_ROOT
|
||||
return `${root}${name}\\${part}\\${name}${part}${OBJECT_ARMOR_CLASS}${mode}${OBJECT_WEAPON}.dcc`
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -688,11 +693,11 @@ export function resolveObjectArt(request: ObjectArtRequest): ObjectArt {
|
|||
let modeIndex = requested
|
||||
let cof: string | undefined
|
||||
for (const candidate of modeOrder(requested)) {
|
||||
const found = findMember(members, objectCofMember(token, candidate))
|
||||
const found = findMember(members, objectCofMember(token, candidate, request.baseIsMonsters))
|
||||
if (found !== undefined) { cof = found; modeIndex = candidate; break }
|
||||
}
|
||||
if (cof === undefined) {
|
||||
notes.push(`token ${token} ships no COF for any mode (looked for ${objectCofMember(token, requested)})`)
|
||||
notes.push(`token ${token} ships no COF for any mode (looked for ${objectCofMember(token, requested, request.baseIsMonsters)})`)
|
||||
} else if (modeIndex !== requested) {
|
||||
notes.push(`mode ${String(requested)} has no COF; fell back to ${OBJECT_MODE_TOKENS[modeIndex] ?? 'NU'}`)
|
||||
}
|
||||
|
|
@ -704,7 +709,7 @@ export function resolveObjectArt(request: ObjectArtRequest): ObjectArt {
|
|||
const order = cof === undefined ? modeOrder(requested) : [modeIndex]
|
||||
outer: for (const index of order) {
|
||||
for (const component of COMPONENT_PREFERENCE) {
|
||||
const found = findMember(members, objectSpriteMember(token, component, index))
|
||||
const found = findMember(members, objectSpriteMember(token, component, index, request.baseIsMonsters))
|
||||
if (found === undefined) continue
|
||||
layers.push(found)
|
||||
if (member === null) { member = found; chosenMode = index }
|
||||
|
|
@ -716,7 +721,8 @@ export function resolveObjectArt(request: ObjectArtRequest): ObjectArt {
|
|||
if (member === null) {
|
||||
for (const index of order) {
|
||||
const mode = OBJECT_MODE_TOKENS[index]?.toLowerCase() ?? 'nu'
|
||||
const prefix = `${OBJECT_ROOT}${token.toLowerCase()}\\`
|
||||
const root = request.baseIsMonsters ? 'data\\global\\monsters\\' : OBJECT_ROOT
|
||||
const prefix = `${root}${token.toLowerCase()}\\`
|
||||
const suffix = `${mode}${OBJECT_WEAPON}.dcc`
|
||||
const hits = members
|
||||
.filter(name => name.toLowerCase().startsWith(prefix) && name.toLowerCase().endsWith(suffix))
|
||||
|
|
@ -785,8 +791,9 @@ export async function loadObjectSheet(
|
|||
token: string,
|
||||
modeIndex: number,
|
||||
row?: ObjectsRow,
|
||||
baseIsMonsters = false,
|
||||
): Promise<ObjectSheet> {
|
||||
const cofMember = objectCofMember(token, modeIndex)
|
||||
const cofMember = objectCofMember(token, modeIndex, baseIsMonsters)
|
||||
let cof: CofFile
|
||||
try {
|
||||
cof = decodeCof(await archives.read(cofMember))
|
||||
|
|
@ -800,7 +807,7 @@ export async function loadObjectSheet(
|
|||
for (const layer of cof.layers) {
|
||||
const component = OBJECT_COMPONENTS[layer.type]
|
||||
if (component === undefined) { notes.push(`layer type ${String(layer.type)} has no component directory`); sprites.push(null); continue }
|
||||
const wanted = objectSpriteMember(token, component, modeIndex)
|
||||
const wanted = objectSpriteMember(token, component, modeIndex, baseIsMonsters)
|
||||
let found = findMember(names, wanted)
|
||||
if (found === undefined) found = findMember(names, wanted.replace(/\.dcc$/i, '.dc6'))
|
||||
if (found === undefined) { notes.push(`no sprite for component ${component} (${wanted})`); sprites.push(null); continue }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import { test, expect } from 'vitest'
|
||||
import { objectCofMember, objectSpriteMember } from '../src/game/objects.ts'
|
||||
|
||||
test('ordinary object art paths are unchanged (non-:m)', () => {
|
||||
expect(objectCofMember('D1', 0, false)).toBe('data\\global\\objects\\d1\\cof\\d1nuhth.cof')
|
||||
expect(objectSpriteMember('D1', 'TR', 0, false)).toBe('data\\global\\objects\\d1\\tr\\d1trlitnuhth.dcc')
|
||||
|
||||
// Also verify that omission (default) preserves the old path
|
||||
expect(objectCofMember('D1', 0)).toBe('data\\global\\objects\\d1\\cof\\d1nuhth.cof')
|
||||
expect(objectSpriteMember('D1', 'TR', 0)).toBe('data\\global\\objects\\d1\\tr\\d1trlitnuhth.dcc')
|
||||
})
|
||||
|
||||
test('NPC art paths resolve to monsters/ directory (:m)', () => {
|
||||
expect(objectCofMember('DC', 0, true)).toBe('data\\global\\monsters\\dc\\cof\\dcnuhth.cof')
|
||||
expect(objectSpriteMember('DC', 'TR', 0, true)).toBe('data\\global\\monsters\\dc\\tr\\dctrlitnuhth.dcc')
|
||||
})
|
||||
Loading…
Reference in New Issue