From 7ae4ad428ce7619bd910f770848c6a75aea00190 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Thu, 19 Sep 2019 22:20:17 +0200 Subject: [PATCH] Basic entity creation via drag and drop. --- .../data_formats/parsing/quest/entities.ts | 6 +- src/core/data_formats/parsing/quest/index.ts | 298 +--- .../data_formats/parsing/quest/npc_types.ts | 1508 +++++++++++++++-- .../observable/property/list/ListProperty.ts | 5 +- .../property/list/SimpleListProperty.ts | 8 +- src/core/rendering/Renderer.ts | 6 +- src/quest_editor/gui/EntityListView.css | 22 + src/quest_editor/gui/EntityListView.ts | 161 ++ src/quest_editor/gui/NpcCountsView.ts | 21 +- src/quest_editor/gui/NpcListView.ts | 11 + src/quest_editor/gui/QuestEditorView.ts | 22 +- src/quest_editor/loading/areas.ts | 62 +- src/quest_editor/model/AreaVariantModel.ts | 7 +- src/quest_editor/model/QuestEntityModel.ts | 6 + src/quest_editor/model/QuestModel.ts | 17 +- src/quest_editor/model/QuestNpcModel.ts | 21 +- src/quest_editor/model/SectionModel.ts | 11 +- .../rendering/QuestEntityControls.ts | 234 ++- .../rendering/QuestModelManager.ts | 351 ++-- src/quest_editor/rendering/QuestRenderer.ts | 30 +- .../rendering/conversion/areas.ts | 9 +- src/quest_editor/stores/AreaStore.ts | 8 +- src/quest_editor/stores/QuestEditorStore.ts | 60 +- 23 files changed, 2232 insertions(+), 652 deletions(-) create mode 100644 src/quest_editor/gui/EntityListView.css create mode 100644 src/quest_editor/gui/EntityListView.ts create mode 100644 src/quest_editor/gui/NpcListView.ts diff --git a/src/core/data_formats/parsing/quest/entities.ts b/src/core/data_formats/parsing/quest/entities.ts index dd393191..bc347fa3 100644 --- a/src/core/data_formats/parsing/quest/entities.ts +++ b/src/core/data_formats/parsing/quest/entities.ts @@ -24,7 +24,7 @@ export type QuestNpc = { readonly pso_type_id: number; readonly npc_id: number; readonly script_label: number; - readonly roaming: number; + readonly pso_roaming: number; }; export type QuestObject = { @@ -56,6 +56,10 @@ export function entity_type_to_string(type: EntityType): string { return (NpcType as any)[type] || (ObjectType as any)[type]; } +export function is_npc_type(entity_type: EntityType): entity_type is NpcType { + return NpcType[entity_type] != undefined; +} + export function entity_data(type: EntityType): EntityTypeData { return npc_data(type as NpcType) || object_data(type as ObjectType); } diff --git a/src/core/data_formats/parsing/quest/index.ts b/src/core/data_formats/parsing/quest/index.ts index 8207381f..6fba5f2a 100644 --- a/src/core/data_formats/parsing/quest/index.ts +++ b/src/core/data_formats/parsing/quest/index.ts @@ -19,7 +19,7 @@ import { QuestNpc, QuestObject } from "./entities"; import { Episode } from "./Episode"; import { object_data, ObjectType, pso_id_to_object_type } from "./object_types"; import { parse_qst, QstContainedFile, write_qst } from "./qst"; -import { NpcType } from "./npc_types"; +import { npc_data, NpcType } from "./npc_types"; const logger = Logger.get("core/data_formats/parsing/quest"); @@ -284,7 +284,7 @@ function parse_npc_data(episode: number, npcs: DatNpc[]): QuestNpc[] { pso_type_id: npc_data.type_id, npc_id: npc_data.npc_id, script_label: Math.round(npc_data.script_label), - roaming: npc_data.roaming, + pso_roaming: npc_data.roaming, }; }); } @@ -583,307 +583,29 @@ function npcs_to_dat_data(npcs: QuestNpc[]): DatNpc[] { const dv = new DataView(new ArrayBuffer(4)); return npcs.map(npc => { - const type_data = npc_type_to_dat_data(npc.type) || { - type_id: npc.pso_type_id, - roaming: npc.roaming, - regular: true, - }; + const type_data = npc_data(npc.type); + const type_id = + type_data.pso_type_id == undefined ? npc.pso_type_id : type_data.pso_type_id; + const roaming = type_data.pso_roaming == undefined ? npc.pso_roaming : type_data.pso_roaming; + const regular = type_data.pso_regular == undefined ? true : type_data.pso_regular; dv.setFloat32(0, npc.scale.y); - dv.setUint32(0, (dv.getUint32(0) & ~0x800000) | (type_data.regular ? 0 : 0x800000)); + dv.setUint32(0, (dv.getUint32(0) & ~0x800000) | (regular ? 0 : 0x800000)); const scale_y = dv.getFloat32(0); let scale = new Vec3(npc.scale.x, scale_y, npc.scale.z); return { - type_id: type_data.type_id, + type_id, section_id: npc.section_id, position: npc.position, rotation: npc.rotation, scale, npc_id: npc.npc_id, script_label: npc.script_label, - roaming: type_data.roaming, + roaming, area_id: npc.area_id, unknown: npc.unknown, }; }); } - -function npc_type_to_dat_data( - type: NpcType, -): { type_id: number; roaming: number; regular: boolean } | undefined { - switch (type) { - default: - throw new Error(`Unexpected type ${NpcType[type]}.`); - - case NpcType.Unknown: - return undefined; - - case NpcType.FemaleFat: - return { type_id: 0x004, roaming: 0, regular: true }; - case NpcType.FemaleMacho: - return { type_id: 0x005, roaming: 0, regular: true }; - case NpcType.FemaleTall: - return { type_id: 0x007, roaming: 0, regular: true }; - case NpcType.MaleDwarf: - return { type_id: 0x00a, roaming: 0, regular: true }; - case NpcType.MaleFat: - return { type_id: 0x00b, roaming: 0, regular: true }; - case NpcType.MaleMacho: - return { type_id: 0x00c, roaming: 0, regular: true }; - case NpcType.MaleOld: - return { type_id: 0x00d, roaming: 0, regular: true }; - case NpcType.BlueSoldier: - return { type_id: 0x019, roaming: 0, regular: true }; - case NpcType.RedSoldier: - return { type_id: 0x01a, roaming: 0, regular: true }; - case NpcType.Principal: - return { type_id: 0x01b, roaming: 0, regular: true }; - case NpcType.Tekker: - return { type_id: 0x01c, roaming: 0, regular: true }; - case NpcType.GuildLady: - return { type_id: 0x01d, roaming: 0, regular: true }; - case NpcType.Scientist: - return { type_id: 0x01e, roaming: 0, regular: true }; - case NpcType.Nurse: - return { type_id: 0x01f, roaming: 0, regular: true }; - case NpcType.Irene: - return { type_id: 0x020, roaming: 0, regular: true }; - case NpcType.ItemShop: - return { type_id: 0x0f1, roaming: 0, regular: true }; - case NpcType.Nurse2: - return { type_id: 0x0fe, roaming: 0, regular: true }; - - case NpcType.Hildebear: - return { type_id: 0x040, roaming: 0, regular: true }; - case NpcType.Hildeblue: - return { type_id: 0x040, roaming: 1, regular: true }; - case NpcType.RagRappy: - return { type_id: 0x041, roaming: 0, regular: true }; - case NpcType.AlRappy: - return { type_id: 0x041, roaming: 1, regular: true }; - case NpcType.Monest: - return { type_id: 0x042, roaming: 0, regular: true }; - case NpcType.SavageWolf: - return { type_id: 0x043, roaming: 0, regular: true }; - case NpcType.BarbarousWolf: - return { type_id: 0x043, roaming: 0, regular: false }; - case NpcType.Booma: - return { type_id: 0x044, roaming: 0, regular: true }; - case NpcType.Gobooma: - return { type_id: 0x044, roaming: 1, regular: true }; - case NpcType.Gigobooma: - return { type_id: 0x044, roaming: 2, regular: true }; - case NpcType.Dragon: - return { type_id: 0x0c0, roaming: 0, regular: true }; - - case NpcType.GrassAssassin: - return { type_id: 0x060, roaming: 0, regular: true }; - case NpcType.PoisonLily: - return { type_id: 0x061, roaming: 0, regular: true }; - case NpcType.NarLily: - return { type_id: 0x061, roaming: 1, regular: true }; - case NpcType.NanoDragon: - return { type_id: 0x062, roaming: 0, regular: true }; - case NpcType.EvilShark: - return { type_id: 0x063, roaming: 0, regular: true }; - case NpcType.PalShark: - return { type_id: 0x063, roaming: 1, regular: true }; - case NpcType.GuilShark: - return { type_id: 0x063, roaming: 2, regular: true }; - case NpcType.PofuillySlime: - return { type_id: 0x064, roaming: 0, regular: true }; - case NpcType.PouillySlime: - return { type_id: 0x064, roaming: 0, regular: false }; - case NpcType.PanArms: - return { type_id: 0x065, roaming: 0, regular: true }; - case NpcType.DeRolLe: - return { type_id: 0x0c1, roaming: 0, regular: true }; - - case NpcType.Dubchic: - return { type_id: 0x080, roaming: 0, regular: true }; - case NpcType.Gilchic: - return { type_id: 0x080, roaming: 1, regular: true }; - case NpcType.Garanz: - return { type_id: 0x081, roaming: 0, regular: true }; - case NpcType.SinowBeat: - return { type_id: 0x082, roaming: 0, regular: true }; - case NpcType.SinowGold: - return { type_id: 0x082, roaming: 0, regular: false }; - case NpcType.Canadine: - return { type_id: 0x083, roaming: 0, regular: true }; - case NpcType.Canane: - return { type_id: 0x084, roaming: 0, regular: true }; - case NpcType.Dubswitch: - return { type_id: 0x085, roaming: 0, regular: true }; - case NpcType.VolOpt: - return { type_id: 0x0c5, roaming: 0, regular: true }; - - case NpcType.Delsaber: - return { type_id: 0x0a0, roaming: 0, regular: true }; - case NpcType.ChaosSorcerer: - return { type_id: 0x0a1, roaming: 0, regular: true }; - case NpcType.DarkGunner: - return { type_id: 0x0a2, roaming: 0, regular: true }; - case NpcType.ChaosBringer: - return { type_id: 0x0a4, roaming: 0, regular: true }; - case NpcType.DarkBelra: - return { type_id: 0x0a5, roaming: 0, regular: true }; - case NpcType.Dimenian: - return { type_id: 0x0a6, roaming: 0, regular: true }; - case NpcType.LaDimenian: - return { type_id: 0x0a6, roaming: 1, regular: true }; - case NpcType.SoDimenian: - return { type_id: 0x0a6, roaming: 2, regular: true }; - case NpcType.Bulclaw: - return { type_id: 0x0a7, roaming: 0, regular: true }; - case NpcType.Claw: - return { type_id: 0x0a8, roaming: 0, regular: true }; - case NpcType.DarkFalz: - return { type_id: 0x0c8, roaming: 0, regular: true }; - - case NpcType.Hildebear2: - return { type_id: 0x040, roaming: 0, regular: true }; - case NpcType.Hildeblue2: - return { type_id: 0x040, roaming: 1, regular: true }; - case NpcType.RagRappy2: - return { type_id: 0x041, roaming: 0, regular: true }; - case NpcType.LoveRappy: - return { type_id: 0x041, roaming: 1, regular: true }; - case NpcType.Monest2: - return { type_id: 0x042, roaming: 0, regular: true }; - case NpcType.PoisonLily2: - return { type_id: 0x061, roaming: 0, regular: true }; - case NpcType.NarLily2: - return { type_id: 0x061, roaming: 1, regular: true }; - case NpcType.GrassAssassin2: - return { type_id: 0x060, roaming: 0, regular: true }; - case NpcType.Dimenian2: - return { type_id: 0x0a6, roaming: 0, regular: true }; - case NpcType.LaDimenian2: - return { type_id: 0x0a6, roaming: 1, regular: true }; - case NpcType.SoDimenian2: - return { type_id: 0x0a6, roaming: 2, regular: true }; - case NpcType.DarkBelra2: - return { type_id: 0x0a5, roaming: 0, regular: true }; - case NpcType.BarbaRay: - return { type_id: 0x0cb, roaming: 0, regular: true }; - - case NpcType.SavageWolf2: - return { type_id: 0x043, roaming: 0, regular: true }; - case NpcType.BarbarousWolf2: - return { type_id: 0x043, roaming: 0, regular: false }; - case NpcType.PanArms2: - return { type_id: 0x065, roaming: 0, regular: true }; - case NpcType.Dubchic2: - return { type_id: 0x080, roaming: 0, regular: true }; - case NpcType.Gilchic2: - return { type_id: 0x080, roaming: 1, regular: true }; - case NpcType.Garanz2: - return { type_id: 0x081, roaming: 0, regular: true }; - case NpcType.Dubswitch2: - return { type_id: 0x085, roaming: 0, regular: true }; - case NpcType.Delsaber2: - return { type_id: 0x0a0, roaming: 0, regular: true }; - case NpcType.ChaosSorcerer2: - return { type_id: 0x0a1, roaming: 0, regular: true }; - case NpcType.GolDragon: - return { type_id: 0x0cc, roaming: 0, regular: true }; - - case NpcType.SinowBerill: - return { type_id: 0x0d4, roaming: 0, regular: true }; - case NpcType.SinowSpigell: - return { type_id: 0x0d4, roaming: 1, regular: true }; - case NpcType.Merillia: - return { type_id: 0x0d5, roaming: 0, regular: true }; - case NpcType.Meriltas: - return { type_id: 0x0d5, roaming: 1, regular: true }; - case NpcType.Mericarol: - return { type_id: 0x0d6, roaming: 0, regular: true }; - case NpcType.Mericus: - return { type_id: 0x0d6, roaming: 1, regular: true }; - case NpcType.Merikle: - return { type_id: 0x0d6, roaming: 2, regular: true }; - case NpcType.UlGibbon: - return { type_id: 0x0d7, roaming: 0, regular: true }; - case NpcType.ZolGibbon: - return { type_id: 0x0d7, roaming: 1, regular: true }; - case NpcType.Gibbles: - return { type_id: 0x0d8, roaming: 0, regular: true }; - case NpcType.Gee: - return { type_id: 0x0d9, roaming: 0, regular: true }; - case NpcType.GiGue: - return { type_id: 0x0da, roaming: 0, regular: true }; - case NpcType.GalGryphon: - return { type_id: 0x0c0, roaming: 0, regular: true }; - - case NpcType.Deldepth: - return { type_id: 0x0db, roaming: 0, regular: true }; - case NpcType.Delbiter: - return { type_id: 0x0dc, roaming: 0, regular: true }; - case NpcType.Dolmolm: - return { type_id: 0x0dd, roaming: 0, regular: true }; - case NpcType.Dolmdarl: - return { type_id: 0x0dd, roaming: 1, regular: true }; - case NpcType.Morfos: - return { type_id: 0x0de, roaming: 0, regular: true }; - case NpcType.Recobox: - return { type_id: 0x0df, roaming: 0, regular: true }; - case NpcType.Epsilon: - return { type_id: 0x0e0, roaming: 0, regular: true }; - case NpcType.SinowZoa: - return { type_id: 0x0e0, roaming: 0, regular: true }; - case NpcType.SinowZele: - return { type_id: 0x0e0, roaming: 1, regular: true }; - case NpcType.IllGill: - return { type_id: 0x0e1, roaming: 0, regular: true }; - case NpcType.DelLily: - return { type_id: 0x061, roaming: 0, regular: true }; - case NpcType.OlgaFlow: - return { type_id: 0x0ca, roaming: 0, regular: true }; - - case NpcType.SandRappy: - return { type_id: 0x041, roaming: 0, regular: true }; - case NpcType.DelRappy: - return { type_id: 0x041, roaming: 1, regular: true }; - case NpcType.Astark: - return { type_id: 0x110, roaming: 0, regular: true }; - case NpcType.SatelliteLizard: - return { type_id: 0x111, roaming: 0, regular: true }; - case NpcType.Yowie: - return { type_id: 0x111, roaming: 0, regular: false }; - case NpcType.MerissaA: - return { type_id: 0x112, roaming: 0, regular: true }; - case NpcType.MerissaAA: - return { type_id: 0x112, roaming: 1, regular: true }; - case NpcType.Girtablulu: - return { type_id: 0x113, roaming: 0, regular: true }; - case NpcType.Zu: - return { type_id: 0x114, roaming: 0, regular: true }; - case NpcType.Pazuzu: - return { type_id: 0x114, roaming: 1, regular: true }; - case NpcType.Boota: - return { type_id: 0x115, roaming: 0, regular: true }; - case NpcType.ZeBoota: - return { type_id: 0x115, roaming: 1, regular: true }; - case NpcType.BaBoota: - return { type_id: 0x115, roaming: 2, regular: true }; - case NpcType.Dorphon: - return { type_id: 0x116, roaming: 0, regular: true }; - case NpcType.DorphonEclair: - return { type_id: 0x116, roaming: 1, regular: true }; - case NpcType.Goran: - return { type_id: 0x117, roaming: 0, regular: true }; - case NpcType.PyroGoran: - return { type_id: 0x117, roaming: 1, regular: true }; - case NpcType.GoranDetonator: - return { type_id: 0x117, roaming: 2, regular: true }; - case NpcType.SaintMilion: - return { type_id: 0x119, roaming: 0, regular: true }; - case NpcType.Shambertin: - return { type_id: 0x119, roaming: 1, regular: true }; - case NpcType.Kondrieu: - return { type_id: 0x119, roaming: 0, regular: false }; - } -} diff --git a/src/core/data_formats/parsing/quest/npc_types.ts b/src/core/data_formats/parsing/quest/npc_types.ts index e5a4a3e3..b5b17b30 100644 --- a/src/core/data_formats/parsing/quest/npc_types.ts +++ b/src/core/data_formats/parsing/quest/npc_types.ts @@ -187,7 +187,7 @@ export enum NpcType { export type NpcTypeData = { /** - * Unique name. E.g. a Delsaber would have (Ep. II) appended to its name. + * Unique name. E.g. an episode II Delsaber would have (Ep. II) appended to its name. */ readonly name: string; /** @@ -199,6 +199,21 @@ export type NpcTypeData = { readonly episode?: Episode; readonly enemy: boolean; readonly rare_type?: NpcType; + /** + * Type ID used by the game. + */ + readonly pso_type_id?: number; + /** + * Roaming value used by the game. + */ + readonly pso_roaming?: number; + /** + * Boolean specifying whether an NPC is the regular or special variant. The game uses a single + * bit in the y component of the NPC's scale vector for this value. + * Sometimes signifies a variant (e.g. Barbarous Wolf), sometimes a rare variant (e.g. Pouilly + * Slime). + */ + readonly pso_regular?: boolean; }; export const NPC_TYPES: NpcType[] = []; @@ -233,7 +248,10 @@ function define_npc_type_data( ultimate_name: string, episode: Episode | undefined, enemy: boolean, - rare_type?: NpcType, + rare_type: NpcType | undefined, + pso_type_id: number | undefined, + pso_roaming: number | undefined, + pso_regular: boolean | undefined, ): void { NPC_TYPES.push(npc_type); @@ -248,6 +266,9 @@ function define_npc_type_data( episode, enemy, rare_type, + pso_type_id, + pso_roaming, + pso_regular, }; if (episode) { @@ -264,13 +285,35 @@ function define_npc_type_data( // Unknown NPCs // -define_npc_type_data(NpcType.Unknown, "Unknown", "Unknown", "Unknown", undefined, false); +define_npc_type_data( + NpcType.Unknown, + "Unknown", + "Unknown", + "Unknown", + undefined, + false, + undefined, + undefined, + undefined, + undefined, +); // // Friendly NPCs // -define_npc_type_data(NpcType.FemaleFat, "Female Fat", "Female Fat", "Female Fat", undefined, false); +define_npc_type_data( + NpcType.FemaleFat, + "Female Fat", + "Female Fat", + "Female Fat", + undefined, + false, + undefined, + 0x004, + 0, + true, +); define_npc_type_data( NpcType.FemaleMacho, "Female Macho", @@ -278,6 +321,10 @@ define_npc_type_data( "Female Macho", undefined, false, + undefined, + 0x005, + 0, + true, ); define_npc_type_data( NpcType.FemaleTall, @@ -286,11 +333,59 @@ define_npc_type_data( "Female Tall", undefined, false, + undefined, + 0x007, + 0, + true, +); +define_npc_type_data( + NpcType.MaleDwarf, + "Male Dwarf", + "Male Dwarf", + "Male Dwarf", + undefined, + false, + undefined, + 0x00a, + 0, + true, +); +define_npc_type_data( + NpcType.MaleFat, + "Male Fat", + "Male Fat", + "Male Fat", + undefined, + false, + undefined, + 0x00b, + 0, + true, +); +define_npc_type_data( + NpcType.MaleMacho, + "Male Macho", + "Male Macho", + "Male Macho", + undefined, + false, + undefined, + 0x00c, + 0, + true, +); +define_npc_type_data( + NpcType.MaleOld, + "Male Old", + "Male Old", + "Male Old", + undefined, + false, + undefined, + 0x00d, + 0, + true, ); -define_npc_type_data(NpcType.MaleDwarf, "Male Dwarf", "Male Dwarf", "Male Dwarf", undefined, false); -define_npc_type_data(NpcType.MaleFat, "Male Fat", "Male Fat", "Male Fat", undefined, false); -define_npc_type_data(NpcType.MaleMacho, "Male Macho", "Male Macho", "Male Macho", undefined, false); -define_npc_type_data(NpcType.MaleOld, "Male Old", "Male Old", "Male Old", undefined, false); define_npc_type_data( NpcType.BlueSoldier, "Blue Soldier", @@ -298,6 +393,10 @@ define_npc_type_data( "Blue Soldier", undefined, false, + undefined, + 0x019, + 0, + true, ); define_npc_type_data( NpcType.RedSoldier, @@ -306,15 +405,107 @@ define_npc_type_data( "Red Soldier", undefined, false, + undefined, + 0x01a, + 0, + true, +); +define_npc_type_data( + NpcType.Principal, + "Principal", + "Principal", + "Principal", + undefined, + false, + undefined, + 0x01b, + 0, + true, +); +define_npc_type_data( + NpcType.Tekker, + "Tekker", + "Tekker", + "Tekker", + undefined, + false, + undefined, + 0x01c, + 0, + true, +); +define_npc_type_data( + NpcType.GuildLady, + "Guild Lady", + "Guild Lady", + "Guild Lady", + undefined, + false, + undefined, + 0x01d, + 0, + true, +); +define_npc_type_data( + NpcType.Scientist, + "Scientist", + "Scientist", + "Scientist", + undefined, + false, + undefined, + 0x01e, + 0, + true, +); +define_npc_type_data( + NpcType.Nurse, + "Nurse", + "Nurse", + "Nurse", + undefined, + false, + undefined, + 0x01f, + 0, + true, +); +define_npc_type_data( + NpcType.Irene, + "Irene", + "Irene", + "Irene", + undefined, + false, + undefined, + 0x020, + 0, + true, +); +define_npc_type_data( + NpcType.ItemShop, + "Item Shop", + "Item Shop", + "Item Shop", + undefined, + false, + undefined, + 0x0f1, + 0, + true, +); +define_npc_type_data( + NpcType.Nurse2, + "Nurse (Ep. II)", + "Nurse", + "Nurse", + 2, + false, + undefined, + 0x0fe, + 0, + true, ); -define_npc_type_data(NpcType.Principal, "Principal", "Principal", "Principal", undefined, false); -define_npc_type_data(NpcType.Tekker, "Tekker", "Tekker", "Tekker", undefined, false); -define_npc_type_data(NpcType.GuildLady, "Guild Lady", "Guild Lady", "Guild Lady", undefined, false); -define_npc_type_data(NpcType.Scientist, "Scientist", "Scientist", "Scientist", undefined, false); -define_npc_type_data(NpcType.Nurse, "Nurse", "Nurse", "Nurse", undefined, false); -define_npc_type_data(NpcType.Irene, "Irene", "Irene", "Irene", undefined, false); -define_npc_type_data(NpcType.ItemShop, "Item Shop", "Item Shop", "Item Shop", undefined, false); -define_npc_type_data(NpcType.Nurse2, "Nurse (Ep. II)", "Nurse", "Nurse", 2, false); // // Enemy NPCs @@ -330,8 +521,22 @@ define_npc_type_data( 1, true, NpcType.Hildeblue, + 0x040, + 0, + true, +); +define_npc_type_data( + NpcType.Hildeblue, + "Hildeblue", + "Hildeblue", + "Hildetorr", + 1, + true, + undefined, + 0x040, + 1, + true, ); -define_npc_type_data(NpcType.Hildeblue, "Hildeblue", "Hildeblue", "Hildetorr", 1, true); define_npc_type_data( NpcType.RagRappy, "Rag Rappy", @@ -340,11 +545,58 @@ define_npc_type_data( 1, true, NpcType.AlRappy, + 0x041, + 0, + true, +); +define_npc_type_data( + NpcType.AlRappy, + "Al Rappy", + "Al Rappy", + "Pal Rappy", + 1, + true, + undefined, + 0x041, + 1, + true, +); +define_npc_type_data( + NpcType.Monest, + "Monest", + "Monest", + "Mothvist", + 1, + true, + undefined, + 0x042, + 0, + true, +); +define_npc_type_data( + NpcType.Mothmant, + "Mothmant", + "Mothmant", + "Mothvert", + 1, + true, + undefined, + undefined, + undefined, + undefined, +); +define_npc_type_data( + NpcType.SavageWolf, + "Savage Wolf", + "Savage Wolf", + "Gulgus", + 1, + true, + undefined, + 0x043, + 0, + true, ); -define_npc_type_data(NpcType.AlRappy, "Al Rappy", "Al Rappy", "Pal Rappy", 1, true); -define_npc_type_data(NpcType.Monest, "Monest", "Monest", "Mothvist", 1, true); -define_npc_type_data(NpcType.Mothmant, "Mothmant", "Mothmant", "Mothvert", 1, true); -define_npc_type_data(NpcType.SavageWolf, "Savage Wolf", "Savage Wolf", "Gulgus", 1, true); define_npc_type_data( NpcType.BarbarousWolf, "Barbarous Wolf", @@ -352,11 +604,48 @@ define_npc_type_data( "Gulgus-Gue", 1, true, + undefined, + 0x043, + 0, + false, +); +define_npc_type_data(NpcType.Booma, "Booma", "Booma", "Bartle", 1, true, undefined, 0x044, 0, true); +define_npc_type_data( + NpcType.Gobooma, + "Gobooma", + "Gobooma", + "Barble", + 1, + true, + undefined, + 0x044, + 1, + true, +); +define_npc_type_data( + NpcType.Gigobooma, + "Gigobooma", + "Gigobooma", + "Tollaw", + 1, + true, + undefined, + 0x044, + 2, + true, +); +define_npc_type_data( + NpcType.Dragon, + "Dragon", + "Dragon", + "Sil Dragon", + 1, + true, + undefined, + 0x0c0, + 0, + true, ); -define_npc_type_data(NpcType.Booma, "Booma", "Booma", "Bartle", 1, true); -define_npc_type_data(NpcType.Gobooma, "Gobooma", "Gobooma", "Barble", 1, true); -define_npc_type_data(NpcType.Gigobooma, "Gigobooma", "Gigobooma", "Tollaw", 1, true); -define_npc_type_data(NpcType.Dragon, "Dragon", "Dragon", "Sil Dragon", 1, true); // Episode I Caves @@ -367,6 +656,10 @@ define_npc_type_data( "Crimson Assassin", 1, true, + undefined, + 0x060, + 0, + true, ); define_npc_type_data( NpcType.PoisonLily, @@ -376,12 +669,70 @@ define_npc_type_data( 1, true, NpcType.NarLily, + 0x061, + 0, + true, +); +define_npc_type_data( + NpcType.NarLily, + "Nar Lily", + "Nar Lily", + "Mil Lily", + 1, + true, + undefined, + 0x061, + 1, + true, +); +define_npc_type_data( + NpcType.NanoDragon, + "Nano Dragon", + "Nano Dragon", + "Nano Dragon", + 1, + true, + undefined, + 0x062, + 0, + true, +); +define_npc_type_data( + NpcType.EvilShark, + "Evil Shark", + "Evil Shark", + "Vulmer", + 1, + true, + undefined, + 0x063, + 0, + true, +); +define_npc_type_data( + NpcType.PalShark, + "Pal Shark", + "Pal Shark", + "Govulmer", + 1, + true, + undefined, + 0x063, + 1, + true, +); +define_npc_type_data( + NpcType.GuilShark, + "Guil Shark", + "Guil Shark", + "Melqueek", + 1, + true, + undefined, + 0x063, + 2, + true, ); -define_npc_type_data(NpcType.NarLily, "Nar Lily", "Nar Lily", "Mil Lily", 1, true); -define_npc_type_data(NpcType.NanoDragon, "Nano Dragon", "Nano Dragon", "Nano Dragon", 1, true); -define_npc_type_data(NpcType.EvilShark, "Evil Shark", "Evil Shark", "Vulmer", 1, true); -define_npc_type_data(NpcType.PalShark, "Pal Shark", "Pal Shark", "Govulmer", 1, true); -define_npc_type_data(NpcType.GuilShark, "Guil Shark", "Guil Shark", "Melqueek", 1, true); define_npc_type_data( NpcType.PofuillySlime, "Pofuilly Slime", @@ -390,6 +741,9 @@ define_npc_type_data( 1, true, NpcType.PouillySlime, + 0x064, + 0, + true, ); define_npc_type_data( NpcType.PouillySlime, @@ -398,27 +752,185 @@ define_npc_type_data( "Pouilly Slime", 1, true, + undefined, + 0x064, + 0, + false, +); +define_npc_type_data( + NpcType.PanArms, + "Pan Arms", + "Pan Arms", + "Pan Arms", + 1, + true, + undefined, + 0x065, + 0, + true, +); +define_npc_type_data( + NpcType.Migium, + "Migium", + "Migium", + "Migium", + 1, + true, + undefined, + undefined, + undefined, + undefined, +); +define_npc_type_data( + NpcType.Hidoom, + "Hidoom", + "Hidoom", + "Hidoom", + 1, + true, + undefined, + undefined, + undefined, + undefined, +); +define_npc_type_data( + NpcType.DeRolLe, + "De Rol Le", + "De Rol Le", + "Dal Ra Lie", + 1, + true, + undefined, + 0x0c1, + 0, + true, ); -define_npc_type_data(NpcType.PanArms, "Pan Arms", "Pan Arms", "Pan Arms", 1, true); -define_npc_type_data(NpcType.Migium, "Migium", "Migium", "Migium", 1, true); -define_npc_type_data(NpcType.Hidoom, "Hidoom", "Hidoom", "Hidoom", 1, true); -define_npc_type_data(NpcType.DeRolLe, "De Rol Le", "De Rol Le", "Dal Ra Lie", 1, true); // Episode I Mines -define_npc_type_data(NpcType.Dubchic, "Dubchic", "Dubchic", "Dubchich", 1, true); -define_npc_type_data(NpcType.Gilchic, "Gilchic", "Gilchic", "Gilchich", 1, true); -define_npc_type_data(NpcType.Garanz, "Garanz", "Garanz", "Baranz", 1, true); -define_npc_type_data(NpcType.SinowBeat, "Sinow Beat", "Sinow Beat", "Sinow Blue", 1, true); -define_npc_type_data(NpcType.SinowGold, "Sinow Gold", "Sinow Gold", "Sinow Red", 1, true); -define_npc_type_data(NpcType.Canadine, "Canadine", "Canadine", "Canabin", 1, true); -define_npc_type_data(NpcType.Canane, "Canane", "Canane", "Canune", 1, true); -define_npc_type_data(NpcType.Dubswitch, "Dubswitch", "Dubswitch", "Dubswitch", 1, true); -define_npc_type_data(NpcType.VolOpt, "Vol Opt", "Vol Opt", "Vol Opt ver.2", 1, true); +define_npc_type_data( + NpcType.Dubchic, + "Dubchic", + "Dubchic", + "Dubchich", + 1, + true, + undefined, + 0x080, + 0, + true, +); +define_npc_type_data( + NpcType.Gilchic, + "Gilchic", + "Gilchic", + "Gilchich", + 1, + true, + undefined, + 0x080, + 1, + true, +); +define_npc_type_data( + NpcType.Garanz, + "Garanz", + "Garanz", + "Baranz", + 1, + true, + undefined, + 0x081, + 0, + true, +); +define_npc_type_data( + NpcType.SinowBeat, + "Sinow Beat", + "Sinow Beat", + "Sinow Blue", + 1, + true, + undefined, + 0x082, + 0, + true, +); +define_npc_type_data( + NpcType.SinowGold, + "Sinow Gold", + "Sinow Gold", + "Sinow Red", + 1, + true, + undefined, + 0x082, + 0, + false, +); +define_npc_type_data( + NpcType.Canadine, + "Canadine", + "Canadine", + "Canabin", + 1, + true, + undefined, + 0x083, + 0, + true, +); +define_npc_type_data( + NpcType.Canane, + "Canane", + "Canane", + "Canune", + 1, + true, + undefined, + 0x084, + 0, + true, +); +define_npc_type_data( + NpcType.Dubswitch, + "Dubswitch", + "Dubswitch", + "Dubswitch", + 1, + true, + undefined, + 0x085, + 0, + true, +); +define_npc_type_data( + NpcType.VolOpt, + "Vol Opt", + "Vol Opt", + "Vol Opt ver.2", + 1, + true, + undefined, + 0x0c5, + 0, + true, +); // Episode I Ruins -define_npc_type_data(NpcType.Delsaber, "Delsaber", "Delsaber", "Delsaber", 1, true); +define_npc_type_data( + NpcType.Delsaber, + "Delsaber", + "Delsaber", + "Delsaber", + 1, + true, + undefined, + 0x0a0, + 0, + true, +); define_npc_type_data( NpcType.ChaosSorcerer, "Chaos Sorcerer", @@ -426,9 +938,35 @@ define_npc_type_data( "Gran Sorcerer", 1, true, + undefined, + 0x0a1, + 0, + true, +); +define_npc_type_data( + NpcType.DarkGunner, + "Dark Gunner", + "Dark Gunner", + "Dark Gunner", + 1, + true, + undefined, + 0x0a2, + 0, + true, +); +define_npc_type_data( + NpcType.DeathGunner, + "Death Gunner", + "Death Gunner", + "Death Gunner", + 1, + true, + undefined, + undefined, + undefined, + undefined, ); -define_npc_type_data(NpcType.DarkGunner, "Dark Gunner", "Dark Gunner", "Dark Gunner", 1, true); -define_npc_type_data(NpcType.DeathGunner, "Death Gunner", "Death Gunner", "Death Gunner", 1, true); define_npc_type_data( NpcType.ChaosBringer, "Chaos Bringer", @@ -436,15 +974,96 @@ define_npc_type_data( "Dark Bringer", 1, true, + undefined, + 0x0a4, + 0, + true, +); +define_npc_type_data( + NpcType.DarkBelra, + "Dark Belra", + "Dark Belra", + "Indi Belra", + 1, + true, + undefined, + 0x0a5, + 0, + true, +); +define_npc_type_data( + NpcType.Dimenian, + "Dimenian", + "Dimenian", + "Arlan", + 1, + true, + undefined, + 0x0a6, + 0, + true, +); +define_npc_type_data( + NpcType.LaDimenian, + "La Dimenian", + "La Dimenian", + "Merlan", + 1, + true, + undefined, + 0x0a6, + 1, + true, +); +define_npc_type_data( + NpcType.SoDimenian, + "So Dimenian", + "So Dimenian", + "Del-D", + 1, + true, + undefined, + 0x0a6, + 2, + true, +); +define_npc_type_data( + NpcType.Bulclaw, + "Bulclaw", + "Bulclaw", + "Bulclaw", + 1, + true, + undefined, + 0x0a7, + 0, + true, +); +define_npc_type_data( + NpcType.Bulk, + "Bulk", + "Bulk", + "Bulk", + 1, + true, + undefined, + undefined, + undefined, + undefined, +); +define_npc_type_data(NpcType.Claw, "Claw", "Claw", "Claw", 1, true, undefined, 0x0a8, 0, true); +define_npc_type_data( + NpcType.DarkFalz, + "Dark Falz", + "Dark Falz", + "Dark Falz", + 1, + true, + undefined, + 0x0c8, + 0, + true, ); -define_npc_type_data(NpcType.DarkBelra, "Dark Belra", "Dark Belra", "Indi Belra", 1, true); -define_npc_type_data(NpcType.Dimenian, "Dimenian", "Dimenian", "Arlan", 1, true); -define_npc_type_data(NpcType.LaDimenian, "La Dimenian", "La Dimenian", "Merlan", 1, true); -define_npc_type_data(NpcType.SoDimenian, "So Dimenian", "So Dimenian", "Del-D", 1, true); -define_npc_type_data(NpcType.Bulclaw, "Bulclaw", "Bulclaw", "Bulclaw", 1, true); -define_npc_type_data(NpcType.Bulk, "Bulk", "Bulk", "Bulk", 1, true); -define_npc_type_data(NpcType.Claw, "Claw", "Claw", "Claw", 1, true); -define_npc_type_data(NpcType.DarkFalz, "Dark Falz", "Dark Falz", "Dark Falz", 1, true); // Episode II VR Temple @@ -456,8 +1075,22 @@ define_npc_type_data( 2, true, NpcType.Hildeblue2, + 0x040, + 0, + true, +); +define_npc_type_data( + NpcType.Hildeblue2, + "Hildeblue (Ep. II)", + "Hildeblue", + "Hildetorr", + 2, + true, + undefined, + 0x040, + 1, + true, ); -define_npc_type_data(NpcType.Hildeblue2, "Hildeblue (Ep. II)", "Hildeblue", "Hildetorr", 2, true); define_npc_type_data( NpcType.RagRappy2, "Rag Rappy (Ep. II)", @@ -466,13 +1099,82 @@ define_npc_type_data( 2, true, NpcType.LoveRappy, + 0x041, + 0, + true, +); +define_npc_type_data( + NpcType.LoveRappy, + "Love Rappy", + "Love Rappy", + "Love Rappy", + 2, + true, + undefined, + 0x041, + 1, + true, +); +define_npc_type_data( + NpcType.StRappy, + "St. Rappy", + "St. Rappy", + "St. Rappy", + 2, + true, + undefined, + undefined, + undefined, + undefined, +); +define_npc_type_data( + NpcType.HalloRappy, + "Hallo Rappy", + "Hallo Rappy", + "Hallo Rappy", + 2, + true, + undefined, + undefined, + undefined, + undefined, +); +define_npc_type_data( + NpcType.EggRappy, + "Egg Rappy", + "Egg Rappy", + "Egg Rappy", + 2, + true, + undefined, + undefined, + undefined, + undefined, +); +define_npc_type_data( + NpcType.Monest2, + "Monest (Ep. II)", + "Monest", + "Mothvist", + 2, + true, + undefined, + 0x042, + 0, + true, +); +define_npc_type_data( + NpcType.Mothmant2, + "Mothmant", + "Mothmant", + "Mothvert", + 2, + true, + undefined, + undefined, + undefined, + undefined, ); -define_npc_type_data(NpcType.LoveRappy, "Love Rappy", "Love Rappy", "Love Rappy", 2, true); -define_npc_type_data(NpcType.StRappy, "St. Rappy", "St. Rappy", "St. Rappy", 2, true); -define_npc_type_data(NpcType.HalloRappy, "Hallo Rappy", "Hallo Rappy", "Hallo Rappy", 2, true); -define_npc_type_data(NpcType.EggRappy, "Egg Rappy", "Egg Rappy", "Egg Rappy", 2, true); -define_npc_type_data(NpcType.Monest2, "Monest (Ep. II)", "Monest", "Mothvist", 2, true); -define_npc_type_data(NpcType.Mothmant2, "Mothmant", "Mothmant", "Mothvert", 2, true); define_npc_type_data( NpcType.PoisonLily2, "Poison Lily (Ep. II)", @@ -481,8 +1183,22 @@ define_npc_type_data( 2, true, NpcType.NarLily2, + 0x061, + 0, + true, +); +define_npc_type_data( + NpcType.NarLily2, + "Nar Lily (Ep. II)", + "Nar Lily", + "Mil Lily", + 2, + true, + undefined, + 0x061, + 1, + true, ); -define_npc_type_data(NpcType.NarLily2, "Nar Lily (Ep. II)", "Nar Lily", "Mil Lily", 2, true); define_npc_type_data( NpcType.GrassAssassin2, "Grass Assassin (Ep. II)", @@ -490,10 +1206,47 @@ define_npc_type_data( "Crimson Assassin", 2, true, + undefined, + 0x060, + 0, + true, +); +define_npc_type_data( + NpcType.Dimenian2, + "Dimenian (Ep. II)", + "Dimenian", + "Arlan", + 2, + true, + undefined, + 0x0a6, + 0, + true, +); +define_npc_type_data( + NpcType.LaDimenian2, + "La Dimenian (Ep. II)", + "La Dimenian", + "Merlan", + 2, + true, + undefined, + 0x0a6, + 1, + true, +); +define_npc_type_data( + NpcType.SoDimenian2, + "So Dimenian (Ep. II)", + "So Dimenian", + "Del-D", + 2, + true, + undefined, + 0x0a6, + 2, + true, ); -define_npc_type_data(NpcType.Dimenian2, "Dimenian (Ep. II)", "Dimenian", "Arlan", 2, true); -define_npc_type_data(NpcType.LaDimenian2, "La Dimenian (Ep. II)", "La Dimenian", "Merlan", 2, true); -define_npc_type_data(NpcType.SoDimenian2, "So Dimenian (Ep. II)", "So Dimenian", "Del-D", 2, true); define_npc_type_data( NpcType.DarkBelra2, "Dark Belra (Ep. II)", @@ -501,12 +1254,38 @@ define_npc_type_data( "Indi Belra", 2, true, + undefined, + 0x0a5, + 0, + true, +); +define_npc_type_data( + NpcType.BarbaRay, + "Barba Ray", + "Barba Ray", + "Barba Ray", + 2, + true, + undefined, + 0x0cb, + 0, + true, ); -define_npc_type_data(NpcType.BarbaRay, "Barba Ray", "Barba Ray", "Barba Ray", 2, true); // Episode II VR Spaceship -define_npc_type_data(NpcType.SavageWolf2, "Savage Wolf (Ep. II)", "Savage Wolf", "Gulgus", 2, true); +define_npc_type_data( + NpcType.SavageWolf2, + "Savage Wolf (Ep. II)", + "Savage Wolf", + "Gulgus", + 2, + true, + undefined, + 0x043, + 0, + true, +); define_npc_type_data( NpcType.BarbarousWolf2, "Barbarous Wolf (Ep. II)", @@ -514,15 +1293,107 @@ define_npc_type_data( "Gulgus-Gue", 2, true, + undefined, + 0x043, + 0, + false, +); +define_npc_type_data( + NpcType.PanArms2, + "Pan Arms (Ep. II)", + "Pan Arms", + "Pan Arms", + 2, + true, + undefined, + 0x065, + 0, + true, +); +define_npc_type_data( + NpcType.Migium2, + "Migium (Ep. II)", + "Migium", + "Migium", + 2, + true, + undefined, + undefined, + undefined, + undefined, +); +define_npc_type_data( + NpcType.Hidoom2, + "Hidoom (Ep. II)", + "Hidoom", + "Hidoom", + 2, + true, + undefined, + undefined, + undefined, + undefined, +); +define_npc_type_data( + NpcType.Dubchic2, + "Dubchic (Ep. II)", + "Dubchic", + "Dubchich", + 2, + true, + undefined, + 0x080, + 0, + true, +); +define_npc_type_data( + NpcType.Gilchic2, + "Gilchic (Ep. II)", + "Gilchic", + "Gilchich", + 2, + true, + undefined, + 0x080, + 1, + true, +); +define_npc_type_data( + NpcType.Garanz2, + "Garanz (Ep. II)", + "Garanz", + "Baranz", + 2, + true, + undefined, + 0x081, + 0, + true, +); +define_npc_type_data( + NpcType.Dubswitch2, + "Dubswitch (Ep. II)", + "Dubswitch", + "Dubswitch", + 2, + true, + undefined, + 0x085, + 0, + true, +); +define_npc_type_data( + NpcType.Delsaber2, + "Delsaber (Ep. II)", + "Delsaber", + "Delsaber", + 2, + true, + undefined, + 0x0a0, + 0, + true, ); -define_npc_type_data(NpcType.PanArms2, "Pan Arms (Ep. II)", "Pan Arms", "Pan Arms", 2, true); -define_npc_type_data(NpcType.Migium2, "Migium (Ep. II)", "Migium", "Migium", 2, true); -define_npc_type_data(NpcType.Hidoom2, "Hidoom (Ep. II)", "Hidoom", "Hidoom", 2, true); -define_npc_type_data(NpcType.Dubchic2, "Dubchic (Ep. II)", "Dubchic", "Dubchich", 2, true); -define_npc_type_data(NpcType.Gilchic2, "Gilchic (Ep. II)", "Gilchic", "Gilchich", 2, true); -define_npc_type_data(NpcType.Garanz2, "Garanz (Ep. II)", "Garanz", "Baranz", 2, true); -define_npc_type_data(NpcType.Dubswitch2, "Dubswitch (Ep. II)", "Dubswitch", "Dubswitch", 2, true); -define_npc_type_data(NpcType.Delsaber2, "Delsaber (Ep. II)", "Delsaber", "Delsaber", 2, true); define_npc_type_data( NpcType.ChaosSorcerer2, "Chaos Sorcerer (Ep. II)", @@ -530,12 +1401,38 @@ define_npc_type_data( "Gran Sorcerer", 2, true, + undefined, + 0x0a1, + 0, + true, +); +define_npc_type_data( + NpcType.GolDragon, + "Gol Dragon", + "Gol Dragon", + "Gol Dragon", + 2, + true, + undefined, + 0x0cc, + 0, + true, ); -define_npc_type_data(NpcType.GolDragon, "Gol Dragon", "Gol Dragon", "Gol Dragon", 2, true); // Episode II Central Control Area -define_npc_type_data(NpcType.SinowBerill, "Sinow Berill", "Sinow Berill", "Sinow Berill", 2, true); +define_npc_type_data( + NpcType.SinowBerill, + "Sinow Berill", + "Sinow Berill", + "Sinow Berill", + 2, + true, + undefined, + 0x0d4, + 0, + true, +); define_npc_type_data( NpcType.SinowSpigell, "Sinow Spigell", @@ -543,34 +1440,291 @@ define_npc_type_data( "Sinow Spigell", 2, true, + undefined, + 0x0d4, + 1, + true, +); +define_npc_type_data( + NpcType.Merillia, + "Merillia", + "Merillia", + "Merillia", + 2, + true, + undefined, + 0x0d5, + 0, + true, +); +define_npc_type_data( + NpcType.Meriltas, + "Meriltas", + "Meriltas", + "Meriltas", + 2, + true, + undefined, + 0x0d5, + 1, + true, +); +define_npc_type_data( + NpcType.Mericarol, + "Mericarol", + "Mericarol", + "Mericarol", + 2, + true, + undefined, + 0x0d6, + 0, + true, +); +define_npc_type_data( + NpcType.Mericus, + "Mericus", + "Mericus", + "Mericus", + 2, + true, + undefined, + 0x0d6, + 1, + true, +); +define_npc_type_data( + NpcType.Merikle, + "Merikle", + "Merikle", + "Merikle", + 2, + true, + undefined, + 0x0d6, + 2, + true, +); +define_npc_type_data( + NpcType.UlGibbon, + "Ul Gibbon", + "Ul Gibbon", + "Ul Gibbon", + 2, + true, + undefined, + 0x0d7, + 0, + true, +); +define_npc_type_data( + NpcType.ZolGibbon, + "Zol Gibbon", + "Zol Gibbon", + "Zol Gibbon", + 2, + true, + undefined, + 0x0d7, + 1, + true, +); +define_npc_type_data( + NpcType.Gibbles, + "Gibbles", + "Gibbles", + "Gibbles", + 2, + true, + undefined, + 0x0d8, + 0, + true, +); +define_npc_type_data(NpcType.Gee, "Gee", "Gee", "Gee", 2, true, undefined, 0x0d9, 0, true); +define_npc_type_data( + NpcType.GiGue, + "Gi Gue", + "Gi Gue", + "Gi Gue", + 2, + true, + undefined, + 0x0da, + 0, + true, +); +define_npc_type_data( + NpcType.IllGill, + "Ill Gill", + "Ill Gill", + "Ill Gill", + 2, + true, + undefined, + 0x0e1, + 0, + true, +); +define_npc_type_data( + NpcType.DelLily, + "Del Lily", + "Del Lily", + "Del Lily", + 2, + true, + undefined, + 0x061, + 0, + true, +); +define_npc_type_data( + NpcType.Epsilon, + "Epsilon", + "Epsilon", + "Epsilon", + 2, + true, + undefined, + 0x0e0, + 0, + true, +); +define_npc_type_data( + NpcType.GalGryphon, + "Gal Gryphon", + "Gal Gryphon", + "Gal Gryphon", + 2, + true, + undefined, + 0x0c0, + 0, + true, ); -define_npc_type_data(NpcType.Merillia, "Merillia", "Merillia", "Merillia", 2, true); -define_npc_type_data(NpcType.Meriltas, "Meriltas", "Meriltas", "Meriltas", 2, true); -define_npc_type_data(NpcType.Mericarol, "Mericarol", "Mericarol", "Mericarol", 2, true); -define_npc_type_data(NpcType.Mericus, "Mericus", "Mericus", "Mericus", 2, true); -define_npc_type_data(NpcType.Merikle, "Merikle", "Merikle", "Merikle", 2, true); -define_npc_type_data(NpcType.UlGibbon, "Ul Gibbon", "Ul Gibbon", "Ul Gibbon", 2, true); -define_npc_type_data(NpcType.ZolGibbon, "Zol Gibbon", "Zol Gibbon", "Zol Gibbon", 2, true); -define_npc_type_data(NpcType.Gibbles, "Gibbles", "Gibbles", "Gibbles", 2, true); -define_npc_type_data(NpcType.Gee, "Gee", "Gee", "Gee", 2, true); -define_npc_type_data(NpcType.GiGue, "Gi Gue", "Gi Gue", "Gi Gue", 2, true); -define_npc_type_data(NpcType.IllGill, "Ill Gill", "Ill Gill", "Ill Gill", 2, true); -define_npc_type_data(NpcType.DelLily, "Del Lily", "Del Lily", "Del Lily", 2, true); -define_npc_type_data(NpcType.Epsilon, "Epsilon", "Epsilon", "Epsilon", 2, true); -define_npc_type_data(NpcType.GalGryphon, "Gal Gryphon", "Gal Gryphon", "Gal Gryphon", 2, true); // Episode II Seabed -define_npc_type_data(NpcType.Deldepth, "Deldepth", "Deldepth", "Deldepth", 2, true); -define_npc_type_data(NpcType.Delbiter, "Delbiter", "Delbiter", "Delbiter", 2, true); -define_npc_type_data(NpcType.Dolmolm, "Dolmolm", "Dolmolm", "Dolmolm", 2, true); -define_npc_type_data(NpcType.Dolmdarl, "Dolmdarl", "Dolmdarl", "Dolmdarl", 2, true); -define_npc_type_data(NpcType.Morfos, "Morfos", "Morfos", "Morfos", 2, true); -define_npc_type_data(NpcType.Recobox, "Recobox", "Recobox", "Recobox", 2, true); -define_npc_type_data(NpcType.Recon, "Recon", "Recon", "Recon", 2, true); -define_npc_type_data(NpcType.SinowZoa, "Sinow Zoa", "Sinow Zoa", "Sinow Zoa", 2, true); -define_npc_type_data(NpcType.SinowZele, "Sinow Zele", "Sinow Zele", "Sinow Zele", 2, true); -define_npc_type_data(NpcType.OlgaFlow, "Olga Flow", "Olga Flow", "Olga Flow", 2, true); +define_npc_type_data( + NpcType.Deldepth, + "Deldepth", + "Deldepth", + "Deldepth", + 2, + true, + undefined, + 0x0db, + 0, + true, +); +define_npc_type_data( + NpcType.Delbiter, + "Delbiter", + "Delbiter", + "Delbiter", + 2, + true, + undefined, + 0x0dc, + 0, + true, +); +define_npc_type_data( + NpcType.Dolmolm, + "Dolmolm", + "Dolmolm", + "Dolmolm", + 2, + true, + undefined, + 0x0dd, + 0, + true, +); +define_npc_type_data( + NpcType.Dolmdarl, + "Dolmdarl", + "Dolmdarl", + "Dolmdarl", + 2, + true, + undefined, + 0x0dd, + 1, + true, +); +define_npc_type_data( + NpcType.Morfos, + "Morfos", + "Morfos", + "Morfos", + 2, + true, + undefined, + 0x0de, + 0, + true, +); +define_npc_type_data( + NpcType.Recobox, + "Recobox", + "Recobox", + "Recobox", + 2, + true, + undefined, + 0x0df, + 0, + true, +); +define_npc_type_data( + NpcType.Recon, + "Recon", + "Recon", + "Recon", + 2, + true, + undefined, + undefined, + undefined, + undefined, +); +define_npc_type_data( + NpcType.SinowZoa, + "Sinow Zoa", + "Sinow Zoa", + "Sinow Zoa", + 2, + true, + undefined, + 0x0e0, + 0, + true, +); +define_npc_type_data( + NpcType.SinowZele, + "Sinow Zele", + "Sinow Zele", + "Sinow Zele", + 2, + true, + undefined, + 0x0e0, + 1, + true, +); +define_npc_type_data( + NpcType.OlgaFlow, + "Olga Flow", + "Olga Flow", + "Olga Flow", + 2, + true, + undefined, + 0x0ca, + 0, + true, +); // Episode IV @@ -582,9 +1736,34 @@ define_npc_type_data( 4, true, NpcType.DelRappy, + 0x041, + 0, + true, +); +define_npc_type_data( + NpcType.DelRappy, + "Del Rappy", + "Del Rappy", + "Del Rappy", + 4, + true, + undefined, + 0x041, + 1, + true, +); +define_npc_type_data( + NpcType.Astark, + "Astark", + "Astark", + "Astark", + 4, + true, + undefined, + 0x110, + 0, + true, ); -define_npc_type_data(NpcType.DelRappy, "Del Rappy", "Del Rappy", "Del Rappy", 4, true); -define_npc_type_data(NpcType.Astark, "Astark", "Astark", "Astark", 4, true); define_npc_type_data( NpcType.SatelliteLizard, "Satellite Lizard", @@ -592,8 +1771,12 @@ define_npc_type_data( "Satellite Lizard", 4, true, + undefined, + 0x111, + 0, + true, ); -define_npc_type_data(NpcType.Yowie, "Yowie", "Yowie", "Yowie", 4, true); +define_npc_type_data(NpcType.Yowie, "Yowie", "Yowie", "Yowie", 4, true, undefined, 0x111, 0, false); define_npc_type_data( NpcType.MerissaA, "Merissa A", @@ -602,14 +1785,72 @@ define_npc_type_data( 4, true, NpcType.MerissaAA, + 0x112, + 0, + true, +); +define_npc_type_data( + NpcType.MerissaAA, + "Merissa AA", + "Merissa AA", + "Merissa AA", + 4, + true, + undefined, + 0x112, + 1, + true, +); +define_npc_type_data( + NpcType.Girtablulu, + "Girtablulu", + "Girtablulu", + "Girtablulu", + 4, + true, + undefined, + 0x113, + 0, + true, +); +define_npc_type_data(NpcType.Zu, "Zu", "Zu", "Zu", 4, true, NpcType.Pazuzu, 0x114, 0, true); +define_npc_type_data( + NpcType.Pazuzu, + "Pazuzu", + "Pazuzu", + "Pazuzu", + 4, + true, + undefined, + 0x114, + 1, + true, +); +define_npc_type_data(NpcType.Boota, "Boota", "Boota", "Boota", 4, true, undefined, 0x115, 0, true); +define_npc_type_data( + NpcType.ZeBoota, + "Ze Boota", + "Ze Boota", + "Ze Boota", + 4, + true, + undefined, + 0x115, + 1, + true, +); +define_npc_type_data( + NpcType.BaBoota, + "Ba Boota", + "Ba Boota", + "Ba Boota", + 4, + true, + undefined, + 0x115, + 2, + true, ); -define_npc_type_data(NpcType.MerissaAA, "Merissa AA", "Merissa AA", "Merissa AA", 4, true); -define_npc_type_data(NpcType.Girtablulu, "Girtablulu", "Girtablulu", "Girtablulu", 4, true); -define_npc_type_data(NpcType.Zu, "Zu", "Zu", "Zu", 4, true, NpcType.Pazuzu); -define_npc_type_data(NpcType.Pazuzu, "Pazuzu", "Pazuzu", "Pazuzu", 4, true); -define_npc_type_data(NpcType.Boota, "Boota", "Boota", "Boota", 4, true); -define_npc_type_data(NpcType.ZeBoota, "Ze Boota", "Ze Boota", "Ze Boota", 4, true); -define_npc_type_data(NpcType.BaBoota, "Ba Boota", "Ba Boota", "Ba Boota", 4, true); define_npc_type_data( NpcType.Dorphon, "Dorphon", @@ -618,6 +1859,9 @@ define_npc_type_data( 4, true, NpcType.DorphonEclair, + 0x116, + 0, + true, ); define_npc_type_data( NpcType.DorphonEclair, @@ -626,9 +1870,24 @@ define_npc_type_data( "Dorphon Eclair", 4, true, + undefined, + 0x116, + 1, + true, +); +define_npc_type_data(NpcType.Goran, "Goran", "Goran", "Goran", 4, true, undefined, 0x117, 0, true); +define_npc_type_data( + NpcType.PyroGoran, + "Pyro Goran", + "Pyro Goran", + "Pyro Goran", + 4, + true, + undefined, + 0x117, + 1, + true, ); -define_npc_type_data(NpcType.Goran, "Goran", "Goran", "Goran", 4, true); -define_npc_type_data(NpcType.PyroGoran, "Pyro Goran", "Pyro Goran", "Pyro Goran", 4, true); define_npc_type_data( NpcType.GoranDetonator, "Goran Detonator", @@ -636,6 +1895,10 @@ define_npc_type_data( "Goran Detonator", 4, true, + undefined, + 0x117, + 2, + true, ); define_npc_type_data( NpcType.SaintMilion, @@ -645,6 +1908,9 @@ define_npc_type_data( 4, true, NpcType.Kondrieu, + 0x119, + 0, + true, ); define_npc_type_data( NpcType.Shambertin, @@ -654,5 +1920,19 @@ define_npc_type_data( 4, true, NpcType.Kondrieu, + 0x119, + 1, + true, +); +define_npc_type_data( + NpcType.Kondrieu, + "Kondrieu", + "Kondrieu", + "Kondrieu", + 4, + true, + undefined, + 0x119, + 0, + false, ); -define_npc_type_data(NpcType.Kondrieu, "Kondrieu", "Kondrieu", "Kondrieu", 4, true); diff --git a/src/core/observable/property/list/ListProperty.ts b/src/core/observable/property/list/ListProperty.ts index 162eb5ec..e26f313d 100644 --- a/src/core/observable/property/list/ListProperty.ts +++ b/src/core/observable/property/list/ListProperty.ts @@ -29,10 +29,7 @@ export interface ListProperty extends Property { get(index: number): T; - observe_list( - observer: (change: ListPropertyChangeEvent) => void, - options?: { call_now?: boolean }, - ): Disposable; + observe_list(observer: (change: ListPropertyChangeEvent) => void): Disposable; } export function is_list_property(observable: Observable): observable is ListProperty { diff --git a/src/core/observable/property/list/SimpleListProperty.ts b/src/core/observable/property/list/SimpleListProperty.ts index c86e609c..763dfa11 100644 --- a/src/core/observable/property/list/SimpleListProperty.ts +++ b/src/core/observable/property/list/SimpleListProperty.ts @@ -4,7 +4,7 @@ import { WritableProperty } from "../WritableProperty"; import { Observable } from "../../Observable"; import { property } from "../../index"; import { AbstractProperty } from "../AbstractProperty"; -import { Property } from "../Property"; +import { is_property, Property } from "../Property"; import { is_list_property, ListChangeType, ListPropertyChangeEvent } from "./ListProperty"; import Logger from "js-logger"; @@ -97,12 +97,18 @@ export class SimpleListProperty extends AbstractProperty bind_to(observable: Observable): Disposable { if (is_list_property(observable)) { + this.val = observable.val; + return observable.observe_list(change => { if (change.type === ListChangeType.ListChange) { this.splice(change.index, change.removed.length, ...change.inserted); } }); } else { + if (is_property(observable)) { + this.val = observable.val; + } + return observable.observe(({ value }) => this.set_val(value)); } } diff --git a/src/core/rendering/Renderer.ts b/src/core/rendering/Renderer.ts index 83ece4f2..8de5c888 100644 --- a/src/core/rendering/Renderer.ts +++ b/src/core/rendering/Renderer.ts @@ -72,10 +72,10 @@ export abstract class Renderer implements Disposable { this.schedule_render(); } - pointer_pos_to_device_coords(e: MouseEvent): Vector2 { + pointer_pos_to_device_coords(v: Vector2): Vector2 { const coords = this.renderer.getSize(new Vector2()); - coords.width = (e.offsetX / coords.width) * 2 - 1; - coords.height = (e.offsetY / coords.height) * -2 + 1; + coords.width = (v.x / coords.width) * 2 - 1; + coords.height = (v.y / coords.height) * -2 + 1; return coords; } diff --git a/src/quest_editor/gui/EntityListView.css b/src/quest_editor/gui/EntityListView.css new file mode 100644 index 00000000..158b7ec6 --- /dev/null +++ b/src/quest_editor/gui/EntityListView.css @@ -0,0 +1,22 @@ +.quest_editor_EntityListView { + overflow: auto; +} + +.quest_editor_EntityListView_entity_list { + display: grid; + grid-template-columns: repeat(auto-fill, 100px); + justify-content: center; +} + +.quest_editor_EntityListView_entity { + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; + height: 100px; + padding: 5px; + border: solid 2px olivedrab; + background-color: darkolivegreen; + color: greenyellow; +} diff --git a/src/quest_editor/gui/EntityListView.ts b/src/quest_editor/gui/EntityListView.ts new file mode 100644 index 00000000..bb74fec6 --- /dev/null +++ b/src/quest_editor/gui/EntityListView.ts @@ -0,0 +1,161 @@ +import { ResizableWidget } from "../../core/gui/ResizableWidget"; +import { bind_children_to, el } from "../../core/gui/dom"; +import "./EntityListView.css"; +import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/entities"; +import { ListProperty } from "../../core/observable/property/list/ListProperty"; +import { Vec2 } from "../../core/data_formats/vector"; +import { Disposable } from "../../core/observable/Disposable"; + +export abstract class EntityListView extends ResizableWidget { + readonly element: HTMLElement; + + protected constructor(private readonly class_name: string, entities: ListProperty) { + super(); + + const list_element = el.div({ class: "quest_editor_EntityListView_entity_list" }); + + this.element = el.div({ class: `${class_name} quest_editor_EntityListView` }, list_element); + + this.disposables( + bind_children_to(list_element, entities, this.create_entity_element), + + make_draggable(list_element, target => { + if (target !== list_element) { + const drag_element = target.cloneNode(true) as HTMLElement; + drag_element.style.width = "100px"; + return [drag_element, entities.get(parseInt(target.dataset.index!, 10))]; + } else { + return undefined; + } + }), + ); + } + + private create_entity_element = (entity: T, index: number): HTMLElement => { + const div = el.div({ + class: "quest_editor_EntityListView_entity", + text: entity_data(entity).name, + data: { index: index.toString() }, + }); + + div.draggable = true; + + return div; + }; +} + +export type EntityDrag = { + readonly offset_x: number; + readonly offset_y: number; + readonly data_transfer: DataTransfer; + readonly drag_element: HTMLElement; + readonly entity_type: EntityType; +}; + +function make_draggable( + element: HTMLElement, + start: (target: HTMLElement) => [HTMLElement, any] | undefined, +): Disposable { + let detail: { drag_element: HTMLElement; entity_type: EntityType } | undefined; + const grab_point = new Vec2(0, 0); + + function clear(): void { + if (detail) { + detail.drag_element.remove(); + detail = undefined; + } + } + + function redispatch(e: DragEvent): void { + if (e.target instanceof HTMLElement && detail && e.dataTransfer) { + e.target.dispatchEvent( + new CustomEvent(`phantasmal-${e.type}`, { + detail: { + ...detail, + data_transfer: e.dataTransfer, + offset_x: e.offsetX, + offset_y: e.offsetY, + }, + bubbles: true, + }), + ); + } + } + + function dragstart(e: DragEvent): void { + if (e.target instanceof HTMLElement) { + clear(); + + const result = start(e.target); + + if (result) { + grab_point.set(e.offsetX + 2, e.offsetY + 2); + + detail = { + drag_element: result[0], + entity_type: result[1], + }; + + detail.drag_element.style.position = "fixed"; + detail.drag_element.style.pointerEvents = "none"; + detail.drag_element.style.zIndex = "500"; + detail.drag_element.style.top = "0"; + detail.drag_element.style.left = "0"; + detail.drag_element.style.transform = `translate(${e.clientX - + grab_point.x}px, ${e.clientY - grab_point.y}px)`; + document.body.append(detail.drag_element); + + e.dataTransfer!.setDragImage(el.div(), 0, 0); + } + } + } + + function dragenter(e: DragEvent): void { + redispatch(e); + } + + function dragover(e: DragEvent): void { + if (e.target instanceof HTMLElement && detail) { + detail.drag_element.style.transform = `translate(${e.clientX - + grab_point.x}px, ${e.clientY - grab_point.y}px)`; + + redispatch(e); + } + } + + function dragleave(e: DragEvent): void { + redispatch(e); + } + + function dragend(): void { + clear(); + } + + function drop(e: DragEvent): void { + try { + redispatch(e); + } finally { + clear(); + } + } + + element.addEventListener("dragstart", dragstart); + document.addEventListener("dragenter", dragenter); + document.addEventListener("dragover", dragover); + document.addEventListener("dragleave", dragleave); + document.addEventListener("dragend", dragend); + document.addEventListener("drop", drop); + + return { + dispose(): void { + element.removeEventListener("dragstart", dragstart); + document.removeEventListener("dragenter", dragenter); + document.removeEventListener("dragover", dragover); + document.removeEventListener("dragleave", dragleave); + document.removeEventListener("dragend", dragend); + document.removeEventListener("drop", drop); + + clear(); + }, + }; +} diff --git a/src/quest_editor/gui/NpcCountsView.ts b/src/quest_editor/gui/NpcCountsView.ts index b4fa5f64..d402b88b 100644 --- a/src/quest_editor/gui/NpcCountsView.ts +++ b/src/quest_editor/gui/NpcCountsView.ts @@ -2,9 +2,10 @@ import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { el } from "../../core/gui/dom"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { npc_data, NpcType } from "../../core/data_formats/parsing/quest/npc_types"; -import { QuestModel } from "../model/QuestModel"; import "./NpcCountsView.css"; import { DisabledView } from "./DisabledView"; +import { property } from "../../core/observable"; +import { QuestNpcModel } from "../model/QuestNpcModel"; export class NpcCountsView extends ResizableWidget { readonly element = el.div({ class: "quest_editor_NpcCountsView" }); @@ -26,24 +27,24 @@ export class NpcCountsView extends ResizableWidget { this.disposables( this.no_quest_view.visible.bind_to(no_quest), - quest.observe(({ value }) => this.update_view(value), { - call_now: true, - }), + quest + .flat_map(quest => (quest ? quest.npcs : property([]))) + .observe(({ value: npcs }) => this.update_view(npcs), { + call_now: true, + }), ); this.finalize_construction(NpcCountsView.prototype); } - private update_view(quest?: QuestModel): void { + private update_view(npcs: QuestNpcModel[]): void { const frag = document.createDocumentFragment(); const npc_counts = new Map(); - if (quest) { - for (const npc of quest.npcs.val) { - const val = npc_counts.get(npc.type) || 0; - npc_counts.set(npc.type, val + 1); - } + for (const npc of npcs) { + const val = npc_counts.get(npc.type) || 0; + npc_counts.set(npc.type, val + 1); } const extra_canadines = (npc_counts.get(NpcType.Canane) || 0) * 8; diff --git a/src/quest_editor/gui/NpcListView.ts b/src/quest_editor/gui/NpcListView.ts new file mode 100644 index 00000000..6c371d80 --- /dev/null +++ b/src/quest_editor/gui/NpcListView.ts @@ -0,0 +1,11 @@ +import { EntityListView } from "./EntityListView"; +import { NPC_TYPES, NpcType } from "../../core/data_formats/parsing/quest/npc_types"; +import { list_property } from "../../core/observable"; + +export class NpcListView extends EntityListView { + constructor() { + super("quest_editor_NpcListView", list_property(undefined, ...NPC_TYPES)); + + this.finalize_construction(NpcListView.prototype); + } +} diff --git a/src/quest_editor/gui/QuestEditorView.ts b/src/quest_editor/gui/QuestEditorView.ts index 1a9f8faf..9477c5d2 100644 --- a/src/quest_editor/gui/QuestEditorView.ts +++ b/src/quest_editor/gui/QuestEditorView.ts @@ -12,6 +12,7 @@ import { AsmEditorView } from "./AsmEditorView"; import { EntityInfoView } from "./EntityInfoView"; import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { quest_editor_store } from "../stores/QuestEditorStore"; +import { NpcListView } from "./NpcListView"; import Logger = require("js-logger"); const logger = Logger.get("quest_editor/gui/QuestEditorView"); @@ -23,7 +24,7 @@ const VIEW_TO_NAME = new Map ResizableWidget, string>([ [QuestRendererView, "quest_renderer"], [AsmEditorView, "asm_editor"], [EntityInfoView, "entity_info"], - // [AddObjectView, "add_object"], + [NpcListView, "npc_list_view"], ]); const DEFAULT_LAYOUT_CONFIG = { @@ -83,11 +84,22 @@ const DEFAULT_LAYOUT_CONTENT: ItemConfigType[] = [ ], }, { - title: "Entity", - type: "component", - componentName: VIEW_TO_NAME.get(EntityInfoView), - isClosable: false, + type: "stack", width: 2, + content: [ + { + title: "Entity", + type: "component", + componentName: VIEW_TO_NAME.get(EntityInfoView), + isClosable: false, + }, + { + title: "NPCs", + type: "component", + componentName: VIEW_TO_NAME.get(NpcListView), + isClosable: false, + }, + ], }, ], }, diff --git a/src/quest_editor/loading/areas.ts b/src/quest_editor/loading/areas.ts index 85cb754e..cd039ca1 100644 --- a/src/quest_editor/loading/areas.ts +++ b/src/quest_editor/loading/areas.ts @@ -11,6 +11,7 @@ import { area_collision_geometry_to_object_3d, area_geometry_to_sections_and_object_3d, } from "../rendering/conversion/areas"; +import { AreaVariantModel } from "../model/AreaVariantModel"; const render_geometry_cache = new LoadingCache< string, @@ -20,46 +21,47 @@ const collision_geometry_cache = new LoadingCache>(); export async function load_area_sections( episode: Episode, - area_id: number, - area_variant: number, + area_variant: AreaVariantModel, ): Promise { - return render_geometry_cache.get_or_set(`${episode}-${area_id}-${area_variant}`, () => - load_area_sections_and_render_geometry(episode, area_id, area_variant), + return render_geometry_cache.get_or_set( + `${episode}-${area_variant.area.id}-${area_variant.id}`, + () => load_area_sections_and_render_geometry(episode, area_variant), ).sections; } export async function load_area_render_geometry( episode: Episode, - area_id: number, - area_variant: number, + area_variant: AreaVariantModel, ): Promise { - return render_geometry_cache.get_or_set(`${episode}-${area_id}-${area_variant}`, () => - load_area_sections_and_render_geometry(episode, area_id, area_variant), + return render_geometry_cache.get_or_set( + `${episode}-${area_variant.area.id}-${area_variant.id}`, + () => load_area_sections_and_render_geometry(episode, area_variant), ).geometry; } export async function load_area_collision_geometry( episode: Episode, - area_id: number, - area_variant: number, + area_variant: AreaVariantModel, ): 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)), + return collision_geometry_cache.get_or_set( + `${episode}-${area_variant.area.id}-${area_variant.id}`, + () => + get_area_asset(episode, 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: Episode, - area_id: number, - area_variant: number, + area_variant: AreaVariantModel, ): { geometry: Promise; sections: Promise } { - const promise = get_area_asset(episode, area_id, area_variant, "render").then(buffer => + const promise = get_area_asset(episode, area_variant, "render").then(buffer => area_geometry_to_sections_and_object_3d( parse_area_geometry(new ArrayBufferCursor(buffer, Endianness.Little)), + area_variant, ), ); @@ -126,21 +128,23 @@ const area_base_names = [ async function get_area_asset( episode: Episode, - area_id: number, - area_variant: number, + area_variant: AreaVariantModel, type: "render" | "collision", ): Promise { - const base_url = area_version_to_base_url(episode, area_id, area_variant); + const base_url = area_version_to_base_url(episode, area_variant); const suffix = type === "render" ? "n.rel" : "c.rel"; return load_array_buffer(base_url + suffix); } -function area_version_to_base_url(episode: Episode, area_id: number, area_variant: number): string { - // Exception for Seaside area at night variant 1. +function area_version_to_base_url(episode: Episode, area_variant: AreaVariantModel): string { + let area_id = area_variant.area.id; + let area_variant_id = area_variant.id; + + // Exception for Seaside area at night, variant 1. // Phantasmal World 4 and Lost heart breaker use this to have two tower maps. - if (area_id === 16 && area_variant === 1) { + if (area_id === 16 && area_variant_id === 1) { area_id = 17; - area_variant = 1; + area_variant_id = 1; } const episode_base_names = area_base_names[episode - 1]; @@ -148,20 +152,20 @@ function area_version_to_base_url(episode: Episode, area_id: number, area_varian 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) { + if (0 <= area_variant_id && area_variant_id < variants) { let variant: string; if (variants === 1) { variant = ""; } else { - variant = String(area_variant); - while (variant.length < 2) variant = "0" + variant; + variant = String(area_variant_id); + variant = variant.padStart(2, "0"); } return `/maps/map_${base_name}${variant}`; } else { throw new Error( - `Unknown variant ${area_variant} of area ${area_id} in episode ${episode}.`, + `Unknown variant ${area_variant_id} of area ${area_id} in episode ${episode}.`, ); } } else { diff --git a/src/quest_editor/model/AreaVariantModel.ts b/src/quest_editor/model/AreaVariantModel.ts index 919c5ba0..36bdc9b0 100644 --- a/src/quest_editor/model/AreaVariantModel.ts +++ b/src/quest_editor/model/AreaVariantModel.ts @@ -5,11 +5,10 @@ import { AreaModel } from "./AreaModel"; import { SectionModel } from "./SectionModel"; export class AreaVariantModel { - readonly id: number; - - readonly area: AreaModel; - private readonly _sections: WritableListProperty = list_property(); + + readonly id: number; + readonly area: AreaModel; readonly sections: ListProperty = this._sections; constructor(id: number, area: AreaModel) { diff --git a/src/quest_editor/model/QuestEntityModel.ts b/src/quest_editor/model/QuestEntityModel.ts index 06706e98..f0fc8911 100644 --- a/src/quest_editor/model/QuestEntityModel.ts +++ b/src/quest_editor/model/QuestEntityModel.ts @@ -73,6 +73,12 @@ export abstract class QuestEntityModel { position: Vec3, rotation: Vec3, ) { + if (type == undefined) throw new Error("type is required."); + if (!Number.isInteger(area_id)) throw new Error("area_id should be an integer."); + if (!Number.isInteger(section_id)) throw new Error("section_id should be an integer."); + if (!position) throw new Error("position is required."); + if (!rotation) throw new Error("rotation is required."); + this.type = type; this.area_id = area_id; this.section = this._section; diff --git a/src/quest_editor/model/QuestModel.ts b/src/quest_editor/model/QuestModel.ts index d956dec9..4e5e351b 100644 --- a/src/quest_editor/model/QuestModel.ts +++ b/src/quest_editor/model/QuestModel.ts @@ -11,6 +11,7 @@ import { AreaVariantModel } from "./AreaVariantModel"; import { area_store } from "../stores/AreaStore"; import { ListProperty } from "../../core/observable/property/list/ListProperty"; import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty"; +import { QuestEntityModel } from "./QuestEntityModel"; const logger = Logger.get("quest_editor/model/QuestModel"); @@ -110,6 +111,7 @@ export class QuestModel { private readonly _long_description: WritableProperty = property(""); private readonly _map_designations: WritableProperty>; private readonly _area_variants: WritableListProperty = list_property(); + private readonly _npcs: WritableListProperty; constructor( id: number, @@ -149,7 +151,8 @@ export class QuestModel { this._map_designations = property(map_designations); this.map_designations = this._map_designations; this.objects = list_property(undefined, ...objects); - this.npcs = list_property(undefined, ...npcs); + this._npcs = list_property(undefined, ...npcs); + this.npcs = this._npcs; this.dat_unknowns = dat_unknowns; this.object_code = object_code; this.shop_items = shop_items; @@ -176,6 +179,18 @@ export class QuestModel { this.map_designations.observe(this.update_area_variants); } + add_npc(npc: QuestNpcModel): void { + this._npcs.push(npc); + } + + remove_entity(entity: QuestEntityModel): void { + if (entity instanceof QuestNpcModel) { + this._npcs.remove(entity); + } else { + // TODO: objects + } + } + private update_area_variants = (): void => { const variants = new Map(); diff --git a/src/quest_editor/model/QuestNpcModel.ts b/src/quest_editor/model/QuestNpcModel.ts index b1049e0b..7f428289 100644 --- a/src/quest_editor/model/QuestNpcModel.ts +++ b/src/quest_editor/model/QuestNpcModel.ts @@ -6,7 +6,7 @@ export class QuestNpcModel extends QuestEntityModel { readonly pso_type_id: number; readonly npc_id: number; readonly script_label: number; - readonly roaming: number; + readonly pso_roaming: number; readonly scale: Vec3; /** * Data of which the purpose hasn't been discovered yet. @@ -18,7 +18,7 @@ export class QuestNpcModel extends QuestEntityModel { pso_type_id: number, npc_id: number, script_label: number, - roaming: number, + pso_roaming: number, area_id: number, section_id: number, position: Vec3, @@ -26,12 +26,27 @@ export class QuestNpcModel extends QuestEntityModel { scale: Vec3, unknown: number[][], ) { + if (!Number.isInteger(pso_type_id)) throw new Error("pso_type_id should be an integer."); + if (!Number.isFinite(npc_id)) throw new Error("npc_id should be a number."); + if (!Number.isInteger(script_label)) throw new Error("script_label should be an integer."); + if (!Number.isInteger(pso_roaming)) throw new Error("pso_roaming should be an integer."); + if (!scale) throw new Error("scale is required."); + if (!unknown) throw new Error("unknown is required."); + if (unknown.length !== 3) + throw new Error(`unknown should be of length 3, was ${unknown.length}.`); + if (unknown[0].length !== 10) + throw new Error(`unknown[0] should be of length 10, was ${unknown[0].length}`); + if (unknown[1].length !== 6) + throw new Error(`unknown[1] should be of length 6, was ${unknown[1].length}`); + if (unknown[2].length !== 4) + throw new Error(`unknown[2] should be of length 4, was ${unknown[2].length}`); + super(type, area_id, section_id, position, rotation); this.pso_type_id = pso_type_id; this.npc_id = npc_id; this.script_label = script_label; - this.roaming = roaming; + this.pso_roaming = pso_roaming; this.unknown = unknown; this.scale = scale; } diff --git a/src/quest_editor/model/SectionModel.ts b/src/quest_editor/model/SectionModel.ts index 81f558c2..42ed6b27 100644 --- a/src/quest_editor/model/SectionModel.ts +++ b/src/quest_editor/model/SectionModel.ts @@ -1,4 +1,5 @@ import { Vec3 } from "../../core/data_formats/vector"; +import { AreaVariantModel } from "./AreaVariantModel"; export class SectionModel { readonly id: number; @@ -6,17 +7,25 @@ export class SectionModel { readonly y_axis_rotation: number; readonly sin_y_axis_rotation: number; readonly cos_y_axis_rotation: number; + readonly area_variant: AreaVariantModel; - constructor(id: number, position: Vec3, y_axis_rotation: number) { + constructor( + id: number, + position: Vec3, + y_axis_rotation: number, + area_variant: AreaVariantModel, + ) { if (!Number.isInteger(id) || id < -1) throw new Error(`Expected id to be an integer greater than or equal to -1, got ${id}.`); if (!position) throw new Error("position is required."); if (!Number.isFinite(y_axis_rotation)) throw new Error("y_axis_rotation is required."); + if (!area_variant) throw new Error("area_variant is required."); this.id = id; this.position = position; this.y_axis_rotation = y_axis_rotation; this.sin_y_axis_rotation = Math.sin(this.y_axis_rotation); this.cos_y_axis_rotation = Math.cos(this.y_axis_rotation); + this.area_variant = area_variant; } } diff --git a/src/quest_editor/rendering/QuestEntityControls.ts b/src/quest_editor/rendering/QuestEntityControls.ts index 9ac48a1b..fb32d0c6 100644 --- a/src/quest_editor/rendering/QuestEntityControls.ts +++ b/src/quest_editor/rendering/QuestEntityControls.ts @@ -9,6 +9,9 @@ import { AreaUserData } from "./conversion/areas"; import { SectionModel } from "../model/SectionModel"; import { Disposable } from "../../core/observable/Disposable"; import { Disposer } from "../../core/observable/Disposer"; +import { EntityDrag } from "../gui/EntityListView"; +import { is_npc_type } from "../../core/data_formats/parsing/quest/entities"; +import { npc_data } from "../../core/data_formats/parsing/quest/npc_types"; const DOWN_VECTOR = new Vector3(0, -1, 0); @@ -18,10 +21,28 @@ type Highlighted = { }; type Pick = { + /** + * Whether we picked an entity that is being created or one that has existed before. + */ + creating: boolean; + initial_section?: SectionModel; + initial_position: Vec3; + + /** + * Vector that points from the grabbing point to the model's origin. + */ grab_offset: Vector3; + + /** + * Vector that points from the grabbing point to the terrain point directly under the model's origin. + */ drag_adjust: Vector3; + + /** + * Distance to terrain. + */ drag_y: number; }; @@ -57,6 +78,13 @@ export class QuestEntityControls implements Disposable { } }), ); + + renderer.dom_element.addEventListener("mousedown", this.mousedown); + renderer.dom_element.addEventListener("mousemove", this.mousemove); + renderer.dom_element.addEventListener("mouseup", this.mouseup); + renderer.dom_element.addEventListener("phantasmal-dragenter", this.dragenter); + renderer.dom_element.addEventListener("phantasmal-dragover", this.dragover); + renderer.dom_element.addEventListener("phantasmal-dragleave", this.dragleave); } dispose(): void { @@ -71,14 +99,22 @@ export class QuestEntityControls implements Disposable { if (mesh) { this.select({ entity, mesh }); + } else { + if (this.selected) { + set_color(this.selected, ColorType.Normal); + } + + this.selected = undefined; } }; - on_mouse_down = (e: MouseEvent) => { + private mousedown = (e: MouseEvent) => { this.process_event(e); this.stop_transforming(); - const new_pick = this.pick_entity(this.renderer.pointer_pos_to_device_coords(e)); + const new_pick = this.pick_entity( + this.renderer.pointer_pos_to_device_coords(new Vector2(e.offsetX, e.offsetY)), + ); if (new_pick) { // Disable camera controls while the user is transforming an entity. @@ -93,25 +129,12 @@ export class QuestEntityControls implements Disposable { this.renderer.schedule_render(); }; - on_mouse_up = (e: MouseEvent) => { + private mousemove = (e: MouseEvent) => { this.process_event(e); - if (!this.moved_since_last_mouse_down && !this.pick) { - // If the user clicks on nothing, deselect the currently selected entity. - this.deselect(); - } - - this.stop_transforming(); - // Enable camera controls again after transforming an entity. - this.renderer.controls.enabled = true; - - this.renderer.schedule_render(); - }; - - on_mouse_move = (e: MouseEvent) => { - this.process_event(e); - - const pointer_device_pos = this.renderer.pointer_pos_to_device_coords(e); + const pointer_device_pos = this.renderer.pointer_pos_to_device_coords( + new Vector2(e.offsetX, e.offsetY), + ); if (this.selected && this.pick) { if (this.moved_since_last_mouse_down) { @@ -139,19 +162,168 @@ export class QuestEntityControls implements Disposable { } }; - private process_event(e: MouseEvent): void { - if (e.type === "mousedown") { + private mouseup = (e: MouseEvent) => { + this.process_event(e); + + if (!this.moved_since_last_mouse_down && !this.pick) { + // If the user clicks on nothing, deselect the currently selected entity. + this.deselect(); + } + + this.stop_transforming(); + // Enable camera controls again after transforming an entity. + this.renderer.controls.enabled = true; + + this.renderer.schedule_render(); + }; + + private dragenter = (e: Event) => { + const area = quest_editor_store.current_area.val; + if (!area) return; + + const detail = (e as CustomEvent).detail; + + const pointer_position = this.renderer.pointer_pos_to_device_coords( + new Vector2(detail.offset_x, detail.offset_y), + ); + + const { intersection, section } = this.pick_terrain(pointer_position, new Vector3()); + + let position: Vec3 | undefined; + + if (intersection) { + position = new Vec3(intersection.point.x, intersection.point.y, intersection.point.z); + } else { + // If the cursor is not over any terrain, we translate the entity across the horizontal plane in which the origin lies. + this.raycaster.setFromCamera(pointer_position, this.renderer.camera); + const ray = this.raycaster.ray; + const plane = new Plane(new Vector3(0, 1, 0), 0); + const intersection_point = new Vector3(); + + if (ray.intersectPlane(plane, intersection_point)) { + position = new Vec3(intersection_point.x, 0, intersection_point.z); + } + } + + const quest = quest_editor_store.current_quest.val; + + if (quest && position) { + if (is_npc_type(detail.entity_type)) { + const data = npc_data(detail.entity_type); + + if (data.pso_type_id !== undefined && data.pso_roaming != undefined) { + detail.drag_element.style.display = "none"; + detail.data_transfer.dropEffect = "copy"; + + const npc = new QuestNpcModel( + detail.entity_type, + data.pso_type_id, + 0, + 0, + data.pso_roaming, + section ? section.area_variant.area.id : area.id, + section ? section.id : 0, + new Vec3(0, 0, 0), + new Vec3(0, 0, 0), + new Vec3(1, 1, 1), + // TODO: do the following values make sense? + [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0]], + ); + npc.set_world_position(position); + quest.add_npc(npc); + + quest_editor_store.set_selected_entity(npc); + + this.pick = { + creating: true, + initial_section: section, + initial_position: position, + grab_offset: new Vector3(0, 0, 0), + drag_adjust: new Vector3(0, 0, 0), + drag_y: 0, + }; + } + } + } + }; + + private dragover = (e: Event) => { + if (!quest_editor_store.current_area.val) return; + + const detail = (e as CustomEvent).detail; + detail.data_transfer.dropEffect = "copy"; + + if (this.selected && this.pick) { + const pointer_device_pos = this.renderer.pointer_pos_to_device_coords( + new Vector2(detail.offset_x, detail.offset_y), + ); + + // TODO: + // if (e.buttons === 1) { + // User is transforming selected entity. + // User is dragging selected entity. + // if (e.shiftKey) { + // Vertical movement. + // this.translate_vertically(this.selected, this.pick, pointer_device_pos); + // } else { + // Horizontal movement across terrain. + this.translate_horizontally(this.selected, this.pick, pointer_device_pos); + // } + // } + + this.renderer.schedule_render(); + } + }; + + private dragleave = (e: Event) => { + if (!quest_editor_store.current_area.val) return; + + const detail = (e as CustomEvent).detail; + if (detail.drag_element) detail.drag_element.style.display = "flex"; + + const quest = quest_editor_store.current_quest.val; + + if (quest && this.selected && this.selected.entity.type == detail.entity_type) { + quest.remove_entity(this.selected.entity); + } + }; + + private drop = () => { + // TODO: push onto undo stack. + }; + + private process_event(e: Event): void { + let offset_x: number; + let offset_y: number; + + if (e instanceof MouseEvent) { + offset_x = e.offsetX; + offset_y = e.offsetY; + } else if (e instanceof CustomEvent) { + const detail = (e as CustomEvent).detail; + + if (!("offset_x" in detail && "offset_y" in detail)) { + return; + } + + offset_x = detail.offset_x; + offset_y = detail.offset_y; + } else { + return; + } + + if (e.type === "mousedown" || e.type === "phantasmal-dragenter") { this.moved_since_last_mouse_down = false; } else { if ( - e.offsetX !== this.last_pointer_position.x || - e.offsetY !== this.last_pointer_position.y + offset_x !== this.last_pointer_position.x || + offset_y !== this.last_pointer_position.y ) { this.moved_since_last_mouse_down = true; } } - this.last_pointer_position.set(e.offsetX, e.offsetY); + this.last_pointer_position.set(offset_x, offset_y); } /** @@ -246,7 +418,7 @@ export class QuestEntityControls implements Disposable { pointer_position: Vector2, ): void { // Cast ray adjusted for dragging entities. - const { intersection, section } = this.pick_terrain(pointer_position, pick); + const { intersection, section } = this.pick_terrain(pointer_position, pick.drag_adjust); if (intersection) { selection.entity.set_world_position( @@ -284,7 +456,7 @@ export class QuestEntityControls implements Disposable { } private stop_transforming = () => { - if (this.moved_since_last_mouse_down && this.selected && this.pick) { + if (this.moved_since_last_mouse_down && this.selected && this.pick && !this.pick.creating) { const entity = this.selected.entity; quest_editor_store.push_translate_entity_action( entity, @@ -314,11 +486,8 @@ export class QuestEntityControls implements Disposable { } 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. @@ -334,6 +503,7 @@ export class QuestEntityControls implements Disposable { } return { + creating: false, mesh: intersection.object as Mesh, entity, initial_section: entity.section.val, @@ -346,17 +516,17 @@ export class QuestEntityControls implements Disposable { /** * @param pointer_pos - pointer coordinates in normalized device space - * @param data - entity picking data + * @param drag_adjust - vector from origin of entity to grabbing point */ private pick_terrain( pointer_pos: Vector2, - data: Pick, + drag_adjust: Vector3, ): { intersection?: Intersection; section?: SectionModel; } { this.raycaster.setFromCamera(pointer_pos, this.renderer.camera); - this.raycaster.ray.origin.add(data.drag_adjust); + this.raycaster.ray.origin.add(drag_adjust); const intersections = this.raycaster.intersectObjects( this.renderer.collision_geometry.children, true, diff --git a/src/quest_editor/rendering/QuestModelManager.ts b/src/quest_editor/rendering/QuestModelManager.ts index b342a623..24074afb 100644 --- a/src/quest_editor/rendering/QuestModelManager.ts +++ b/src/quest_editor/rendering/QuestModelManager.ts @@ -13,10 +13,16 @@ import { QuestEntityModel } from "../model/QuestEntityModel"; import { Disposer } from "../../core/observable/Disposer"; import { Disposable } from "../../core/observable/Disposable"; import { AreaModel } from "../model/AreaModel"; -import { AreaVariantModel } from "../model/AreaVariantModel"; -import { area_store } from "../stores/AreaStore"; import { create_npc_mesh, create_object_mesh } from "./conversion/entities"; import { AreaUserData } from "./conversion/areas"; +import { quest_editor_store } from "../stores/QuestEditorStore"; +import { + ListChangeType, + ListPropertyChangeEvent, +} from "../../core/observable/property/list/ListProperty"; +import { QuestNpcModel } from "../model/QuestNpcModel"; +import { QuestObjectModel } from "../model/QuestObjectModel"; +import { entity_type_to_string } from "../../core/data_formats/parsing/quest/entities"; const logger = Logger.get("quest_editor/rendering/QuestModelManager"); @@ -25,122 +31,153 @@ const CAMERA_LOOK_AT = new Vector3(0, 0, 0); const DUMMY_OBJECT = new Object3D(); export class QuestModelManager implements Disposable { - private quest?: QuestModel; - private area?: AreaModel; - private area_variant?: AreaVariantModel; - private disposer = new Disposer(); + private readonly disposer = new Disposer(); + private readonly quest_disposer = this.disposer.add(new Disposer()); + private readonly area_model_manager: AreaModelManager; + private readonly npc_model_manager: EntityModelManager; + private readonly object_model_manager: EntityModelManager; - constructor(private renderer: QuestRenderer) {} + constructor(private readonly renderer: QuestRenderer) { + this.area_model_manager = new AreaModelManager(this.renderer); + this.npc_model_manager = new EntityModelManager(this.renderer); + this.object_model_manager = new EntityModelManager(this.renderer); - async load_models(quest?: QuestModel, area?: AreaModel): Promise { - let area_variant: AreaVariantModel | undefined; + this.disposer.add_all( + quest_editor_store.current_quest.observe(this.quest_or_area_changed), - if (quest && area) { - area_variant = - quest.area_variants.val.find(v => v.area.id === area.id) || - area_store.get_variant(quest.episode, area.id, 0); - } - - if (this.quest === quest && this.area_variant === area_variant) { - return; - } - - this.quest = quest; - this.area = area; - this.area_variant = area_variant; - - this.disposer.dispose_all(); - - if (quest && area) { - try { - // Load necessary area geometry. - const episode = quest.episode; - const area_id = area.id; - const variant_id = area_variant ? area_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, - ); - - this.add_sections_to_collision_geometry(collision_geometry, render_geometry); - - if (this.quest !== quest || this.area_variant !== area_variant) return; - - this.renderer.collision_geometry = collision_geometry; - this.renderer.render_geometry = render_geometry; - - this.renderer.reset_camera(CAMERA_POSITION, CAMERA_LOOK_AT); - - // Load entity models. - this.renderer.reset_entity_models(); - - for (const npc of quest.npcs.val) { - 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_variant !== area_variant) return; - - const model = create_npc_mesh(npc, npc_geom, npc_tex); - this.update_entity_geometry(npc, model); - } - } - - for (const object of quest.objects.val) { - 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_variant !== area_variant) return; - - const model = create_object_mesh(object, object_geom, object_tex); - this.update_entity_geometry(object, 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.reset_entity_models(); - } - } else { - this.renderer.collision_geometry = DUMMY_OBJECT; - this.renderer.render_geometry = DUMMY_OBJECT; - this.renderer.reset_entity_models(); - } + quest_editor_store.current_area.observe(this.quest_or_area_changed), + ); } dispose(): void { this.disposer.dispose(); } + private quest_or_area_changed = async () => { + this.quest_disposer.dispose_all(); + this.npc_model_manager.remove_all(); + this.object_model_manager.remove_all(); + this.renderer.reset_entity_models(); + + const quest = quest_editor_store.current_quest.val; + const area = quest_editor_store.current_area.val; + + // Load area model. + await this.area_model_manager.load(quest, area); + + if ( + quest !== quest_editor_store.current_quest.val || + area !== quest_editor_store.current_area.val + ) { + return; + } + + // Load entity models. + if (quest && area) { + this.npc_model_manager.add(quest.npcs.val.filter(entity => entity.area_id === area.id)); + this.object_model_manager.add( + quest.objects.val.filter(entity => entity.area_id === area.id), + ); + this.quest_disposer.add_all( + quest.npcs.observe_list(this.npcs_changed), + quest.objects.observe_list(this.objects_changed), + ); + } + }; + + private npcs_changed = (change: ListPropertyChangeEvent): void => { + const area = quest_editor_store.current_area.val; + + if (change.type === ListChangeType.ListChange && area) { + this.npc_model_manager.remove(change.removed); + + this.npc_model_manager.add( + change.inserted.filter(entity => entity.area_id === area.id), + ); + } + }; + + private objects_changed = (change: ListPropertyChangeEvent): void => { + const area = quest_editor_store.current_area.val; + + if (change.type === ListChangeType.ListChange && area) { + this.object_model_manager.remove(change.removed); + + this.object_model_manager.add( + change.inserted.filter(entity => entity.area_id === area.id), + ); + } + }; +} + +class AreaModelManager { + private readonly raycaster = new Raycaster(); + private readonly origin = new Vector3(); + private readonly down = new Vector3(0, -1, 0); + private readonly up = new Vector3(0, 1, 0); + private quest?: QuestModel; + private area?: AreaModel; + + constructor(private readonly renderer: QuestRenderer) {} + + async load(quest?: QuestModel, area?: AreaModel): Promise { + this.quest = quest; + this.area = area; + + if (!quest || !area) { + this.renderer.collision_geometry = DUMMY_OBJECT; + this.renderer.render_geometry = DUMMY_OBJECT; + return; + } + + try { + const area_variant = + quest.area_variants.val.find(v => v.area.id === area.id) || area.area_variants[0]; + + // Load necessary area geometry. + const episode = quest.episode; + + const collision_geometry = await load_area_collision_geometry(episode, area_variant); + if (this.should_cancel(quest, area)) return; + + const render_geometry = await load_area_render_geometry(episode, area_variant); + if (this.should_cancel(quest, area)) return; + + this.add_sections_to_collision_geometry(collision_geometry, render_geometry); + + this.renderer.collision_geometry = collision_geometry; + this.renderer.render_geometry = render_geometry; + + this.renderer.reset_camera(CAMERA_POSITION, CAMERA_LOOK_AT); + } catch (e) { + logger.error(`Couldn't load area models for quest ${quest.id}, ${area.name}.`, e); + + this.renderer.collision_geometry = DUMMY_OBJECT; + this.renderer.render_geometry = DUMMY_OBJECT; + } + } + + /** + * Ensures that {@link load} is reentrant. + */ + private should_cancel(quest: QuestModel, area: AreaModel): boolean { + return this.quest !== quest || this.area !== area; + } + private add_sections_to_collision_geometry( collision_geom: Object3D, render_geom: Object3D, ): void { - const raycaster = new Raycaster(); - const origin = new Vector3(); - const down = new Vector3(0, -1, 0); - const up = new Vector3(0, 1, 0); - for (const collision_area of collision_geom.children) { - (collision_area as Mesh).geometry.boundingBox.getCenter(origin); + (collision_area as Mesh).geometry.boundingBox.getCenter(this.origin); - raycaster.set(origin, down); - const intersection1 = raycaster + this.raycaster.set(this.origin, this.down); + const intersection1 = this.raycaster .intersectObject(render_geom, true) .find(i => (i.object.userData as AreaUserData).section != undefined); - raycaster.set(origin, up); - const intersection2 = raycaster + this.raycaster.set(this.origin, this.up); + const intersection2 = this.raycaster .intersectObject(render_geom, true) .find(i => (i.object.userData as AreaUserData).section != undefined); @@ -162,20 +199,114 @@ export class QuestModelManager implements Disposable { } } } +} + +class EntityModelManager { + private readonly queue: QuestEntityModel[] = []; + private readonly loaded_entities: { + entity: QuestEntityModel; + disposer: Disposer; + }[] = []; + private loading = false; + + constructor(private readonly renderer: QuestRenderer) {} + + async add(entities: QuestEntityModel[]): Promise { + this.queue.push(...entities); + + if (!this.loading) { + try { + this.loading = true; + + while (this.queue.length) { + const entity = this.queue[0]; + + try { + await this.load(entity); + } catch (e) { + logger.error( + `Couldn't load model for entity ${entity_type_to_string(entity.type)}.`, + e, + ); + } finally { + const index = this.queue.indexOf(entity); + + if (index !== -1) { + this.queue.splice(index, 1); + } + } + } + } finally { + this.loading = false; + } + } + } + + remove(entities: QuestEntityModel[]): void { + for (const entity of entities) { + const queue_index = this.queue.indexOf(entity); + + if (queue_index !== -1) { + this.queue.splice(queue_index, 1); + } + + const loaded_index = this.loaded_entities.findIndex(loaded => loaded.entity === entity); + + if (loaded_index !== -1) { + const loaded = this.loaded_entities.splice(loaded_index, 1)[0]; + + this.renderer.remove_entity_model(loaded.entity); + loaded.disposer.dispose(); + } + } + } + + remove_all(): void { + for (const { disposer } of this.loaded_entities) { + disposer.dispose(); + } + + this.loaded_entities.splice(0, Infinity); + this.queue.splice(0, Infinity); + } + + private async load(entity: QuestEntityModel): Promise { + let model: Mesh; + + if (entity instanceof QuestNpcModel) { + const npc_geom = await load_npc_geometry(entity.type); + const npc_tex = await load_npc_textures(entity.type); + model = create_npc_mesh(entity, npc_geom, npc_tex); + } else if (entity instanceof QuestObjectModel) { + const object_geom = await load_object_geometry(entity.type); + const object_tex = await load_object_textures(entity.type); + model = create_object_mesh(entity, object_geom, object_tex); + } else { + throw new Error(`Unknown entity type ${entity.type}.`); + } + + // The model load might be cancelled by now. + if (this.queue.includes(entity)) { + this.update_entity_geometry(entity, model); + } + } private update_entity_geometry(entity: QuestEntityModel, model: Mesh): void { this.renderer.add_entity_model(model); - this.disposer.add_all( - entity.world_position.observe(({ value: { x, y, z } }) => { - model.position.set(x, y, z); - this.renderer.schedule_render(); - }), + this.loaded_entities.push({ + entity, + disposer: new Disposer( + entity.world_position.observe(({ value: { x, y, z } }) => { + model.position.set(x, y, z); + this.renderer.schedule_render(); + }), - entity.rotation.observe(({ value: { x, y, z } }) => { - model.rotation.set(x, y, z); - this.renderer.schedule_render(); - }), - ); + entity.rotation.observe(({ value: { x, y, z } }) => { + model.rotation.set(x, y, z); + this.renderer.schedule_render(); + }), + ), + }); } } diff --git a/src/quest_editor/rendering/QuestRenderer.ts b/src/quest_editor/rendering/QuestRenderer.ts index 583375d3..c3095684 100644 --- a/src/quest_editor/rendering/QuestRenderer.ts +++ b/src/quest_editor/rendering/QuestRenderer.ts @@ -34,10 +34,6 @@ export class QuestRenderer extends Renderer { 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; @@ -54,7 +50,6 @@ export class QuestRenderer extends Renderer { private readonly disposer = new Disposer(); private readonly perspective_camera: PerspectiveCamera; private readonly entity_to_mesh = new Map(); - private readonly model_manager = this.disposer.add(new QuestModelManager(this)); private readonly entity_controls = this.disposer.add(new QuestEntityControls(this)); constructor() { @@ -63,14 +58,10 @@ export class QuestRenderer extends Renderer { this.perspective_camera = this.camera as PerspectiveCamera; this.disposer.add_all( - quest_editor_store.current_quest.observe(this.load_models), - quest_editor_store.current_area.observe(this.load_models), + new QuestModelManager(this), + quest_editor_store.debug.observe(({ value }) => (this.debug = value)), ); - - this.dom_element.addEventListener("mousedown", this.entity_controls.on_mouse_down); - this.dom_element.addEventListener("mouseup", this.entity_controls.on_mouse_up); - this.dom_element.addEventListener("mousemove", this.entity_controls.on_mouse_move); } dispose(): void { @@ -89,6 +80,7 @@ export class QuestRenderer extends Renderer { this._entity_models = new Group(); this.scene.add(this._entity_models); this.entity_to_mesh.clear(); + this.schedule_render(); } add_entity_model(model: Mesh): void { @@ -103,14 +95,16 @@ export class QuestRenderer extends Renderer { this.schedule_render(); } + remove_entity_model(entity: QuestEntityModel): void { + const mesh = this.entity_to_mesh.get(entity); + + if (mesh) { + this._entity_models.remove(mesh); + this.schedule_render(); + } + } + get_entity_mesh(entity: QuestEntityModel): Mesh | undefined { return this.entity_to_mesh.get(entity); } - - private load_models = () => { - this.model_manager.load_models( - quest_editor_store.current_quest.val, - quest_editor_store.current_area.val, - ); - }; } diff --git a/src/quest_editor/rendering/conversion/areas.ts b/src/quest_editor/rendering/conversion/areas.ts index ff2a0f49..3461306c 100644 --- a/src/quest_editor/rendering/conversion/areas.ts +++ b/src/quest_editor/rendering/conversion/areas.ts @@ -15,6 +15,7 @@ import { RenderObject } from "../../../core/data_formats/parsing/area_geometry"; import { GeometryBuilder } from "../../../core/rendering/conversion/GeometryBuilder"; import { ninja_object_to_geometry_builder } from "../../../core/rendering/conversion/ninja_geometry"; import { SectionModel } from "../../model/SectionModel"; +import { AreaVariantModel } from "../../model/AreaVariantModel"; const materials = [ // Wall @@ -116,6 +117,7 @@ export function area_collision_geometry_to_object_3d(object: CollisionObject): O export function area_geometry_to_sections_and_object_3d( object: RenderObject, + area_variant: AreaVariantModel, ): [SectionModel[], Object3D] { const sections: SectionModel[] = []; const group = new Group(); @@ -144,7 +146,12 @@ export function area_geometry_to_sections_and_object_3d( mesh.updateMatrixWorld(); if (section.id >= 0) { - const sec = new SectionModel(section.id, section.position, section.rotation.y); + const sec = new SectionModel( + section.id, + section.position, + section.rotation.y, + area_variant, + ); sections.push(sec); (mesh.userData as AreaUserData).section = sec; } diff --git a/src/quest_editor/stores/AreaStore.ts b/src/quest_editor/stores/AreaStore.ts index b42b74ef..a2ef2e43 100644 --- a/src/quest_editor/stores/AreaStore.ts +++ b/src/quest_editor/stores/AreaStore.ts @@ -46,12 +46,8 @@ class AreaStore { return area_variant; }; - get_area_sections = ( - episode: Episode, - area_id: number, - variant_id: number, - ): Promise => { - return load_area_sections(episode, area_id, variant_id); + get_area_sections = (episode: Episode, variant: AreaVariantModel): Promise => { + return load_area_sections(episode, variant); }; } diff --git a/src/quest_editor/stores/QuestEditorStore.ts b/src/quest_editor/stores/QuestEditorStore.ts index 6372edff..e05cb80d 100644 --- a/src/quest_editor/stores/QuestEditorStore.ts +++ b/src/quest_editor/stores/QuestEditorStore.ts @@ -29,26 +29,21 @@ import Logger = require("js-logger"); const logger = Logger.get("quest_editor/gui/QuestEditorStore"); export class QuestEditorStore implements Disposable { - readonly debug: WritableProperty = property(false); - readonly undo = new UndoStack(); - readonly current_quest_filename: Property; - readonly current_quest: Property; - readonly current_area: Property; - readonly selected_entity: Property; - private readonly disposer = new Disposer(); private readonly _current_quest_filename = property(undefined); private readonly _current_quest = property(undefined); private readonly _current_area = property(undefined); private readonly _selected_entity = property(undefined); - constructor() { - this.current_quest_filename = this._current_quest_filename; - this.current_quest = this._current_quest; - this.current_area = this._current_area; - this.selected_entity = this._selected_entity; + readonly debug: WritableProperty = property(false); + readonly undo = new UndoStack(); + readonly current_quest_filename: Property = this._current_quest_filename; + readonly current_quest: Property = this._current_quest; + readonly current_area: Property = this._current_area; + readonly selected_entity: Property = this._selected_entity; - this.disposer.add( + constructor() { + this.disposer.add_all( gui_store.tool.observe( ({ value: tool }) => { if (tool === GuiTool.QuestEditor) { @@ -57,6 +52,26 @@ export class QuestEditorStore implements Disposable { }, { call_now: true }, ), + + this.current_quest + .flat_map(quest => (quest ? quest.npcs : property([]))) + .observe(({ value: npcs }) => { + const selected = this.selected_entity.val; + + if (selected instanceof QuestNpcModel && !npcs.includes(selected)) { + this.set_selected_entity(undefined); + } + }), + + this.current_quest + .flat_map(quest => (quest ? quest.objects : property([]))) + .observe(({ value: objects }) => { + const selected = this.selected_entity.val; + + if (selected instanceof QuestObjectModel && !objects.includes(selected)) { + this.set_selected_entity(undefined); + } + }), ); } @@ -121,7 +136,7 @@ export class QuestEditorStore implements Disposable { npc.pso_type_id, npc.npc_id, npc.script_label, - npc.roaming, + npc.pso_roaming, npc.area_id, npc.section_id, npc.position, @@ -145,7 +160,14 @@ export class QuestEditorStore implements Disposable { const quest = this.current_quest.val; if (!quest) return; - let file_name = prompt("File name:"); + let default_file_name = this.current_quest_filename.val; + + if (default_file_name) { + const ext_start = default_file_name.lastIndexOf("."); + if (ext_start !== -1) default_file_name = default_file_name.slice(0, ext_start); + } + + let file_name = prompt("File name:", default_file_name); if (!file_name) return; const buffer = write_quest_qst( @@ -178,7 +200,7 @@ export class QuestEditorStore implements Disposable { pso_type_id: npc.pso_type_id, npc_id: npc.npc_id, script_label: npc.script_label, - roaming: npc.roaming, + pso_roaming: npc.pso_roaming, })), dat_unknowns: quest.dat_unknowns, object_code: quest.object_code, @@ -261,11 +283,7 @@ export class QuestEditorStore implements Disposable { // Load section data. for (const variant of quest.area_variants.val) { - const sections = await area_store.get_area_sections( - quest.episode, - variant.area.id, - variant.id, - ); + const sections = await area_store.get_area_sections(quest.episode, variant); variant.sections.val.splice(0, Infinity, ...sections); for (const object of quest.objects.val.filter(o => o.area_id === variant.area.id)) {