mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
The model viewer can now show any character class' body type.
This commit is contained in:
parent
c9d4b6ab92
commit
dc9deee5a7
@ -14,6 +14,22 @@ export function arrays_equal<T>(
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param min - The minimum value, inclusive.
|
||||
* @param max - The maximum value, exclusive.
|
||||
* @returns A random integer between `min` and `max`.
|
||||
*/
|
||||
export function random_integer(min: number, max: number): number {
|
||||
return min + Math.floor(Math.random() * (max - min));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A random element from `array`.
|
||||
*/
|
||||
export function random_array_element<T>(array: readonly T[]): T {
|
||||
return array[random_integer(0, array.length)];
|
||||
}
|
||||
|
||||
export function array_buffers_equal(a: ArrayBuffer, b: ArrayBuffer): boolean {
|
||||
if (a.byteLength !== b.byteLength) return false;
|
||||
|
||||
@ -27,16 +43,6 @@ export function array_buffers_equal(a: ArrayBuffer, b: ArrayBuffer): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function create_array<T>(length: number, value: (index: number) => T): T[] {
|
||||
const array = Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
array[i] = value(i);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given filename without the file extension.
|
||||
*/
|
||||
|
@ -6,9 +6,24 @@ import { NjcmModel } from "../../core/data_formats/parsing/ninja/njcm";
|
||||
import { NjMotion, parse_njm } from "../../core/data_formats/parsing/ninja/motion";
|
||||
import { Disposable } from "../../core/observable/Disposable";
|
||||
import { DisposablePromise } from "../../core/DisposablePromise";
|
||||
import { CharacterClassModel } from "../model/CharacterClassModel";
|
||||
import {
|
||||
CharacterClassModel,
|
||||
FOMAR,
|
||||
FOMARL,
|
||||
FONEWEARL,
|
||||
FONEWM,
|
||||
HUCASEAL,
|
||||
HUCAST,
|
||||
HUMAR,
|
||||
HUNEWEARL,
|
||||
RACASEAL,
|
||||
RACAST,
|
||||
RAMAR,
|
||||
RAMARL,
|
||||
} from "../model/CharacterClassModel";
|
||||
import { parse_xvm, XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
|
||||
import { parse_afs } from "../../core/data_formats/parsing/afs";
|
||||
import { SectionId } from "../../core/model";
|
||||
|
||||
export class CharacterClassAssetLoader implements Disposable {
|
||||
private readonly nj_object_cache: Map<
|
||||
@ -53,6 +68,8 @@ export class CharacterClassAssetLoader implements Disposable {
|
||||
private load_all_nj_objects(
|
||||
model: CharacterClassModel,
|
||||
): DisposablePromise<NjObject<NjcmModel>> {
|
||||
const tex_ids = texture_ids(model, SectionId.Viridia, 0);
|
||||
|
||||
return this.load_body_part_geometry(model.name, "Body").then(body => {
|
||||
if (!body) {
|
||||
throw new Error(`Couldn't load body for player class ${model.name}.`);
|
||||
@ -64,7 +81,7 @@ export class CharacterClassAssetLoader implements Disposable {
|
||||
}
|
||||
|
||||
// Shift by 1 for the section ID and once for every body texture ID.
|
||||
let shift = 1 + model.body_tex_ids.length;
|
||||
let shift = 1 + tex_ids.body.length;
|
||||
this.shift_texture_ids(head, shift);
|
||||
this.add_to_bone(body, head, 59);
|
||||
|
||||
@ -77,7 +94,7 @@ export class CharacterClassAssetLoader implements Disposable {
|
||||
return body;
|
||||
}
|
||||
|
||||
shift += model.head_tex_ids.length;
|
||||
shift += tex_ids.head.length;
|
||||
this.shift_texture_ids(hair, shift);
|
||||
this.add_to_bone(head, hair, 0);
|
||||
|
||||
@ -88,7 +105,7 @@ export class CharacterClassAssetLoader implements Disposable {
|
||||
return this.load_body_part_geometry(model.name, "Accessory", 0).then(
|
||||
accessory => {
|
||||
if (accessory) {
|
||||
shift += model.hair_tex_ids.length;
|
||||
shift += tex_ids.hair.length;
|
||||
this.shift_texture_ids(accessory, shift);
|
||||
this.add_to_bone(hair, accessory, 0);
|
||||
}
|
||||
@ -139,11 +156,15 @@ export class CharacterClassAssetLoader implements Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
load_textures(model: CharacterClassModel): Promise<readonly XvrTexture[]> {
|
||||
let xvr_texture = this.xvr_texture_cache.get(model.name);
|
||||
async load_textures(
|
||||
model: CharacterClassModel,
|
||||
section_id: SectionId,
|
||||
body: number,
|
||||
): Promise<readonly (XvrTexture | undefined)[]> {
|
||||
let xvr_textures = this.xvr_texture_cache.get(model.name);
|
||||
|
||||
if (!xvr_texture) {
|
||||
xvr_texture = this.http_client
|
||||
if (!xvr_textures) {
|
||||
xvr_textures = this.http_client
|
||||
.get(`/player/${model.name}Tex.afs`)
|
||||
.array_buffer()
|
||||
.then(buffer => {
|
||||
@ -159,7 +180,16 @@ export class CharacterClassAssetLoader implements Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
return xvr_texture;
|
||||
const tex = await xvr_textures;
|
||||
const tex_ids = texture_ids(model, section_id, body);
|
||||
|
||||
return [
|
||||
tex_ids.section_id,
|
||||
...tex_ids.body,
|
||||
...tex_ids.head,
|
||||
...tex_ids.hair,
|
||||
...tex_ids.accessories,
|
||||
].map(idx => (idx == undefined ? undefined : tex[idx]));
|
||||
}
|
||||
|
||||
load_animation(animation_id: number, bone_count: number): Promise<NjMotion> {
|
||||
@ -181,3 +211,151 @@ export class CharacterClassAssetLoader implements Disposable {
|
||||
function character_class_to_url(player_class: string, body_part: string, no?: number): string {
|
||||
return `/player/${player_class}${body_part}${no == null ? "" : no}.nj`;
|
||||
}
|
||||
|
||||
function texture_ids(
|
||||
model: CharacterClassModel,
|
||||
section_id: SectionId,
|
||||
body: number,
|
||||
): {
|
||||
section_id: number;
|
||||
body: number[];
|
||||
head: number[];
|
||||
hair: (number | undefined)[];
|
||||
accessories: (number | undefined)[];
|
||||
} {
|
||||
switch (model) {
|
||||
case HUMAR: {
|
||||
const body_idx = body * 3;
|
||||
return {
|
||||
section_id: section_id + 126,
|
||||
body: [body_idx, body_idx + 1, body_idx + 2, body + 108],
|
||||
head: [54, 55],
|
||||
hair: [94, 95],
|
||||
accessories: [],
|
||||
};
|
||||
}
|
||||
case HUNEWEARL: {
|
||||
const body_idx = body * 13;
|
||||
return {
|
||||
section_id: section_id + 299,
|
||||
body: [
|
||||
body_idx + 13,
|
||||
body_idx,
|
||||
body_idx + 1,
|
||||
body_idx + 2,
|
||||
body_idx + 3,
|
||||
277,
|
||||
body + 281,
|
||||
],
|
||||
head: [235, 239],
|
||||
hair: [260, 259],
|
||||
accessories: [],
|
||||
};
|
||||
}
|
||||
case HUCAST: {
|
||||
const body_idx = body * 5;
|
||||
return {
|
||||
section_id: section_id + 275,
|
||||
body: [body_idx, body_idx + 1, body_idx + 2, body + 250],
|
||||
// Eyes don't look correct because NJCM material chunks (which contain alpha blending
|
||||
// details) aren't parsed yet. Material.blending should be AdditiveBlending.
|
||||
head: [body_idx + 3, body_idx + 4],
|
||||
hair: [],
|
||||
accessories: [],
|
||||
};
|
||||
}
|
||||
case HUCASEAL: {
|
||||
const body_idx = body * 5;
|
||||
return {
|
||||
section_id: section_id + 375,
|
||||
body: [body_idx, body_idx + 1, body_idx + 2],
|
||||
head: [body_idx + 3, body_idx + 4],
|
||||
hair: [],
|
||||
accessories: [],
|
||||
};
|
||||
}
|
||||
case RAMAR: {
|
||||
const body_idx = body * 7;
|
||||
return {
|
||||
section_id: section_id + 197,
|
||||
body: [body_idx + 4, body_idx + 5, body_idx + 6, body + 179],
|
||||
head: [126, 127],
|
||||
hair: [166, 167],
|
||||
accessories: [undefined, undefined, body_idx + 2],
|
||||
};
|
||||
}
|
||||
case RAMARL: {
|
||||
const body_idx = body * 16;
|
||||
return {
|
||||
section_id: section_id + 322,
|
||||
body: [body_idx + 15, body_idx + 1, body_idx],
|
||||
head: [288],
|
||||
hair: [308, 309],
|
||||
accessories: [undefined, undefined, body_idx + 8],
|
||||
};
|
||||
}
|
||||
case RACAST: {
|
||||
const body_idx = body * 5;
|
||||
return {
|
||||
section_id: section_id + 300,
|
||||
body: [body_idx, body_idx + 1, body_idx + 2, body_idx + 3, body + 275],
|
||||
head: [body_idx + 4],
|
||||
hair: [],
|
||||
accessories: [],
|
||||
};
|
||||
}
|
||||
case RACASEAL: {
|
||||
const body_idx = body * 5;
|
||||
return {
|
||||
section_id: section_id + 375,
|
||||
body: [body + 350, body_idx, body_idx + 1, body_idx + 2],
|
||||
head: [body_idx + 3],
|
||||
hair: [body_idx + 4],
|
||||
accessories: [],
|
||||
};
|
||||
}
|
||||
case FOMAR: {
|
||||
const body_idx = body === 0 ? 0 : body * 15 + 2;
|
||||
return {
|
||||
section_id: section_id + 310,
|
||||
body: [body_idx + 12, body_idx + 13, body_idx + 14, body_idx],
|
||||
head: [276, 272],
|
||||
hair: [undefined, 296, 297],
|
||||
accessories: [body_idx + 4],
|
||||
};
|
||||
}
|
||||
case FOMARL: {
|
||||
const body_idx = body * 16;
|
||||
return {
|
||||
section_id: section_id + 310,
|
||||
body: [body_idx, body_idx + 2, body_idx + 1, 322 /*hands*/],
|
||||
head: [288],
|
||||
hair: [undefined, undefined, 308],
|
||||
accessories: [body_idx + 3, body_idx + 4],
|
||||
};
|
||||
}
|
||||
case FONEWM: {
|
||||
const body_idx = body * 17;
|
||||
return {
|
||||
section_id: section_id + 344,
|
||||
body: [body_idx + 4, 340 /*hands*/, body_idx, body_idx + 5],
|
||||
head: [306, 310],
|
||||
hair: [undefined, undefined, 330],
|
||||
// ID 16 for glasses is incorrect but looks decent.
|
||||
accessories: [body_idx + 6, body_idx + 16, 330],
|
||||
};
|
||||
}
|
||||
case FONEWEARL: {
|
||||
const body_idx = body * 26;
|
||||
return {
|
||||
section_id: section_id + 505,
|
||||
body: [body_idx + 1, body_idx, body_idx + 2, 501 /*hands*/],
|
||||
head: [472, 468],
|
||||
hair: [undefined, undefined, 492],
|
||||
accessories: [body_idx + 12, body_idx + 13],
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(`No textures for character class ${model.name}.`);
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +1,118 @@
|
||||
import { SectionIds } from "../../core/model";
|
||||
import { create_array } from "../../core/util";
|
||||
|
||||
export class CharacterClassModel {
|
||||
readonly name: string;
|
||||
readonly body_style_count: number;
|
||||
readonly head_style_count: number;
|
||||
readonly hair_style_count: number;
|
||||
readonly hair_styles_with_accessory: Set<number>;
|
||||
/**
|
||||
* Can be indexed with {@link SectionId}
|
||||
*/
|
||||
readonly section_id_tex_ids: readonly number[];
|
||||
readonly body_tex_ids: readonly number[];
|
||||
readonly body_tex_ids: readonly number[][];
|
||||
readonly head_tex_ids: readonly (number | undefined)[];
|
||||
readonly hair_tex_ids: readonly (number | undefined)[];
|
||||
readonly accessory_tex_ids: readonly (number | undefined)[];
|
||||
|
||||
constructor(props: {
|
||||
name: string;
|
||||
body_style_count?: number;
|
||||
head_style_count: number;
|
||||
hair_style_count: number;
|
||||
hair_styles_with_accessory: Set<number>;
|
||||
section_id_tex_id: number;
|
||||
body_tex_ids: number[];
|
||||
body_tex_ids?: number[][];
|
||||
head_tex_ids?: (number | undefined)[];
|
||||
hair_tex_ids?: (number | undefined)[];
|
||||
accessory_tex_ids?: (number | undefined)[];
|
||||
}) {
|
||||
this.name = props.name;
|
||||
this.body_style_count = props.body_style_count ?? 1;
|
||||
this.head_style_count = props.head_style_count;
|
||||
this.hair_style_count = props.hair_style_count;
|
||||
this.hair_styles_with_accessory = props.hair_styles_with_accessory;
|
||||
this.section_id_tex_ids = create_array(SectionIds.length, i => props.section_id_tex_id + i);
|
||||
this.body_tex_ids = props.body_tex_ids;
|
||||
this.body_tex_ids = props.body_tex_ids ?? [];
|
||||
this.head_tex_ids = props.head_tex_ids ?? [];
|
||||
this.hair_tex_ids = props.hair_tex_ids ?? [];
|
||||
this.accessory_tex_ids = props.accessory_tex_ids ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
export const HUMAR = new CharacterClassModel({
|
||||
name: "HUmar",
|
||||
body_style_count: 18,
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([6]),
|
||||
});
|
||||
export const HUNEWEARL = new CharacterClassModel({
|
||||
name: "HUnewearl",
|
||||
body_style_count: 18,
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set(),
|
||||
});
|
||||
export const HUCAST = new CharacterClassModel({
|
||||
name: "HUcast",
|
||||
body_style_count: 25,
|
||||
head_style_count: 5,
|
||||
hair_style_count: 0,
|
||||
hair_styles_with_accessory: new Set(),
|
||||
});
|
||||
export const HUCASEAL = new CharacterClassModel({
|
||||
name: "HUcaseal",
|
||||
body_style_count: 25,
|
||||
head_style_count: 5,
|
||||
hair_style_count: 0,
|
||||
hair_styles_with_accessory: new Set(),
|
||||
});
|
||||
export const RAMAR = new CharacterClassModel({
|
||||
name: "RAmar",
|
||||
body_style_count: 18,
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
});
|
||||
export const RAMARL = new CharacterClassModel({
|
||||
name: "RAmarl",
|
||||
body_style_count: 18,
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
});
|
||||
export const RACAST = new CharacterClassModel({
|
||||
name: "RAcast",
|
||||
body_style_count: 25,
|
||||
head_style_count: 5,
|
||||
hair_style_count: 0,
|
||||
hair_styles_with_accessory: new Set(),
|
||||
});
|
||||
export const RACASEAL = new CharacterClassModel({
|
||||
name: "RAcaseal",
|
||||
body_style_count: 25,
|
||||
head_style_count: 5,
|
||||
hair_style_count: 0,
|
||||
hair_styles_with_accessory: new Set(),
|
||||
});
|
||||
export const FOMAR = new CharacterClassModel({
|
||||
name: "FOmar",
|
||||
body_style_count: 18,
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
});
|
||||
export const FOMARL = new CharacterClassModel({
|
||||
name: "FOmarl",
|
||||
body_style_count: 18,
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
});
|
||||
export const FONEWM = new CharacterClassModel({
|
||||
name: "FOnewm",
|
||||
body_style_count: 18,
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
});
|
||||
export const FONEWEARL = new CharacterClassModel({
|
||||
name: "FOnewearl",
|
||||
body_style_count: 18,
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
});
|
||||
|
@ -2,7 +2,21 @@ import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCur
|
||||
import { Endianness } from "../../core/data_formats/Endianness";
|
||||
import { NjMotion, parse_njm } from "../../core/data_formats/parsing/ninja/motion";
|
||||
import { NjObject, parse_nj, parse_xj } from "../../core/data_formats/parsing/ninja";
|
||||
import { CharacterClassModel } from "../model/CharacterClassModel";
|
||||
import {
|
||||
CharacterClassModel,
|
||||
FOMAR,
|
||||
FOMARL,
|
||||
FONEWEARL,
|
||||
FONEWM,
|
||||
HUCASEAL,
|
||||
HUCAST,
|
||||
HUMAR,
|
||||
HUNEWEARL,
|
||||
RACASEAL,
|
||||
RACAST,
|
||||
RAMAR,
|
||||
RAMARL,
|
||||
} from "../model/CharacterClassModel";
|
||||
import { CharacterClassAnimationModel } from "../model/CharacterClassAnimationModel";
|
||||
import { WritableProperty } from "../../core/observable/property/WritableProperty";
|
||||
import { read_file } from "../../core/read_file";
|
||||
@ -15,6 +29,7 @@ import { Store } from "../../core/stores/Store";
|
||||
import { LogManager } from "../../core/Logger";
|
||||
import { ListProperty } from "../../core/observable/property/list/ListProperty";
|
||||
import { parse_afs } from "../../core/data_formats/parsing/afs";
|
||||
import { random_array_element, random_integer } from "../../core/util";
|
||||
import { SectionIds } from "../../core/model";
|
||||
|
||||
const logger = LogManager.get("viewer/stores/ModelStore");
|
||||
@ -41,132 +56,18 @@ export class Model3DStore extends Store {
|
||||
private readonly _animation_frame: WritableProperty<number> = property(0);
|
||||
|
||||
readonly models: readonly CharacterClassModel[] = [
|
||||
new CharacterClassModel({
|
||||
name: "HUmar",
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([6]),
|
||||
section_id_tex_id: 126,
|
||||
body_tex_ids: [0, 1, 2, 108],
|
||||
head_tex_ids: [54, 55],
|
||||
hair_tex_ids: [94, 95],
|
||||
}),
|
||||
new CharacterClassModel({
|
||||
name: "HUnewearl",
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set(),
|
||||
section_id_tex_id: 299,
|
||||
body_tex_ids: [13, 0, 1, 2, 3, 277, 281],
|
||||
head_tex_ids: [235, 239],
|
||||
hair_tex_ids: [260, 259],
|
||||
}),
|
||||
new CharacterClassModel({
|
||||
name: "HUcast",
|
||||
head_style_count: 5,
|
||||
hair_style_count: 0,
|
||||
hair_styles_with_accessory: new Set(),
|
||||
section_id_tex_id: 275,
|
||||
body_tex_ids: [0, 1, 2, 250],
|
||||
// Eyes don't look correct because NJCM material chunks (which contain alpha blending
|
||||
// details) aren't parsed yet. Material.blending should be AdditiveBlending.
|
||||
head_tex_ids: [3, 4],
|
||||
}),
|
||||
new CharacterClassModel({
|
||||
name: "HUcaseal",
|
||||
head_style_count: 5,
|
||||
hair_style_count: 0,
|
||||
hair_styles_with_accessory: new Set(),
|
||||
section_id_tex_id: 375,
|
||||
body_tex_ids: [0, 1, 2],
|
||||
head_tex_ids: [3, 4],
|
||||
}),
|
||||
new CharacterClassModel({
|
||||
name: "RAmar",
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
section_id_tex_id: 197,
|
||||
body_tex_ids: [4, 5, 6, 179],
|
||||
head_tex_ids: [126, 127],
|
||||
hair_tex_ids: [166, 167],
|
||||
accessory_tex_ids: [undefined, undefined, 2],
|
||||
}),
|
||||
new CharacterClassModel({
|
||||
name: "RAmarl",
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
section_id_tex_id: 322,
|
||||
body_tex_ids: [15, 1, 0],
|
||||
head_tex_ids: [288],
|
||||
hair_tex_ids: [308, 309],
|
||||
accessory_tex_ids: [undefined, undefined, 8],
|
||||
}),
|
||||
new CharacterClassModel({
|
||||
name: "RAcast",
|
||||
head_style_count: 5,
|
||||
hair_style_count: 0,
|
||||
hair_styles_with_accessory: new Set(),
|
||||
section_id_tex_id: 300,
|
||||
body_tex_ids: [0, 1, 2, 3, 275],
|
||||
head_tex_ids: [4],
|
||||
}),
|
||||
new CharacterClassModel({
|
||||
name: "RAcaseal",
|
||||
head_style_count: 5,
|
||||
hair_style_count: 0,
|
||||
hair_styles_with_accessory: new Set(),
|
||||
section_id_tex_id: 375,
|
||||
body_tex_ids: [350, 0, 1, 2],
|
||||
head_tex_ids: [3],
|
||||
hair_tex_ids: [4],
|
||||
}),
|
||||
new CharacterClassModel({
|
||||
name: "FOmar",
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
section_id_tex_id: 310,
|
||||
body_tex_ids: [12, 13, 14, 0],
|
||||
head_tex_ids: [276, 272],
|
||||
hair_tex_ids: [undefined, 296, 297],
|
||||
accessory_tex_ids: [4],
|
||||
}),
|
||||
new CharacterClassModel({
|
||||
name: "FOmarl",
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
section_id_tex_id: 326,
|
||||
body_tex_ids: [0, 2, 1, 322],
|
||||
head_tex_ids: [288],
|
||||
hair_tex_ids: [undefined, undefined, 308],
|
||||
accessory_tex_ids: [3, 4],
|
||||
}),
|
||||
new CharacterClassModel({
|
||||
name: "FOnewm",
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
section_id_tex_id: 344,
|
||||
body_tex_ids: [4, 340, 0, 5],
|
||||
head_tex_ids: [306, 310],
|
||||
hair_tex_ids: [undefined, undefined, 330],
|
||||
// ID 16 for glasses is incorrect but looks decent.
|
||||
accessory_tex_ids: [6, 16, 330],
|
||||
}),
|
||||
new CharacterClassModel({
|
||||
name: "FOnewearl",
|
||||
head_style_count: 1,
|
||||
hair_style_count: 10,
|
||||
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
section_id_tex_id: 505,
|
||||
body_tex_ids: [1, 0, 2, 501],
|
||||
head_tex_ids: [472, 468],
|
||||
hair_tex_ids: [undefined, undefined, 492],
|
||||
accessory_tex_ids: [12, 13],
|
||||
}),
|
||||
HUMAR,
|
||||
HUNEWEARL,
|
||||
HUCAST,
|
||||
HUCASEAL,
|
||||
RAMAR,
|
||||
RAMARL,
|
||||
RACAST,
|
||||
RACASEAL,
|
||||
FOMAR,
|
||||
FOMARL,
|
||||
FONEWM,
|
||||
FONEWEARL,
|
||||
];
|
||||
|
||||
readonly animations: readonly CharacterClassAnimationModel[] = new Array(572)
|
||||
@ -195,29 +96,21 @@ export class Model3DStore extends Store {
|
||||
this.current_animation.observe(({ value }) => this.load_animation(value)),
|
||||
);
|
||||
|
||||
this.set_current_model(this.models[Math.floor(Math.random() * this.models.length)]);
|
||||
this.set_current_model(random_array_element(this.models));
|
||||
}
|
||||
|
||||
set_current_model = (current_model: CharacterClassModel): void => {
|
||||
set_current_model = (current_model?: CharacterClassModel): void => {
|
||||
this._current_model.val = current_model;
|
||||
};
|
||||
|
||||
clear_current_model = (): void => {
|
||||
this._current_model.val = undefined;
|
||||
};
|
||||
|
||||
set_show_skeleton = (show_skeleton: boolean): void => {
|
||||
this._show_skeleton.val = show_skeleton;
|
||||
};
|
||||
|
||||
set_current_animation = (animation: CharacterClassAnimationModel): void => {
|
||||
set_current_animation = (animation?: CharacterClassAnimationModel): void => {
|
||||
this._current_animation.val = animation;
|
||||
};
|
||||
|
||||
clear_current_animation = (): void => {
|
||||
this._current_animation.val = undefined;
|
||||
};
|
||||
|
||||
set_animation_playing = (playing: boolean): void => {
|
||||
this._animation_playing.val = playing;
|
||||
};
|
||||
@ -237,7 +130,8 @@ export class Model3DStore extends Store {
|
||||
const cursor = new ArrayBufferCursor(buffer, Endianness.Little);
|
||||
|
||||
if (file.name.endsWith(".nj")) {
|
||||
this.clear_current_model();
|
||||
this.set_current_model(undefined);
|
||||
this._current_textures.clear();
|
||||
|
||||
const nj_object = parse_nj(cursor)[0];
|
||||
|
||||
@ -247,7 +141,8 @@ export class Model3DStore extends Store {
|
||||
has_skeleton: true,
|
||||
});
|
||||
} else if (file.name.endsWith(".xj")) {
|
||||
this.clear_current_model();
|
||||
this.set_current_model(undefined);
|
||||
this._current_textures.clear();
|
||||
|
||||
const nj_object = parse_xj(cursor)[0];
|
||||
|
||||
@ -257,7 +152,7 @@ export class Model3DStore extends Store {
|
||||
has_skeleton: false,
|
||||
});
|
||||
} else if (file.name.endsWith(".njm")) {
|
||||
this.clear_current_animation();
|
||||
this.set_current_animation(undefined);
|
||||
this._current_nj_motion.val = undefined;
|
||||
|
||||
const nj_data = this.current_nj_data.val;
|
||||
@ -267,22 +162,18 @@ export class Model3DStore extends Store {
|
||||
this._current_nj_motion.val = parse_njm(cursor, nj_data.bone_count);
|
||||
}
|
||||
} else if (file.name.endsWith(".xvm")) {
|
||||
if (this.current_model) {
|
||||
this._current_textures.val = parse_xvm(cursor).textures;
|
||||
}
|
||||
this._current_textures.val = parse_xvm(cursor).textures;
|
||||
} else if (file.name.endsWith(".afs")) {
|
||||
if (this.current_model) {
|
||||
const files = parse_afs(cursor);
|
||||
const textures: XvrTexture[] = [];
|
||||
const files = parse_afs(cursor);
|
||||
const textures: XvrTexture[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
textures.push(
|
||||
...parse_xvm(new ArrayBufferCursor(file, Endianness.Little)).textures,
|
||||
);
|
||||
}
|
||||
|
||||
this._current_textures.val = textures;
|
||||
for (const file of files) {
|
||||
textures.push(
|
||||
...parse_xvm(new ArrayBufferCursor(file, Endianness.Little)).textures,
|
||||
);
|
||||
}
|
||||
|
||||
this._current_textures.val = textures;
|
||||
} else {
|
||||
logger.error(`Unknown file extension in filename "${file.name}".`);
|
||||
}
|
||||
@ -292,32 +183,26 @@ export class Model3DStore extends Store {
|
||||
};
|
||||
|
||||
private load_model = async (model?: CharacterClassModel): Promise<void> => {
|
||||
this.clear_current_animation();
|
||||
this.set_current_animation(undefined);
|
||||
|
||||
if (model) {
|
||||
try {
|
||||
this.set_current_nj_data(undefined);
|
||||
|
||||
const nj_object = await this.asset_loader.load_geometry(model);
|
||||
|
||||
this._current_textures.val = await this.asset_loader.load_textures(
|
||||
model,
|
||||
random_array_element(SectionIds),
|
||||
random_integer(0, model.body_style_count),
|
||||
);
|
||||
|
||||
this.set_current_nj_data({
|
||||
nj_object,
|
||||
// Ignore the bones from the head parts.
|
||||
bone_count: model ? 64 : nj_object.bone_count(),
|
||||
has_skeleton: true,
|
||||
});
|
||||
|
||||
const textures = await this.asset_loader.load_textures(model);
|
||||
|
||||
this._current_textures.val = [
|
||||
textures[
|
||||
model.section_id_tex_ids[Math.floor(Math.random() * SectionIds.length)]
|
||||
],
|
||||
...[
|
||||
...model.body_tex_ids,
|
||||
...model.head_tex_ids,
|
||||
...model.hair_tex_ids,
|
||||
...model.accessory_tex_ids,
|
||||
].map(id => (id == undefined ? undefined : textures[id])),
|
||||
];
|
||||
} catch (e) {
|
||||
logger.error(`Couldn't load model for ${model.name}.`);
|
||||
this._current_nj_data.val = undefined;
|
||||
@ -327,8 +212,7 @@ export class Model3DStore extends Store {
|
||||
}
|
||||
};
|
||||
|
||||
private set_current_nj_data(nj_data: NjData): void {
|
||||
this._current_textures.clear();
|
||||
private set_current_nj_data(nj_data?: NjData): void {
|
||||
this._current_nj_data.val = nj_data;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user