From 8223107921cf6d7d582b5fdb2d24a82ee416077a Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 17 Jul 2019 19:37:48 +0200 Subject: [PATCH] Refactored model loading code. --- src/domain/index.ts | 3 - src/loading/LoadingCache.ts | 18 + src/loading/areas.ts | 162 ++++++ src/loading/entities.ts | 237 +++++++++ src/loading/load_array_buffer.ts | 5 + src/loading/player.ts | 19 + src/rendering/EntityControls.ts | 311 ++++++++++++ src/rendering/QuestModelManager.ts | 128 +++++ src/rendering/QuestRenderer.ts | 465 +++--------------- src/rendering/Renderer.ts | 37 +- src/rendering/TextureRenderer.ts | 2 +- src/rendering/{ => conversion}/areas.ts | 8 +- src/rendering/{ => conversion}/entities.ts | 2 +- src/rendering/{ => conversion}/index.ts | 2 +- .../ninja_animation.ts} | 4 +- .../ninja_geometry.ts} | 6 +- .../ninja_textures.ts} | 2 +- .../xj_models.ts} | 6 +- src/rendering/entities.test.ts | 47 -- src/stores/AreaStore.ts | 97 +--- src/stores/EntityStore.ts | 163 ------ src/stores/ModelViewerStore.ts | 11 +- src/stores/QuestEditorStore.ts | 14 +- src/stores/binary_assets.ts | 249 ---------- 24 files changed, 987 insertions(+), 1011 deletions(-) create mode 100644 src/loading/LoadingCache.ts create mode 100644 src/loading/areas.ts create mode 100644 src/loading/entities.ts create mode 100644 src/loading/load_array_buffer.ts create mode 100644 src/loading/player.ts create mode 100644 src/rendering/EntityControls.ts create mode 100644 src/rendering/QuestModelManager.ts rename src/rendering/{ => conversion}/areas.ts (94%) rename src/rendering/{ => conversion}/entities.ts (96%) rename src/rendering/{ => conversion}/index.ts (71%) rename src/rendering/{animation.ts => conversion/ninja_animation.ts} (95%) rename src/rendering/{models.ts => conversion/ninja_geometry.ts} (97%) rename src/rendering/{textures.ts => conversion/ninja_textures.ts} (94%) rename src/rendering/{xj_model_to_geometry.ts => conversion/xj_models.ts} (95%) delete mode 100644 src/rendering/entities.test.ts delete mode 100644 src/stores/EntityStore.ts delete mode 100644 src/stores/binary_assets.ts diff --git a/src/domain/index.ts b/src/domain/index.ts index a0898037..596656c8 100644 --- a/src/domain/index.ts +++ b/src/domain/index.ts @@ -1,5 +1,4 @@ import { computed, observable } from "mobx"; -import { Object3D } from "three"; import { DatNpc, DatObject, DatUnknown } from "../data_formats/parsing/quest/dat"; import { Vec3 } from "../data_formats/vector"; import { enum_values } from "../enums"; @@ -186,8 +185,6 @@ export class QuestEntity { } } - @observable object_3d?: Object3D; - constructor(area_id: number, section_id: number, position: Vec3, rotation: Vec3) { if (Object.getPrototypeOf(this) === Object.getPrototypeOf(QuestEntity)) throw new Error("Abstract class should not be instantiated directly."); diff --git a/src/loading/LoadingCache.ts b/src/loading/LoadingCache.ts new file mode 100644 index 00000000..34329f6f --- /dev/null +++ b/src/loading/LoadingCache.ts @@ -0,0 +1,18 @@ +export class LoadingCache { + private map = new Map(); + + set(key: K, value: V): void { + this.map.set(key, value); + } + + get_or_set(key: K, new_value: () => V): V { + let v = this.map.get(key); + + if (v === undefined) { + v = new_value(); + this.map.set(key, v); + } + + return v; + } +} diff --git a/src/loading/areas.ts b/src/loading/areas.ts new file mode 100644 index 00000000..dbe45796 --- /dev/null +++ b/src/loading/areas.ts @@ -0,0 +1,162 @@ +import { Object3D } from "three"; +import { Endianness } from "../data_formats"; +import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor"; +import { parse_area_collision_geometry } from "../data_formats/parsing/area_collision_geometry"; +import { parse_area_geometry } from "../data_formats/parsing/area_geometry"; +import { Section } from "../domain"; +import { + area_collision_geometry_to_object_3d, + area_geometry_to_sections_and_object_3d, +} from "../rendering/conversion/areas"; +import { load_array_buffer } from "./load_array_buffer"; +import { LoadingCache } from "./LoadingCache"; + +const render_geometry_cache = new LoadingCache< + string, + { geometry: Promise; sections: Promise } +>(); +const collision_geometry_cache = new LoadingCache>(); + +export async function load_area_sections( + episode: number, + area_id: number, + area_variant: number +): Promise { + return render_geometry_cache.get_or_set(`${episode}-${area_id}-${area_variant}`, () => + load_area_sections_and_render_geometry(episode, area_id, area_variant) + ).sections; +} + +export async function load_area_render_geometry( + episode: number, + area_id: number, + area_variant: number +): Promise { + return render_geometry_cache.get_or_set(`${episode}-${area_id}-${area_variant}`, () => + load_area_sections_and_render_geometry(episode, area_id, area_variant) + ).geometry; +} + +export async function load_area_collision_geometry( + episode: number, + area_id: number, + area_variant: number +): Promise { + return collision_geometry_cache.get_or_set(`${episode}-${area_id}-${area_variant}`, () => + get_area_asset(episode, area_id, area_variant, "collision").then(buffer => + area_collision_geometry_to_object_3d( + parse_area_collision_geometry(new ArrayBufferCursor(buffer, Endianness.Little)) + ) + ) + ); +} + +function load_area_sections_and_render_geometry( + episode: number, + area_id: number, + area_variant: number +): { geometry: Promise; sections: Promise } { + const promise = get_area_asset(episode, area_id, area_variant, "render").then(buffer => + area_geometry_to_sections_and_object_3d( + parse_area_geometry(new ArrayBufferCursor(buffer, Endianness.Little)) + ) + ); + + return { + geometry: promise.then(([, object_3d]) => object_3d), + sections: promise.then(([sections]) => sections), + }; +} + +const area_base_names = [ + [ + ["city00_00", 1], + ["forest01", 1], + ["forest02", 1], + ["cave01_", 6], + ["cave02_", 5], + ["cave03_", 6], + ["machine01_", 6], + ["machine02_", 6], + ["ancient01_", 5], + ["ancient02_", 5], + ["ancient03_", 5], + ["boss01", 1], + ["boss02", 1], + ["boss03", 1], + ["darkfalz00", 1], + ], + [ + ["labo00_00", 1], + ["ruins01_", 3], + ["ruins02_", 3], + ["space01_", 3], + ["space02_", 3], + ["jungle01_00", 1], + ["jungle02_00", 1], + ["jungle03_00", 1], + ["jungle04_", 3], + ["jungle05_00", 1], + ["seabed01_", 3], + ["seabed02_", 3], + ["boss05", 1], + ["boss06", 1], + ["boss07", 1], + ["boss08", 1], + ["jungle06_00", 1], + ["jungle07_", 5], + ], + [ + // Don't remove this empty array, see usage of area_base_names in area_version_to_base_url. + ], + [ + ["city02_00", 1], + ["wilds01_00", 1], + ["wilds01_01", 1], + ["wilds01_02", 1], + ["wilds01_03", 1], + ["crater01_00", 1], + ["desert01_", 3], + ["desert02_", 3], + ["desert03_", 3], + ["boss09_00", 1], + ], +]; + +async function get_area_asset( + episode: number, + area_id: number, + area_variant: number, + type: "render" | "collision" +): Promise { + const base_url = area_version_to_base_url(episode, area_id, area_variant); + const suffix = type === "render" ? "n.rel" : "c.rel"; + return load_array_buffer(base_url + suffix); +} + +function area_version_to_base_url(episode: number, area_id: number, area_variant: number): string { + const episode_base_names = area_base_names[episode - 1]; + + if (0 <= area_id && area_id < episode_base_names.length) { + const [base_name, variants] = episode_base_names[area_id]; + + if (0 <= area_variant && area_variant < variants) { + let variant: string; + + if (variants === 1) { + variant = ""; + } else { + variant = String(area_variant); + while (variant.length < 2) variant = "0" + variant; + } + + return `/maps/map_${base_name}${variant}`; + } else { + throw new Error( + `Unknown variant ${area_variant} of area ${area_id} in episode ${episode}.` + ); + } + } else { + throw new Error(`Unknown episode ${episode} area ${area_id}.`); + } +} diff --git a/src/loading/entities.ts b/src/loading/entities.ts new file mode 100644 index 00000000..91918eda --- /dev/null +++ b/src/loading/entities.ts @@ -0,0 +1,237 @@ +import { Texture, CylinderBufferGeometry, BufferGeometry } from "three"; +import { ObjectType, NpcType } from "../domain"; +import Logger from "js-logger"; +import { LoadingCache } from "./LoadingCache"; +import { Endianness } from "../data_formats"; +import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor"; +import { ninja_object_to_buffer_geometry } from "../rendering/conversion/ninja_geometry"; +import { parse_nj, parse_xj } from "../data_formats/parsing/ninja"; +import { parse_xvm } from "../data_formats/parsing/ninja/texture"; +import { xvm_to_textures } from "../rendering/conversion/ninja_textures"; +import { load_array_buffer } from "./load_array_buffer"; + +const logger = Logger.get("loading/entities"); + +const DEFAULT_ENTITY = new CylinderBufferGeometry(3, 3, 20); +DEFAULT_ENTITY.translate(0, 10, 0); + +const DEFAULT_ENTITY_PROMISE: Promise = new Promise(resolve => + resolve(DEFAULT_ENTITY) +); + +const DEFAULT_ENTITY_TEX: Texture[] = []; + +const DEFAULT_ENTITY_TEX_PROMISE: Promise = new Promise(resolve => + resolve(DEFAULT_ENTITY_TEX) +); + +const npc_cache = new LoadingCache>(); +npc_cache.set(NpcType.Unknown, DEFAULT_ENTITY_PROMISE); + +const npc_tex_cache = new LoadingCache>(); +npc_tex_cache.set(NpcType.Unknown, DEFAULT_ENTITY_TEX_PROMISE); + +const object_cache = new LoadingCache>(); +const object_tex_cache = new LoadingCache>(); + +for (const type of [ + ObjectType.Unknown, + ObjectType.PlayerSet, + ObjectType.FogCollision, + ObjectType.EventCollision, + ObjectType.ObjRoomID, + ObjectType.ScriptCollision, + ObjectType.ItemLight, + ObjectType.FogCollisionSW, + ObjectType.MenuActivation, + ObjectType.BoxDetectObject, + ObjectType.SymbolChatObject, + ObjectType.TouchPlateObject, + ObjectType.TargetableObject, + ObjectType.EffectObject, + ObjectType.CountDownObject, + ObjectType.TelepipeLocation, + ObjectType.Pioneer2InvisibleTouchplate, + ObjectType.TempleMapDetect, + ObjectType.LabInvisibleObject, +]) { + object_cache.set(type, DEFAULT_ENTITY_PROMISE); + object_tex_cache.set(type, DEFAULT_ENTITY_TEX_PROMISE); +} + +export async function load_npc_geometry(npc_type: NpcType): Promise { + return npc_cache.get_or_set(npc_type, async () => { + try { + const { url, data } = await load_npc_data(npc_type, AssetType.Geometry); + const cursor = new ArrayBufferCursor(data, Endianness.Little); + const nj_objects = url.endsWith(".nj") ? parse_nj(cursor) : parse_xj(cursor); + + if (nj_objects.length) { + return ninja_object_to_buffer_geometry(nj_objects[0]); + } else { + logger.warn(`Couldn't parse ${url} for ${npc_type.code}.`); + return DEFAULT_ENTITY; + } + } catch (e) { + logger.warn(`Couldn't load geometry file for ${npc_type.code}.`, e); + return DEFAULT_ENTITY; + } + }); +} + +export async function load_npc_tex(npc_type: NpcType): Promise { + return npc_tex_cache.get_or_set(npc_type, async () => { + try { + const { data } = await load_npc_data(npc_type, AssetType.Texture); + const cursor = new ArrayBufferCursor(data, Endianness.Little); + const xvm = parse_xvm(cursor); + return xvm_to_textures(xvm); + } catch (e) { + logger.warn(`Couldn't load texture file for ${npc_type.code}.`, e); + return DEFAULT_ENTITY_TEX; + } + }); +} + +export async function load_object_geometry(object_type: ObjectType): Promise { + return object_cache.get_or_set(object_type, async () => { + try { + const { url, data } = await load_object_data(object_type, AssetType.Geometry); + const cursor = new ArrayBufferCursor(data, Endianness.Little); + const nj_objects = url.endsWith(".nj") ? parse_nj(cursor) : parse_xj(cursor); + + if (nj_objects.length) { + return ninja_object_to_buffer_geometry(nj_objects[0]); + } else { + logger.warn(`Couldn't parse ${url} for ${object_type.name}.`); + return DEFAULT_ENTITY; + } + } catch (e) { + logger.warn(`Couldn't load geometry file for ${object_type.name}.`, e); + return DEFAULT_ENTITY; + } + }); +} + +export async function load_object_tex(object_type: ObjectType): Promise { + return object_tex_cache.get_or_set(object_type, async () => { + try { + const { data } = await load_object_data(object_type, AssetType.Texture); + const cursor = new ArrayBufferCursor(data, Endianness.Little); + const xvm = parse_xvm(cursor); + return xvm_to_textures(xvm); + } catch (e) { + logger.warn(`Couldn't load texture file for ${object_type.name}.`, e); + return DEFAULT_ENTITY_TEX; + } + }); +} + +export async function load_npc_data( + npc_type: NpcType, + type: AssetType +): Promise<{ url: string; data: ArrayBuffer }> { + const url = npc_type_to_url(npc_type, type); + const data = await load_array_buffer(url); + return { url, data }; +} + +export async function load_object_data( + object_type: ObjectType, + type: AssetType +): Promise<{ url: string; data: ArrayBuffer }> { + const url = object_type_to_url(object_type, type); + const data = await load_array_buffer(url); + return { url, data }; +} + +enum AssetType { + Geometry, + Texture, +} + +function npc_type_to_url(npc_type: NpcType, type: AssetType): string { + switch (npc_type) { + // The dubswitch model is in XJ format. + case NpcType.Dubswitch: + return `/npcs/${npc_type.code}.${type === AssetType.Geometry ? "xj" : "xvm"}`; + + // Episode II VR Temple + + case NpcType.Hildebear2: + return npc_type_to_url(NpcType.Hildebear, type); + case NpcType.Hildeblue2: + return npc_type_to_url(NpcType.Hildeblue, type); + case NpcType.RagRappy2: + return npc_type_to_url(NpcType.RagRappy, type); + case NpcType.Monest2: + return npc_type_to_url(NpcType.Monest, type); + case NpcType.PoisonLily2: + return npc_type_to_url(NpcType.PoisonLily, type); + case NpcType.NarLily2: + return npc_type_to_url(NpcType.NarLily, type); + case NpcType.GrassAssassin2: + return npc_type_to_url(NpcType.GrassAssassin, type); + case NpcType.Dimenian2: + return npc_type_to_url(NpcType.Dimenian, type); + case NpcType.LaDimenian2: + return npc_type_to_url(NpcType.LaDimenian, type); + case NpcType.SoDimenian2: + return npc_type_to_url(NpcType.SoDimenian, type); + case NpcType.DarkBelra2: + return npc_type_to_url(NpcType.DarkBelra, type); + + // Episode II VR Spaceship + + case NpcType.SavageWolf2: + return npc_type_to_url(NpcType.SavageWolf, type); + case NpcType.BarbarousWolf2: + return npc_type_to_url(NpcType.BarbarousWolf, type); + case NpcType.PanArms2: + return npc_type_to_url(NpcType.PanArms, type); + case NpcType.Dubchic2: + return npc_type_to_url(NpcType.Dubchic, type); + case NpcType.Gilchic2: + return npc_type_to_url(NpcType.Gilchic, type); + case NpcType.Garanz2: + return npc_type_to_url(NpcType.Garanz, type); + case NpcType.Dubswitch2: + return npc_type_to_url(NpcType.Dubswitch, type); + case NpcType.Delsaber2: + return npc_type_to_url(NpcType.Delsaber, type); + case NpcType.ChaosSorcerer2: + return npc_type_to_url(NpcType.ChaosSorcerer, type); + + default: + return `/npcs/${npc_type.code}.${type === AssetType.Geometry ? "nj" : "xvm"}`; + } +} + +function object_type_to_url(object_type: ObjectType, type: AssetType): string { + if (type === AssetType.Geometry) { + switch (object_type) { + case ObjectType.EasterEgg: + case ObjectType.ChristmasTree: + case ObjectType.ChristmasWreath: + case ObjectType.TwentyFirstCentury: + case ObjectType.Sonic: + case ObjectType.WelcomeBoard: + case ObjectType.FloatingJelifish: + case ObjectType.RuinsSeal: + case ObjectType.Dolphin: + case ObjectType.Cacti: + case ObjectType.BigBrownRock: + case ObjectType.PoisonPlant: + case ObjectType.BigBlackRocks: + case ObjectType.FallingRock: + case ObjectType.DesertFixedTypeBoxBreakableCrystals: + case ObjectType.BeeHive: + return `/objects/${object_type.pso_id}.nj`; + + default: + return `/objects/${object_type.pso_id}.xj`; + } + } else { + return `/objects/${object_type.pso_id}.xvm`; + } +} diff --git a/src/loading/load_array_buffer.ts b/src/loading/load_array_buffer.ts new file mode 100644 index 00000000..577da31c --- /dev/null +++ b/src/loading/load_array_buffer.ts @@ -0,0 +1,5 @@ +export async function load_array_buffer(url: string): Promise { + const base_url = process.env.PUBLIC_URL; + const response = await fetch(base_url + url); + return response.arrayBuffer(); +} diff --git a/src/loading/player.ts b/src/loading/player.ts new file mode 100644 index 00000000..20057989 --- /dev/null +++ b/src/loading/player.ts @@ -0,0 +1,19 @@ +import { load_array_buffer } from "./load_array_buffer"; + +export async function get_player_data( + player_class: string, + body_part: string, + no?: number +): Promise { + return await load_array_buffer(player_class_to_url(player_class, body_part, no)); +} + +export async function get_player_animation_data(animation_id: number): Promise { + return await load_array_buffer( + `/player/animation/animation_${animation_id.toString().padStart(3, "0")}.njm` + ); +} + +function player_class_to_url(player_class: string, body_part: string, no?: number): string { + return `/player/${player_class}${body_part}${no == null ? "" : no}.nj`; +} diff --git a/src/rendering/EntityControls.ts b/src/rendering/EntityControls.ts new file mode 100644 index 00000000..08d6af5c --- /dev/null +++ b/src/rendering/EntityControls.ts @@ -0,0 +1,311 @@ +import { runInAction } from "mobx"; +import { Intersection, Mesh, MeshLambertMaterial, Plane, Raycaster, Vector2, Vector3 } from "three"; +import { Vec3 } from "../data_formats/vector"; +import { QuestEntity, QuestNpc, Section } from "../domain"; +import { quest_editor_store } from "../stores/QuestEditorStore"; +import { + NPC_COLOR, + NPC_HOVER_COLOR, + NPC_SELECTED_COLOR, + OBJECT_COLOR, + OBJECT_HOVER_COLOR, + OBJECT_SELECTED_COLOR, +} from "./conversion/entities"; +import { QuestRenderer } from "./QuestRenderer"; + +type PickEntityResult = { + object: Mesh; + entity: QuestEntity; + grab_offset: Vector3; + drag_adjust: Vector3; + drag_y: number; + manipulating: boolean; +}; + +type EntityUserData = { + entity: QuestEntity; +}; + +export class EntityControls { + private raycaster = new Raycaster(); + private hovered_data?: PickEntityResult; + private selected_data?: PickEntityResult; + private pointer_pos = new Vector2(0, 0); + + constructor(private renderer: QuestRenderer) {} + + on_mouse_down = (e: MouseEvent) => { + const old_selected_data = this.selected_data; + this.renderer.pointer_pos_to_device_coords(e, this.pointer_pos); + const data = this.pick_entity(this.pointer_pos); + + // Did we pick a different object than the previously hovered over 3D object? + if (this.hovered_data && (!data || data.object !== this.hovered_data.object)) { + const color = this.get_color(this.hovered_data.entity, "hover"); + + for (const material of this.hovered_data.object.material as MeshLambertMaterial[]) { + material.color.set(color); + } + } + + // Did we pick a different object than the previously selected 3D object? + if (this.selected_data && (!data || data.object !== this.selected_data.object)) { + const color = this.get_color(this.selected_data.entity, "normal"); + + for (const material of this.selected_data.object.material as MeshLambertMaterial[]) { + if (material.map) { + material.color.set(0xffffff); + } else { + material.color.set(color); + } + } + + this.selected_data.manipulating = false; + } + + if (data) { + // User selected an entity. + const color = this.get_color(data.entity, "selected"); + + for (const material of data.object.material as MeshLambertMaterial[]) { + material.color.set(color); + } + + data.manipulating = true; + this.hovered_data = data; + this.selected_data = data; + this.renderer.controls.enabled = false; + } else { + // User clicked on terrain or outside of area. + this.hovered_data = undefined; + this.selected_data = undefined; + this.renderer.controls.enabled = true; + } + + const selection_changed = + old_selected_data && data + ? old_selected_data.object !== data.object + : old_selected_data !== data; + + if (selection_changed) { + quest_editor_store.set_selected_entity(data && data.entity); + this.renderer.schedule_render(); + } + }; + + on_mouse_up = () => { + if (this.selected_data) { + this.selected_data.manipulating = false; + this.renderer.controls.enabled = true; + this.renderer.schedule_render(); + } + }; + + on_mouse_move = (e: MouseEvent) => { + this.renderer.pointer_pos_to_device_coords(e, this.pointer_pos); + + if (this.selected_data && this.selected_data.manipulating) { + if (e.buttons === 1) { + // User is dragging a selected entity. + const data = this.selected_data; + + if (e.shiftKey) { + // Vertical movement. + // We intersect with a plane that's oriented toward the camera and that's coplanar with the point where the entity was grabbed. + this.raycaster.setFromCamera(this.pointer_pos, this.renderer.camera); + const ray = this.raycaster.ray; + const negative_world_dir = this.renderer.camera + .getWorldDirection(new Vector3()) + .negate(); + const plane = new Plane().setFromNormalAndCoplanarPoint( + new Vector3(negative_world_dir.x, 0, negative_world_dir.z).normalize(), + data.object.position.sub(data.grab_offset) + ); + const intersection_point = new Vector3(); + + if (ray.intersectPlane(plane, intersection_point)) { + const y = intersection_point.y + data.grab_offset.y; + const y_delta = y - data.entity.position.y; + data.drag_y += y_delta; + data.drag_adjust.y -= y_delta; + data.entity.position = new Vec3( + data.entity.position.x, + y, + data.entity.position.z + ); + } + } else { + // Horizontal movement accross terrain. + // Cast ray adjusted for dragging entities. + const { intersection, section } = this.pick_terrain(this.pointer_pos, data); + + if (intersection) { + runInAction(() => { + data.entity.position = new Vec3( + intersection.point.x, + intersection.point.y + data.drag_y, + intersection.point.z + ); + data.entity.section = section; + }); + } else { + // If the cursor is not over any terrain, we translate the entity accross the horizontal plane in which the entity's origin lies. + this.raycaster.setFromCamera(this.pointer_pos, this.renderer.camera); + const ray = this.raycaster.ray; + // ray.origin.add(data.dragAdjust); + const plane = new Plane( + new Vector3(0, 1, 0), + -data.entity.position.y + data.grab_offset.y + ); + const intersection_point = new Vector3(); + + if (ray.intersectPlane(plane, intersection_point)) { + data.entity.position = new Vec3( + intersection_point.x + data.grab_offset.x, + data.entity.position.y, + intersection_point.z + data.grab_offset.z + ); + } + } + } + } + + this.renderer.schedule_render(); + } else { + // User is hovering. + const old_data = this.hovered_data; + const data = this.pick_entity(this.pointer_pos); + + if (old_data && (!data || data.object !== old_data.object)) { + if (!this.selected_data || old_data.object !== this.selected_data.object) { + const color = this.get_color(old_data.entity, "normal"); + + for (const material of old_data.object.material as MeshLambertMaterial[]) { + if (material.map) { + material.color.set(0xffffff); + } else { + material.color.set(color); + } + } + } + + this.hovered_data = undefined; + this.renderer.schedule_render(); + } + + if (data && (!old_data || data.object !== old_data.object)) { + if (!this.selected_data || data.object !== this.selected_data.object) { + const color = this.get_color(data.entity, "hover"); + + for (const material of data.object.material as MeshLambertMaterial[]) { + material.color.set(color); + } + } + + this.hovered_data = data; + this.renderer.schedule_render(); + } + } + }; + + /** + * @param pointer_pos - pointer coordinates in normalized device space + */ + private pick_entity(pointer_pos: Vector2): PickEntityResult | undefined { + // Find the nearest object and NPC under the pointer. + this.raycaster.setFromCamera(pointer_pos, this.renderer.camera); + const [nearest_object] = this.raycaster.intersectObjects( + this.renderer.obj_geometry.children + ); + const [nearest_npc] = this.raycaster.intersectObjects(this.renderer.npc_geometry.children); + + if (!nearest_object && !nearest_npc) { + return; + } + + const object_dist = nearest_object ? nearest_object.distance : Infinity; + const npc_dist = nearest_npc ? nearest_npc.distance : Infinity; + const intersection = object_dist < npc_dist ? nearest_object : nearest_npc; + + const entity = (intersection.object.userData as EntityUserData).entity; + // Vector that points from the grabbing point to the model's origin. + const grab_offset = intersection.object.position.clone().sub(intersection.point); + // Vector that points from the grabbing point to the terrain point directly under the model's origin. + const drag_adjust = grab_offset.clone(); + // Distance to terrain. + let drag_y = 0; + + // Find vertical distance to terrain. + this.raycaster.set(intersection.object.position, new Vector3(0, -1, 0)); + const [terrain] = this.raycaster.intersectObjects( + this.renderer.collision_geometry.children, + true + ); + + if (terrain) { + drag_adjust.sub(new Vector3(0, terrain.distance, 0)); + drag_y += terrain.distance; + } + + return { + object: intersection.object as Mesh, + entity, + grab_offset, + drag_adjust, + drag_y, + manipulating: false, + }; + } + + /** + * @param pointer_pos - pointer coordinates in normalized device space + */ + private pick_terrain( + pointer_pos: Vector2, + data: PickEntityResult + ): { + intersection?: Intersection; + section?: Section; + } { + this.raycaster.setFromCamera(pointer_pos, this.renderer.camera); + this.raycaster.ray.origin.add(data.drag_adjust); + const terrains = this.raycaster.intersectObjects( + this.renderer.collision_geometry.children, + true + ); + + // Don't allow entities to be placed on very steep terrain. + // E.g. walls. + // TODO: make use of the flags field in the collision data. + for (const terrain of terrains) { + if (terrain.face!.normal.y > 0.75) { + // Find section ID. + this.raycaster.set(terrain.point.clone().setY(1000), new Vector3(0, -1, 0)); + const render_terrains = this.raycaster + .intersectObjects(this.renderer.render_geometry.children, true) + .filter(rt => rt.object.userData.section.id >= 0); + + return { + intersection: terrain, + section: render_terrains[0] && render_terrains[0].object.userData.section, + }; + } + } + + return {}; + } + + private get_color(entity: QuestEntity, type: "normal" | "hover" | "selected"): number { + const is_npc = entity instanceof QuestNpc; + + switch (type) { + default: + case "normal": + return is_npc ? NPC_COLOR : OBJECT_COLOR; + case "hover": + return is_npc ? NPC_HOVER_COLOR : OBJECT_HOVER_COLOR; + case "selected": + return is_npc ? NPC_SELECTED_COLOR : OBJECT_SELECTED_COLOR; + } + } +} diff --git a/src/rendering/QuestModelManager.ts b/src/rendering/QuestModelManager.ts new file mode 100644 index 00000000..90f4d452 --- /dev/null +++ b/src/rendering/QuestModelManager.ts @@ -0,0 +1,128 @@ +import { QuestRenderer } from "./QuestRenderer"; +import { Quest, Area, QuestEntity } from "../domain"; +import { IReactionDisposer, autorun } from "mobx"; +import { Object3D, Group, Vector3 } from "three"; +import { load_area_collision_geometry, load_area_render_geometry } from "../loading/areas"; +import { + load_object_geometry, + load_object_tex as load_object_textures, + load_npc_geometry, + load_npc_tex as load_npc_textures, +} from "../loading/entities"; +import { create_object_mesh, create_npc_mesh } from "./conversion/entities"; +import Logger from "js-logger"; + +const logger = Logger.get("rendering/QuestModelManager"); + +const CAMERA_POSITION = new Vector3(0, 800, 700); +const CAMERA_LOOKAT = new Vector3(0, 0, 0); +const DUMMY_OBJECT = new Object3D(); + +export class QuestModelManager { + private quest?: Quest; + private area?: Area; + private entity_reaction_disposers: IReactionDisposer[] = []; + + constructor(private renderer: QuestRenderer) {} + + async load_models(quest?: Quest, area?: Area): Promise { + if (this.quest === quest && this.area === area) { + return; + } + + this.quest = quest; + this.area = area; + + this.dispose_entity_reactions(); + + if (quest && area) { + try { + // Load necessary area geometry. + const episode = quest.episode; + const area_id = area.id; + const variant = quest.area_variants.find(v => v.area.id === area_id); + const variant_id = (variant && variant.id) || 0; + + const collision_geometry = await load_area_collision_geometry( + episode, + area_id, + variant_id + ); + + const render_geometry = await load_area_render_geometry( + episode, + area_id, + variant_id + ); + + if (this.quest !== quest || this.area !== area) return; + + this.renderer.collision_geometry = collision_geometry; + this.renderer.render_geometry = render_geometry; + + this.renderer.reset_camera(CAMERA_POSITION, CAMERA_LOOKAT); + + // Load entity models. + const npc_group = new Group(); + const obj_group = new Group(); + this.renderer.npc_geometry = npc_group; + this.renderer.obj_geometry = obj_group; + + for (const npc of quest.npcs) { + if (npc.area_id === area.id) { + const npc_geom = await load_npc_geometry(npc.type); + const npc_tex = await load_npc_textures(npc.type); + + if (this.quest !== quest || this.area !== area) return; + + const model = create_npc_mesh(npc, npc_geom, npc_tex); + this.update_entity_geometry(npc, npc_group, model); + } + } + + for (const object of quest.objects) { + if (object.area_id === area.id) { + const object_geom = await load_object_geometry(object.type); + const object_tex = await load_object_textures(object.type); + + if (this.quest !== quest || this.area !== area) return; + + const model = create_object_mesh(object, object_geom, object_tex); + this.update_entity_geometry(object, obj_group, model); + } + } + } catch (e) { + logger.error(`Couldn't load models for quest ${quest.id}, ${area.name}.`, e); + this.renderer.collision_geometry = DUMMY_OBJECT; + this.renderer.render_geometry = DUMMY_OBJECT; + this.renderer.obj_geometry = DUMMY_OBJECT; + this.renderer.npc_geometry = DUMMY_OBJECT; + } + } else { + this.renderer.collision_geometry = DUMMY_OBJECT; + this.renderer.render_geometry = DUMMY_OBJECT; + this.renderer.obj_geometry = DUMMY_OBJECT; + this.renderer.npc_geometry = DUMMY_OBJECT; + } + } + + private update_entity_geometry(entity: QuestEntity, group: Group, model: Object3D): void { + group.add(model); + + this.entity_reaction_disposers.push( + autorun(() => { + const { x, y, z } = entity.position; + model.position.set(x, y, z); + const rot = entity.rotation; + model.rotation.set(rot.x, rot.y, rot.z); + this.renderer.schedule_render(); + }) + ); + } + + private dispose_entity_reactions(): void { + for (const disposer of this.entity_reaction_disposers) { + disposer(); + } + } +} diff --git a/src/rendering/QuestRenderer.ts b/src/rendering/QuestRenderer.ts index d86dbb80..907a7043 100644 --- a/src/rendering/QuestRenderer.ts +++ b/src/rendering/QuestRenderer.ts @@ -1,27 +1,8 @@ -import { autorun, IReactionDisposer, when, runInAction } from "mobx"; -import { - Intersection, - Mesh, - MeshLambertMaterial, - Object3D, - Plane, - Raycaster, - Vector2, - Vector3, - PerspectiveCamera, -} from "three"; -import { Vec3 } from "../data_formats/vector"; -import { Area, Quest, QuestEntity, QuestNpc, Section } from "../domain"; -import { area_store } from "../stores/AreaStore"; +import { autorun } from "mobx"; +import { Object3D, PerspectiveCamera } from "three"; import { quest_editor_store } from "../stores/QuestEditorStore"; -import { - NPC_COLOR, - NPC_HOVER_COLOR, - NPC_SELECTED_COLOR, - OBJECT_COLOR, - OBJECT_HOVER_COLOR, - OBJECT_SELECTED_COLOR, -} from "./entities"; +import { EntityControls } from "./EntityControls"; +import { QuestModelManager } from "./QuestModelManager"; import { Renderer } from "./Renderer"; let renderer: QuestRenderer | undefined; @@ -31,56 +12,72 @@ export function get_quest_renderer(): QuestRenderer { return renderer; } -type PickEntityResult = { - object: Mesh; - entity: QuestEntity; - grab_offset: Vector3; - drag_adjust: Vector3; - drag_y: number; - manipulating: boolean; -}; - -type EntityUserData = { - entity: QuestEntity; -}; - export class QuestRenderer extends Renderer { - private raycaster = new Raycaster(); + private _collision_geometry = new Object3D(); - private quest?: Quest; - private area?: Area; - private entity_reaction_disposers: IReactionDisposer[] = []; + get collision_geometry(): Object3D { + return this._collision_geometry; + } - private collision_geometry = new Object3D(); - private render_geometry = new Object3D(); - private obj_geometry = new Object3D(); - private npc_geometry = new Object3D(); + set collision_geometry(collision_geometry: Object3D) { + this.scene.remove(this.collision_geometry); + this._collision_geometry = collision_geometry; + this.scene.add(collision_geometry); + } - private hovered_data?: PickEntityResult; - private selected_data?: PickEntityResult; + private _render_geometry = new Object3D(); + + get render_geometry(): Object3D { + return this._render_geometry; + } + + set render_geometry(render_geometry: Object3D) { + // this.scene.remove(this._render_geometry); + this._render_geometry = render_geometry; + // this.scene.add(render_geometry); + } + + private _obj_geometry = new Object3D(); + + get obj_geometry(): Object3D { + return this._obj_geometry; + } + + set obj_geometry(obj_geometry: Object3D) { + this.scene.remove(this._obj_geometry); + this._obj_geometry = obj_geometry; + this.scene.add(obj_geometry); + } + + private _npc_geometry = new Object3D(); + + get npc_geometry(): Object3D { + return this._npc_geometry; + } + + set npc_geometry(npc_geometry: Object3D) { + this.scene.remove(this._npc_geometry); + this._npc_geometry = npc_geometry; + this.scene.add(npc_geometry); + } constructor() { super(new PerspectiveCamera(60, 1, 10, 10000)); - this.dom_element.addEventListener("mousedown", this.on_mouse_down); - this.dom_element.addEventListener("mouseup", this.on_mouse_up); - this.dom_element.addEventListener("mousemove", this.on_mouse_move); - - this.scene.add(this.obj_geometry); - this.scene.add(this.npc_geometry); + const model_manager = new QuestModelManager(this); autorun(() => { - this.set_quest_and_area( + model_manager.load_models( quest_editor_store.current_quest, quest_editor_store.current_area ); }); - } - set_quest_and_area(quest?: Quest, area?: Area): void { - this.area = area; - this.quest = quest; - this.update_geometry(); + const entity_controls = new EntityControls(this); + + this.dom_element.addEventListener("mousedown", entity_controls.on_mouse_down); + this.dom_element.addEventListener("mouseup", entity_controls.on_mouse_up); + this.dom_element.addEventListener("mousemove", entity_controls.on_mouse_move); } set_size(width: number, height: number): void { @@ -88,356 +85,4 @@ export class QuestRenderer extends Renderer { this.camera.updateProjectionMatrix(); super.set_size(width, height); } - - private async update_geometry(): Promise { - this.dispose_entity_reactions(); - - this.scene.remove(this.obj_geometry); - this.scene.remove(this.npc_geometry); - this.obj_geometry = new Object3D(); - this.npc_geometry = new Object3D(); - this.scene.add(this.obj_geometry); - this.scene.add(this.npc_geometry); - - this.scene.remove(this.collision_geometry); - // this.scene.remove(this.render_geometry); - - if (this.quest && this.area) { - // Add necessary entity geometry when it arrives. - for (const obj of this.quest.objects) { - this.update_entity_geometry(obj, this.obj_geometry); - } - - for (const npc of this.quest.npcs) { - this.update_entity_geometry(npc, this.npc_geometry); - } - - // Load necessary area geometry. - const episode = this.quest.episode; - const area_id = this.area.id; - const variant = this.quest.area_variants.find(v => v.area.id === area_id); - const variant_id = (variant && variant.id) || 0; - - const collision_geometry = await area_store.get_area_collision_geometry( - episode, - area_id, - variant_id - ); - - this.scene.remove(this.collision_geometry); - // this.scene.remove(this.render_geometry); - - this.reset_camera(new Vector3(0, 800, 700), new Vector3(0, 0, 0)); - - this.collision_geometry = collision_geometry; - this.scene.add(collision_geometry); - - const render_geometry = await area_store.get_area_render_geometry( - episode, - area_id, - variant_id - ); - - this.render_geometry = render_geometry; - // this.scene.add(render_geometry); - } - } - - private update_entity_geometry(entity: QuestEntity, entity_geometry: Object3D): void { - if (this.area && entity.area_id === this.area.id) { - this.entity_reaction_disposers.push( - when( - () => entity.object_3d != undefined, - () => { - const object_3d = entity.object_3d!; - entity_geometry.add(object_3d); - - this.entity_reaction_disposers.push( - autorun(() => { - const { x, y, z } = entity.position; - object_3d.position.set(x, y, z); - const rot = entity.rotation; - object_3d.rotation.set(rot.x, rot.y, rot.z); - this.schedule_render(); - }) - ); - - this.schedule_render(); - } - ) - ); - } - } - - private dispose_entity_reactions(): void { - for (const disposer of this.entity_reaction_disposers) { - disposer(); - } - } - - private on_mouse_down = (e: MouseEvent) => { - const old_selected_data = this.selected_data; - const data = this.pick_entity(this.pointer_pos_to_device_coords(e)); - - // Did we pick a different object than the previously hovered over 3D object? - if (this.hovered_data && (!data || data.object !== this.hovered_data.object)) { - const color = this.get_color(this.hovered_data.entity, "hover"); - - for (const material of this.hovered_data.object.material as MeshLambertMaterial[]) { - material.color.set(color); - } - } - - // Did we pick a different object than the previously selected 3D object? - if (this.selected_data && (!data || data.object !== this.selected_data.object)) { - const color = this.get_color(this.selected_data.entity, "normal"); - - for (const material of this.selected_data.object.material as MeshLambertMaterial[]) { - if (material.map) { - material.color.set(0xffffff); - } else { - material.color.set(color); - } - } - - this.selected_data.manipulating = false; - } - - if (data) { - // User selected an entity. - const color = this.get_color(data.entity, "selected"); - - for (const material of data.object.material as MeshLambertMaterial[]) { - material.color.set(color); - } - - data.manipulating = true; - this.hovered_data = data; - this.selected_data = data; - this.controls.enabled = false; - } else { - // User clicked on terrain or outside of area. - this.hovered_data = undefined; - this.selected_data = undefined; - this.controls.enabled = true; - } - - const selection_changed = - old_selected_data && data - ? old_selected_data.object !== data.object - : old_selected_data !== data; - - if (selection_changed) { - quest_editor_store.set_selected_entity(data && data.entity); - this.schedule_render(); - } - }; - - private on_mouse_up = () => { - if (this.selected_data) { - this.selected_data.manipulating = false; - this.controls.enabled = true; - this.schedule_render(); - } - }; - - private on_mouse_move = (e: MouseEvent) => { - const pointer_pos = this.pointer_pos_to_device_coords(e); - - if (this.selected_data && this.selected_data.manipulating) { - if (e.buttons === 1) { - // User is dragging a selected entity. - const data = this.selected_data; - - if (e.shiftKey) { - // Vertical movement. - // We intersect with a plane that's oriented toward the camera and that's coplanar with the point where the entity was grabbed. - this.raycaster.setFromCamera(pointer_pos, this.camera); - const ray = this.raycaster.ray; - const negative_world_dir = this.camera - .getWorldDirection(new Vector3()) - .negate(); - const plane = new Plane().setFromNormalAndCoplanarPoint( - new Vector3(negative_world_dir.x, 0, negative_world_dir.z).normalize(), - data.object.position.sub(data.grab_offset) - ); - const intersection_point = new Vector3(); - - if (ray.intersectPlane(plane, intersection_point)) { - const y = intersection_point.y + data.grab_offset.y; - const y_delta = y - data.entity.position.y; - data.drag_y += y_delta; - data.drag_adjust.y -= y_delta; - data.entity.position = new Vec3( - data.entity.position.x, - y, - data.entity.position.z - ); - } - } else { - // Horizontal movement accross terrain. - // Cast ray adjusted for dragging entities. - const { intersection, section } = this.pick_terrain(pointer_pos, data); - - if (intersection) { - runInAction(() => { - data.entity.position = new Vec3( - intersection.point.x, - intersection.point.y + data.drag_y, - intersection.point.z - ); - data.entity.section = section; - }); - } else { - // If the cursor is not over any terrain, we translate the entity accross the horizontal plane in which the entity's origin lies. - this.raycaster.setFromCamera(pointer_pos, this.camera); - const ray = this.raycaster.ray; - // ray.origin.add(data.dragAdjust); - const plane = new Plane( - new Vector3(0, 1, 0), - -data.entity.position.y + data.grab_offset.y - ); - const intersection_point = new Vector3(); - - if (ray.intersectPlane(plane, intersection_point)) { - data.entity.position = new Vec3( - intersection_point.x + data.grab_offset.x, - data.entity.position.y, - intersection_point.z + data.grab_offset.z - ); - } - } - } - } - - this.schedule_render(); - } else { - // User is hovering. - const old_data = this.hovered_data; - const data = this.pick_entity(pointer_pos); - - if (old_data && (!data || data.object !== old_data.object)) { - if (!this.selected_data || old_data.object !== this.selected_data.object) { - const color = this.get_color(old_data.entity, "normal"); - - for (const material of old_data.object.material as MeshLambertMaterial[]) { - if (material.map) { - material.color.set(0xffffff); - } else { - material.color.set(color); - } - } - } - - this.hovered_data = undefined; - this.schedule_render(); - } - - if (data && (!old_data || data.object !== old_data.object)) { - if (!this.selected_data || data.object !== this.selected_data.object) { - const color = this.get_color(data.entity, "hover"); - - for (const material of data.object.material as MeshLambertMaterial[]) { - material.color.set(color); - } - } - - this.hovered_data = data; - this.schedule_render(); - } - } - }; - - /** - * @param pointer_pos - pointer coordinates in normalized device space - */ - private pick_entity(pointer_pos: Vector2): PickEntityResult | undefined { - // Find the nearest object and NPC under the pointer. - this.raycaster.setFromCamera(pointer_pos, this.camera); - const [nearest_object] = this.raycaster.intersectObjects(this.obj_geometry.children); - const [nearest_npc] = this.raycaster.intersectObjects(this.npc_geometry.children); - - if (!nearest_object && !nearest_npc) { - return; - } - - const object_dist = nearest_object ? nearest_object.distance : Infinity; - const npc_dist = nearest_npc ? nearest_npc.distance : Infinity; - const intersection = object_dist < npc_dist ? nearest_object : nearest_npc; - - const entity = (intersection.object.userData as EntityUserData).entity; - // Vector that points from the grabbing point to the model's origin. - const grab_offset = intersection.object.position.clone().sub(intersection.point); - // Vector that points from the grabbing point to the terrain point directly under the model's origin. - const drag_adjust = grab_offset.clone(); - // Distance to terrain. - let drag_y = 0; - - // Find vertical distance to terrain. - this.raycaster.set(intersection.object.position, new Vector3(0, -1, 0)); - const [terrain] = this.raycaster.intersectObjects(this.collision_geometry.children, true); - - if (terrain) { - drag_adjust.sub(new Vector3(0, terrain.distance, 0)); - drag_y += terrain.distance; - } - - return { - object: intersection.object as Mesh, - entity, - grab_offset, - drag_adjust, - drag_y, - manipulating: false, - }; - } - - /** - * @param pointer_pos - pointer coordinates in normalized device space - */ - private pick_terrain( - pointer_pos: Vector2, - data: PickEntityResult - ): { - intersection?: Intersection; - section?: Section; - } { - this.raycaster.setFromCamera(pointer_pos, this.camera); - this.raycaster.ray.origin.add(data.drag_adjust); - const terrains = this.raycaster.intersectObjects(this.collision_geometry.children, true); - - // Don't allow entities to be placed on very steep terrain. - // E.g. walls. - // TODO: make use of the flags field in the collision data. - for (const terrain of terrains) { - if (terrain.face!.normal.y > 0.75) { - // Find section ID. - this.raycaster.set(terrain.point.clone().setY(1000), new Vector3(0, -1, 0)); - const render_terrains = this.raycaster - .intersectObjects(this.render_geometry.children, true) - .filter(rt => rt.object.userData.section.id >= 0); - - return { - intersection: terrain, - section: render_terrains[0] && render_terrains[0].object.userData.section, - }; - } - } - - return {}; - } - - private get_color(entity: QuestEntity, type: "normal" | "hover" | "selected"): number { - const is_npc = entity instanceof QuestNpc; - - switch (type) { - default: - case "normal": - return is_npc ? NPC_COLOR : OBJECT_COLOR; - case "hover": - return is_npc ? NPC_HOVER_COLOR : OBJECT_HOVER_COLOR; - case "selected": - return is_npc ? NPC_SELECTED_COLOR : OBJECT_SELECTED_COLOR; - } - } } diff --git a/src/rendering/Renderer.ts b/src/rendering/Renderer.ts index 540beba9..6781f0b4 100644 --- a/src/rendering/Renderer.ts +++ b/src/rendering/Renderer.ts @@ -15,29 +15,28 @@ import OrbitControlsCreator from "three-orbit-controls"; const OrbitControls = OrbitControlsCreator(THREE); export class Renderer { - protected camera: C; - protected controls: any; - protected scene = new Scene(); - protected light_holder = new Group(); + readonly camera: C; + readonly controls: any; + readonly scene = new Scene(); + readonly light_holder = new Group(); private renderer = new WebGLRenderer({ antialias: true }); private render_scheduled = false; private light = new HemisphereLight(0xffffff, 0x505050, 1.2); constructor(camera: C) { - this.renderer.setPixelRatio(window.devicePixelRatio); - this.camera = camera; - this.controls = new OrbitControls(this.camera, this.renderer.domElement); + this.controls = new OrbitControls(camera, this.dom_element); this.controls.mouseButtons.ORBIT = MOUSE.RIGHT; this.controls.mouseButtons.PAN = MOUSE.LEFT; this.controls.addEventListener("change", this.schedule_render); this.scene.background = new Color(0x151c21); - this.light_holder.add(this.light); this.scene.add(this.light_holder); + + this.renderer.setPixelRatio(window.devicePixelRatio); } get dom_element(): HTMLElement { @@ -49,29 +48,27 @@ export class Renderer { this.schedule_render(); } - protected schedule_render = () => { + pointer_pos_to_device_coords(e: MouseEvent, coords: Vector2): void { + this.renderer.getSize(coords); + coords.width = (e.offsetX / coords.width) * 2 - 1; + coords.height = (e.offsetY / coords.height) * -2 + 1; + } + + schedule_render = () => { if (!this.render_scheduled) { this.render_scheduled = true; requestAnimationFrame(this.call_render); } }; - protected render(): void { - this.renderer.render(this.scene, this.camera); - } - - protected reset_camera(position: Vector3, look_at: Vector3): void { + reset_camera(position: Vector3, look_at: Vector3): void { this.controls.reset(); this.camera.position.copy(position); this.camera.lookAt(look_at); } - protected pointer_pos_to_device_coords(e: MouseEvent): Vector2 { - const coords = new Vector2(); - this.renderer.getSize(coords); - coords.width = (e.offsetX / coords.width) * 2 - 1; - coords.height = (e.offsetY / coords.height) * -2 + 1; - return coords; + protected render(): void { + this.renderer.render(this.scene, this.camera); } private call_render = () => { diff --git a/src/rendering/TextureRenderer.ts b/src/rendering/TextureRenderer.ts index 1282b559..1641a130 100644 --- a/src/rendering/TextureRenderer.ts +++ b/src/rendering/TextureRenderer.ts @@ -12,7 +12,7 @@ import { import { Xvm } from "../data_formats/parsing/ninja/texture"; import { texture_viewer_store } from "../stores/TextureViewerStore"; import { Renderer } from "./Renderer"; -import { xvm_texture_to_texture } from "./textures"; +import { xvm_texture_to_texture } from "./conversion/ninja_textures"; const logger = Logger.get("rendering/TextureRenderer"); diff --git a/src/rendering/areas.ts b/src/rendering/conversion/areas.ts similarity index 94% rename from src/rendering/areas.ts rename to src/rendering/conversion/areas.ts index 1fde1d1f..fe14d88d 100644 --- a/src/rendering/areas.ts +++ b/src/rendering/conversion/areas.ts @@ -13,10 +13,10 @@ import { Uint16BufferAttribute, Vector3, } from "three"; -import { CollisionObject } from "../data_formats/parsing/area_collision_geometry"; -import { RenderObject } from "../data_formats/parsing/area_geometry"; -import { Section } from "../domain"; -import { xj_model_to_geometry } from "./xj_model_to_geometry"; +import { CollisionObject } from "../../data_formats/parsing/area_collision_geometry"; +import { RenderObject } from "../../data_formats/parsing/area_geometry"; +import { Section } from "../../domain"; +import { xj_model_to_geometry } from "./xj_models"; const materials = [ // Wall diff --git a/src/rendering/entities.ts b/src/rendering/conversion/entities.ts similarity index 96% rename from src/rendering/entities.ts rename to src/rendering/conversion/entities.ts index 5b21f6bb..2f283e2a 100644 --- a/src/rendering/entities.ts +++ b/src/rendering/conversion/entities.ts @@ -7,7 +7,7 @@ import { Texture, Material, } from "three"; -import { QuestEntity, QuestNpc, QuestObject } from "../domain"; +import { QuestEntity, QuestNpc, QuestObject } from "../../domain"; export const OBJECT_COLOR = 0xffff00; export const OBJECT_HOVER_COLOR = 0xffdf3f; diff --git a/src/rendering/index.ts b/src/rendering/conversion/index.ts similarity index 71% rename from src/rendering/index.ts rename to src/rendering/conversion/index.ts index 4f7dab23..7edbd0b7 100644 --- a/src/rendering/index.ts +++ b/src/rendering/conversion/index.ts @@ -1,4 +1,4 @@ -import { Vec3 } from "../data_formats/vector"; +import { Vec3 } from "../../data_formats/vector"; import { Vector3 } from "three"; export function vec3_to_threejs(v: Vec3): Vector3 { diff --git a/src/rendering/animation.ts b/src/rendering/conversion/ninja_animation.ts similarity index 95% rename from src/rendering/animation.ts rename to src/rendering/conversion/ninja_animation.ts index fbfeac98..22b6842a 100644 --- a/src/rendering/animation.ts +++ b/src/rendering/conversion/ninja_animation.ts @@ -8,12 +8,12 @@ import { QuaternionKeyframeTrack, VectorKeyframeTrack, } from "three"; -import { NjModel, NjObject } from "../data_formats/parsing/ninja"; +import { NjModel, NjObject } from "../../data_formats/parsing/ninja"; import { NjInterpolation, NjKeyframeTrackType, NjMotion, -} from "../data_formats/parsing/ninja/motion"; +} from "../../data_formats/parsing/ninja/motion"; export const PSO_FRAME_RATE = 30; diff --git a/src/rendering/models.ts b/src/rendering/conversion/ninja_geometry.ts similarity index 97% rename from src/rendering/models.ts rename to src/rendering/conversion/ninja_geometry.ts index 1b0d0b1d..9acddd4a 100644 --- a/src/rendering/models.ts +++ b/src/rendering/conversion/ninja_geometry.ts @@ -17,9 +17,9 @@ import { Mesh, } from "three"; import { vec3_to_threejs } from "."; -import { is_njcm_model, NjModel, NjObject } from "../data_formats/parsing/ninja"; -import { NjcmModel } from "../data_formats/parsing/ninja/njcm"; -import { xj_model_to_geometry } from "./xj_model_to_geometry"; +import { is_njcm_model, NjModel, NjObject } from "../../data_formats/parsing/ninja"; +import { NjcmModel } from "../../data_formats/parsing/ninja/njcm"; +import { xj_model_to_geometry } from "./xj_models"; const DUMMY_MATERIAL = new MeshBasicMaterial({ color: 0x00ff00, diff --git a/src/rendering/textures.ts b/src/rendering/conversion/ninja_textures.ts similarity index 94% rename from src/rendering/textures.ts rename to src/rendering/conversion/ninja_textures.ts index 3914aff2..279603ea 100644 --- a/src/rendering/textures.ts +++ b/src/rendering/conversion/ninja_textures.ts @@ -7,7 +7,7 @@ import { Texture, CompressedPixelFormat, } from "three"; -import { Xvm, XvmTexture } from "../data_formats/parsing/ninja/texture"; +import { Xvm, XvmTexture } from "../../data_formats/parsing/ninja/texture"; export function xvm_to_textures(xvm: Xvm): Texture[] { return xvm.textures.map(xvm_texture_to_texture); diff --git a/src/rendering/xj_model_to_geometry.ts b/src/rendering/conversion/xj_models.ts similarity index 95% rename from src/rendering/xj_model_to_geometry.ts rename to src/rendering/conversion/xj_models.ts index 0777d9de..28b88302 100644 --- a/src/rendering/xj_model_to_geometry.ts +++ b/src/rendering/conversion/xj_models.ts @@ -1,8 +1,8 @@ import { Matrix3, Matrix4, Vector3 } from "three"; import { vec3_to_threejs } from "."; -import { XjModel } from "../data_formats/parsing/ninja/xj"; -import { Vec2 } from "../data_formats/vector"; -import { VertexGroup } from "./models"; +import { XjModel } from "../../data_formats/parsing/ninja/xj"; +import { Vec2 } from "../../data_formats/vector"; +import { VertexGroup } from "./ninja_geometry"; const DEFAULT_NORMAL = new Vector3(0, 1, 0); const DEFAULT_UV = new Vec2(0, 0); diff --git a/src/rendering/entities.test.ts b/src/rendering/entities.test.ts deleted file mode 100644 index 4a0df84a..00000000 --- a/src/rendering/entities.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { CylinderBufferGeometry, MeshLambertMaterial, Object3D } from "three"; -import { DatNpc, DatObject } from "../data_formats/parsing/quest/dat"; -import { Vec3 } from "../data_formats/vector"; -import { NpcType, ObjectType, QuestNpc, QuestObject } from "../domain"; -import { create_npc_mesh, create_object_mesh, NPC_COLOR, OBJECT_COLOR } from "./entities"; - -const cylinder = new CylinderBufferGeometry(3, 3, 20).translate(0, 10, 0); - -test("create geometry for quest objects", () => { - const object = new QuestObject( - 7, - 13, - new Vec3(17, 19, 23), - new Vec3(0, 0, 0), - ObjectType.PrincipalWarp, - {} as DatObject - ); - const geometry = create_object_mesh(object, cylinder); - - expect(geometry).toBeInstanceOf(Object3D); - expect(geometry.name).toBe("Object"); - expect(geometry.userData.entity).toBe(object); - expect(geometry.position.x).toBe(17); - expect(geometry.position.y).toBe(19); - expect(geometry.position.z).toBe(23); - expect((geometry.material as MeshLambertMaterial).color.getHex()).toBe(OBJECT_COLOR); -}); - -test("create geometry for quest NPCs", () => { - const npc = new QuestNpc( - 7, - 13, - new Vec3(17, 19, 23), - new Vec3(0, 0, 0), - NpcType.Booma, - {} as DatNpc - ); - const geometry = create_npc_mesh(npc, cylinder); - - expect(geometry).toBeInstanceOf(Object3D); - expect(geometry.name).toBe("NPC"); - expect(geometry.userData.entity).toBe(npc); - expect(geometry.position.x).toBe(17); - expect(geometry.position.y).toBe(19); - expect(geometry.position.z).toBe(23); - expect((geometry.material as MeshLambertMaterial).color.getHex()).toBe(NPC_COLOR); -}); diff --git a/src/stores/AreaStore.ts b/src/stores/AreaStore.ts index 7f2fbbad..0713f214 100644 --- a/src/stores/AreaStore.ts +++ b/src/stores/AreaStore.ts @@ -1,14 +1,5 @@ -import { Object3D } from "three"; -import { Endianness } from "../data_formats"; -import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor"; -import { parse_area_collision_geometry } from "../data_formats/parsing/area_collision_geometry"; -import { parse_area_geometry } from "../data_formats/parsing/area_geometry"; import { Area, AreaVariant, Section } from "../domain"; -import { - area_collision_geometry_to_object_3d, - area_geometry_to_sections_and_object_3d, -} from "../rendering/areas"; -import { get_area_collision_data, get_area_render_data } from "./binary_assets"; +import { load_area_sections } from "../loading/areas"; function area(id: number, name: string, order: number, variants: number): Area { const area = new Area(id, name, order, []); @@ -19,16 +10,11 @@ function area(id: number, name: string, order: number, variants: number): Area { return area; } -const sections_cache: Map> = new Map(); -const render_geometry_cache: Map> = new Map(); -const collision_geometry_cache: Map> = new Map(); - class AreaStore { - areas: Area[][]; + readonly areas: Area[][] = []; constructor() { // The IDs match the PSO IDs for areas. - this.areas = []; let order = 0; this.areas[1] = [ area(0, "Pioneer II", order++, 1), @@ -86,7 +72,7 @@ class AreaStore { ]; } - get_variant(episode: number, area_id: number, variant_id: number): AreaVariant { + get_variant = (episode: number, area_id: number, variant_id: number): AreaVariant => { if (episode !== 1 && episode !== 2 && episode !== 4) throw new Error(`Expected episode to be 1, 2 or 4, got ${episode}.`); @@ -100,80 +86,15 @@ class AreaStore { ); return area_variant; - } + }; - async get_area_sections( + get_area_sections = ( episode: number, area_id: number, - area_variant: number - ): Promise { - const key = `${episode}-${area_id}-${area_variant}`; - let sections = sections_cache.get(key); - - if (!sections) { - this.load_area_sections_and_render_geometry(episode, area_id, area_variant); - sections = sections_cache.get(key)!; - } - - return sections; - } - - async get_area_render_geometry( - episode: number, - area_id: number, - area_variant: number - ): Promise { - const key = `${episode}-${area_id}-${area_variant}`; - let object_3d = render_geometry_cache.get(key); - - if (!object_3d) { - this.load_area_sections_and_render_geometry(episode, area_id, area_variant); - object_3d = render_geometry_cache.get(key)!; - } - - return object_3d; - } - - async get_area_collision_geometry( - episode: number, - area_id: number, - area_variant: number - ): Promise { - const object_3d = collision_geometry_cache.get(`${episode}-${area_id}-${area_variant}`); - - if (object_3d) { - return object_3d; - } else { - const object_3d = get_area_collision_data(episode, area_id, area_variant).then(buffer => - area_collision_geometry_to_object_3d( - parse_area_collision_geometry(new ArrayBufferCursor(buffer, Endianness.Little)) - ) - ); - collision_geometry_cache.set(`${area_id}-${area_variant}`, object_3d); - return object_3d; - } - } - - private load_area_sections_and_render_geometry( - episode: number, - area_id: number, - area_variant: number - ): void { - const promise = get_area_render_data(episode, area_id, area_variant).then(buffer => - area_geometry_to_sections_and_object_3d( - parse_area_geometry(new ArrayBufferCursor(buffer, Endianness.Little)) - ) - ); - - sections_cache.set( - `${episode}-${area_id}-${area_variant}`, - promise.then(([sections]) => sections) - ); - render_geometry_cache.set( - `${episode}-${area_id}-${area_variant}`, - promise.then(([, object_3d]) => object_3d) - ); - } + variant_id: number + ): Promise => { + return load_area_sections(episode, area_id, variant_id); + }; } export const area_store = new AreaStore(); diff --git a/src/stores/EntityStore.ts b/src/stores/EntityStore.ts deleted file mode 100644 index 42f6f9ca..00000000 --- a/src/stores/EntityStore.ts +++ /dev/null @@ -1,163 +0,0 @@ -import Logger from "js-logger"; -import { BufferGeometry, CylinderBufferGeometry, Texture } from "three"; -import { parse_nj, parse_xj } from "../data_formats/parsing/ninja"; -import { NpcType, ObjectType } from "../domain"; -import { ninja_object_to_buffer_geometry } from "../rendering/models"; -import { get_npc_data, get_object_data, AssetType } from "./binary_assets"; -import { Endianness } from "../data_formats"; -import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor"; -import { parse_xvm } from "../data_formats/parsing/ninja/texture"; -import { xvm_to_textures } from "../rendering/textures"; - -const logger = Logger.get("stores/EntityStore"); - -const DEFAULT_ENTITY = new CylinderBufferGeometry(3, 3, 20); -DEFAULT_ENTITY.translate(0, 10, 0); - -const DEFAULT_ENTITY_PROMISE: Promise = new Promise(resolve => - resolve(DEFAULT_ENTITY) -); - -const DEFAULT_ENTITY_TEX: Texture[] = []; - -const DEFAULT_ENTITY_TEX_PROMISE: Promise = new Promise(resolve => - resolve(DEFAULT_ENTITY_TEX) -); - -const npc_cache: Map> = new Map(); -npc_cache.set(NpcType.Unknown, DEFAULT_ENTITY_PROMISE); - -const npc_tex_cache: Map> = new Map(); -npc_tex_cache.set(NpcType.Unknown, DEFAULT_ENTITY_TEX_PROMISE); - -const object_cache: Map> = new Map(); -const object_tex_cache: Map> = new Map(); - -for (const type of [ - ObjectType.Unknown, - ObjectType.PlayerSet, - ObjectType.FogCollision, - ObjectType.EventCollision, - ObjectType.ObjRoomID, - ObjectType.ScriptCollision, - ObjectType.ItemLight, - ObjectType.FogCollisionSW, - ObjectType.MenuActivation, - ObjectType.BoxDetectObject, - ObjectType.SymbolChatObject, - ObjectType.TouchPlateObject, - ObjectType.TargetableObject, - ObjectType.EffectObject, - ObjectType.CountDownObject, - ObjectType.TelepipeLocation, - ObjectType.Pioneer2InvisibleTouchplate, - ObjectType.TempleMapDetect, - ObjectType.LabInvisibleObject, -]) { - object_cache.set(type, DEFAULT_ENTITY_PROMISE); - object_tex_cache.set(type, DEFAULT_ENTITY_TEX_PROMISE); -} - -class EntityStore { - async get_npc_geometry(npc_type: NpcType): Promise { - let mesh = npc_cache.get(npc_type); - - if (mesh) { - return mesh; - } else { - mesh = get_npc_data(npc_type, AssetType.Geometry) - .then(({ url, data }) => { - const cursor = new ArrayBufferCursor(data, Endianness.Little); - const nj_objects = url.endsWith(".nj") ? parse_nj(cursor) : parse_xj(cursor); - - if (nj_objects.length) { - return ninja_object_to_buffer_geometry(nj_objects[0]); - } else { - logger.warn(`Could not parse ${url}.`); - return DEFAULT_ENTITY; - } - }) - .catch(e => { - logger.warn(`Could load geometry file for ${npc_type.code}.`, e); - return DEFAULT_ENTITY; - }); - - npc_cache.set(npc_type, mesh); - return mesh; - } - } - - async get_npc_tex(npc_type: NpcType): Promise { - let tex = npc_tex_cache.get(npc_type); - - if (tex) { - return tex; - } else { - tex = get_npc_data(npc_type, AssetType.Texture) - .then(({ data }) => { - const cursor = new ArrayBufferCursor(data, Endianness.Little); - const xvm = parse_xvm(cursor); - return xvm_to_textures(xvm); - }) - .catch(e => { - logger.warn(`Could load texture file for ${npc_type.code}.`, e); - return DEFAULT_ENTITY_TEX; - }); - - npc_tex_cache.set(npc_type, tex); - return tex; - } - } - - async get_object_geometry(object_type: ObjectType): Promise { - let geometry = object_cache.get(object_type); - - if (geometry) { - return geometry; - } else { - geometry = get_object_data(object_type, AssetType.Geometry) - .then(({ url, data }) => { - const cursor = new ArrayBufferCursor(data, Endianness.Little); - const nj_objects = url.endsWith(".nj") ? parse_nj(cursor) : parse_xj(cursor); - - if (nj_objects.length) { - return ninja_object_to_buffer_geometry(nj_objects[0]); - } else { - logger.warn(`Could not parse ${url} for ${object_type.name}.`); - return DEFAULT_ENTITY; - } - }) - .catch(e => { - logger.warn(`Could load geometry file for ${object_type.name}.`, e); - return DEFAULT_ENTITY; - }); - - object_cache.set(object_type, geometry); - return geometry; - } - } - - async get_object_tex(object_type: ObjectType): Promise { - let tex = object_tex_cache.get(object_type); - - if (tex) { - return tex; - } else { - tex = get_object_data(object_type, AssetType.Texture) - .then(({ data }) => { - const cursor = new ArrayBufferCursor(data, Endianness.Little); - const xvm = parse_xvm(cursor); - return xvm_to_textures(xvm); - }) - .catch(e => { - logger.warn(`Could load texture file for ${object_type.name}.`, e); - return DEFAULT_ENTITY_TEX; - }); - - object_tex_cache.set(object_type, tex); - return tex; - } - } -} - -export const entity_store = new EntityStore(); diff --git a/src/stores/ModelViewerStore.ts b/src/stores/ModelViewerStore.ts index 30a7a7a0..1d1aff0d 100644 --- a/src/stores/ModelViewerStore.ts +++ b/src/stores/ModelViewerStore.ts @@ -19,10 +19,13 @@ import { NjMotion, parse_njm } from "../data_formats/parsing/ninja/motion"; import { parse_xvm } from "../data_formats/parsing/ninja/texture"; import { PlayerAnimation, PlayerModel } from "../domain"; import { read_file } from "../read_file"; -import { create_animation_clip, PSO_FRAME_RATE } from "../rendering/animation"; -import { ninja_object_to_mesh, ninja_object_to_skinned_mesh } from "../rendering/models"; -import { xvm_to_textures } from "../rendering/textures"; -import { get_player_animation_data, get_player_data } from "./binary_assets"; +import { create_animation_clip, PSO_FRAME_RATE } from "../rendering/conversion/ninja_animation"; +import { + ninja_object_to_mesh, + ninja_object_to_skinned_mesh, +} from "../rendering/conversion/ninja_geometry"; +import { xvm_to_textures } from "../rendering/conversion/ninja_textures"; +import { get_player_animation_data, get_player_data } from "../loading/player"; const logger = Logger.get("stores/ModelViewerStore"); const nj_object_cache: Map>> = new Map(); diff --git a/src/stores/QuestEditorStore.ts b/src/stores/QuestEditorStore.ts index c2035896..534d24ce 100644 --- a/src/stores/QuestEditorStore.ts +++ b/src/stores/QuestEditorStore.ts @@ -1,14 +1,12 @@ import Logger from "js-logger"; import { action, observable, runInAction } from "mobx"; +import { Endianness } from "../data_formats"; +import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor"; import { parse_quest, write_quest_qst } from "../data_formats/parsing/quest"; import { Vec3 } from "../data_formats/vector"; import { Area, Quest, QuestEntity, Section } from "../domain"; -import { create_npc_mesh, create_object_mesh } from "../rendering/entities"; -import { area_store } from "./AreaStore"; -import { entity_store } from "./EntityStore"; -import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor"; -import { Endianness } from "../data_formats"; import { read_file } from "../read_file"; +import { area_store } from "./AreaStore"; const logger = Logger.get("stores/QuestEditorStore"); @@ -69,10 +67,7 @@ class QuestEditorStore { // Generate object geometry. for (const object of quest.objects.filter(o => o.area_id === variant.area.id)) { try { - const object_geom = await entity_store.get_object_geometry(object.type); - const object_tex = await entity_store.get_object_tex(object.type); this.set_section_on_visible_quest_entity(object, sections); - object.object_3d = create_object_mesh(object, object_geom, object_tex); } catch (e) { logger.error(e); } @@ -81,10 +76,7 @@ class QuestEditorStore { // Generate NPC geometry. for (const npc of quest.npcs.filter(npc => npc.area_id === variant.area.id)) { try { - const npc_geom = await entity_store.get_npc_geometry(npc.type); - const npc_tex = await entity_store.get_npc_tex(npc.type); this.set_section_on_visible_quest_entity(npc, sections); - npc.object_3d = create_npc_mesh(npc, npc_geom, npc_tex); } catch (e) { logger.error(e); } diff --git a/src/stores/binary_assets.ts b/src/stores/binary_assets.ts deleted file mode 100644 index 95118491..00000000 --- a/src/stores/binary_assets.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { NpcType, ObjectType } from "../domain"; - -export enum AssetType { - Geometry, - Texture, -} - -export function get_area_render_data( - episode: number, - area_id: number, - area_version: number -): Promise { - return get_area_asset(episode, area_id, area_version, "render"); -} - -export function get_area_collision_data( - episode: number, - area_id: number, - area_version: number -): Promise { - return get_area_asset(episode, area_id, area_version, "collision"); -} - -export async function get_npc_data( - npc_type: NpcType, - type: AssetType -): Promise<{ url: string; data: ArrayBuffer }> { - const url = npc_type_to_url(npc_type, type); - const data = await get_asset(url); - return { url, data }; -} - -export async function get_object_data( - object_type: ObjectType, - type: AssetType -): Promise<{ url: string; data: ArrayBuffer }> { - const url = object_type_to_url(object_type, type); - const data = await get_asset(url); - return { url, data }; -} - -export async function get_player_data( - player_class: string, - body_part: string, - no?: number -): Promise { - return await get_asset(player_class_to_url(player_class, body_part, no)); -} - -export async function get_player_animation_data(animation_id: number): Promise { - return await get_asset( - `/player/animation/animation_${animation_id.toString().padStart(3, "0")}.njm` - ); -} - -const area_base_names = [ - [ - ["city00_00", 1], - ["forest01", 1], - ["forest02", 1], - ["cave01_", 6], - ["cave02_", 5], - ["cave03_", 6], - ["machine01_", 6], - ["machine02_", 6], - ["ancient01_", 5], - ["ancient02_", 5], - ["ancient03_", 5], - ["boss01", 1], - ["boss02", 1], - ["boss03", 1], - ["darkfalz00", 1], - ], - [ - ["labo00_00", 1], - ["ruins01_", 3], - ["ruins02_", 3], - ["space01_", 3], - ["space02_", 3], - ["jungle01_00", 1], - ["jungle02_00", 1], - ["jungle03_00", 1], - ["jungle04_", 3], - ["jungle05_00", 1], - ["seabed01_", 3], - ["seabed02_", 3], - ["boss05", 1], - ["boss06", 1], - ["boss07", 1], - ["boss08", 1], - ["jungle06_00", 1], - ["jungle07_", 5], - ], - [ - // Don't remove this empty array, see usage of areaBaseNames in areaVersionToBaseUrl. - ], - [ - ["city02_00", 1], - ["wilds01_00", 1], - ["wilds01_01", 1], - ["wilds01_02", 1], - ["wilds01_03", 1], - ["crater01_00", 1], - ["desert01_", 3], - ["desert02_", 3], - ["desert03_", 3], - ["boss09_00", 1], - ], -]; - -function area_version_to_base_url(episode: number, area_id: number, area_variant: number): string { - const episode_base_names = area_base_names[episode - 1]; - - if (0 <= area_id && area_id < episode_base_names.length) { - const [base_name, variants] = episode_base_names[area_id]; - - if (0 <= area_variant && area_variant < variants) { - let variant: string; - - if (variants === 1) { - variant = ""; - } else { - variant = String(area_variant); - while (variant.length < 2) variant = "0" + variant; - } - - return `/maps/map_${base_name}${variant}`; - } else { - throw new Error( - `Unknown variant ${area_variant} of area ${area_id} in episode ${episode}.` - ); - } - } else { - throw new Error(`Unknown episode ${episode} area ${area_id}.`); - } -} - -type AreaAssetType = "render" | "collision"; - -function get_area_asset( - episode: number, - area_id: number, - area_variant: number, - type: AreaAssetType -): Promise { - try { - const base_url = area_version_to_base_url(episode, area_id, area_variant); - const suffix = type === "render" ? "n.rel" : "c.rel"; - return get_asset(base_url + suffix); - } catch (e) { - return Promise.reject(e); - } -} - -function npc_type_to_url(npc_type: NpcType, type: AssetType): string { - switch (npc_type) { - // The dubswitch model is in XJ format. - case NpcType.Dubswitch: - return `/npcs/${npc_type.code}.${type === AssetType.Geometry ? "xj" : "xvm"}`; - - // Episode II VR Temple - - case NpcType.Hildebear2: - return npc_type_to_url(NpcType.Hildebear, type); - case NpcType.Hildeblue2: - return npc_type_to_url(NpcType.Hildeblue, type); - case NpcType.RagRappy2: - return npc_type_to_url(NpcType.RagRappy, type); - case NpcType.Monest2: - return npc_type_to_url(NpcType.Monest, type); - case NpcType.PoisonLily2: - return npc_type_to_url(NpcType.PoisonLily, type); - case NpcType.NarLily2: - return npc_type_to_url(NpcType.NarLily, type); - case NpcType.GrassAssassin2: - return npc_type_to_url(NpcType.GrassAssassin, type); - case NpcType.Dimenian2: - return npc_type_to_url(NpcType.Dimenian, type); - case NpcType.LaDimenian2: - return npc_type_to_url(NpcType.LaDimenian, type); - case NpcType.SoDimenian2: - return npc_type_to_url(NpcType.SoDimenian, type); - case NpcType.DarkBelra2: - return npc_type_to_url(NpcType.DarkBelra, type); - - // Episode II VR Spaceship - - case NpcType.SavageWolf2: - return npc_type_to_url(NpcType.SavageWolf, type); - case NpcType.BarbarousWolf2: - return npc_type_to_url(NpcType.BarbarousWolf, type); - case NpcType.PanArms2: - return npc_type_to_url(NpcType.PanArms, type); - case NpcType.Dubchic2: - return npc_type_to_url(NpcType.Dubchic, type); - case NpcType.Gilchic2: - return npc_type_to_url(NpcType.Gilchic, type); - case NpcType.Garanz2: - return npc_type_to_url(NpcType.Garanz, type); - case NpcType.Dubswitch2: - return npc_type_to_url(NpcType.Dubswitch, type); - case NpcType.Delsaber2: - return npc_type_to_url(NpcType.Delsaber, type); - case NpcType.ChaosSorcerer2: - return npc_type_to_url(NpcType.ChaosSorcerer, type); - - default: - return `/npcs/${npc_type.code}.${type === AssetType.Geometry ? "nj" : "xvm"}`; - } -} - -function object_type_to_url(object_type: ObjectType, type: AssetType): string { - if (type === AssetType.Geometry) { - switch (object_type) { - case ObjectType.EasterEgg: - case ObjectType.ChristmasTree: - case ObjectType.ChristmasWreath: - case ObjectType.TwentyFirstCentury: - case ObjectType.Sonic: - case ObjectType.WelcomeBoard: - case ObjectType.FloatingJelifish: - case ObjectType.RuinsSeal: - case ObjectType.Dolphin: - case ObjectType.Cacti: - case ObjectType.BigBrownRock: - case ObjectType.PoisonPlant: - case ObjectType.BigBlackRocks: - case ObjectType.FallingRock: - case ObjectType.DesertFixedTypeBoxBreakableCrystals: - case ObjectType.BeeHive: - return `/objects/${object_type.pso_id}.nj`; - - default: - return `/objects/${object_type.pso_id}.xj`; - } - } else { - return `/objects/${object_type.pso_id}.xvm`; - } -} - -function player_class_to_url(player_class: string, body_part: string, no?: number): string { - return `/player/${player_class}${body_part}${no == null ? "" : no}.nj`; -} - -function get_asset(url: string): Promise { - const base_url = process.env.PUBLIC_URL; - const promise = fetch(base_url + url).then(r => r.arrayBuffer()); - return promise; -}