mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 06:28:28 +08:00
Quest entities are now backed by an ArrayBufferBlock so that a "custom entity properties" feature can be added later.
This commit is contained in:
parent
cd67e214f1
commit
8b8e87c8c5
@ -173,3 +173,4 @@ Features that are in ***bold italics*** are planned but not yet implemented.
|
||||
- Desert Fixed Type Box (Breakable Crystals)
|
||||
- Merissa A
|
||||
- Merissa AA
|
||||
- All "Sonic" objects, even the ones that aren't actually Sonic, are rendered as Sonic
|
||||
|
BIN
assets/quests/defaults/default_ep_1.qst
Normal file
BIN
assets/quests/defaults/default_ep_1.qst
Normal file
Binary file not shown.
@ -9,10 +9,16 @@ export class ArrayBufferBlock extends AbstractWritableBlock {
|
||||
protected readonly buffer: ArrayBuffer;
|
||||
protected readonly data_view: DataView;
|
||||
|
||||
constructor(size: number, endianness: Endianness) {
|
||||
get backing_buffer(): ArrayBuffer {
|
||||
return this.buffer;
|
||||
}
|
||||
|
||||
constructor(buffer_or_size: ArrayBuffer | number, endianness: Endianness) {
|
||||
super(endianness);
|
||||
|
||||
this.buffer = new ArrayBuffer(size);
|
||||
this.buffer =
|
||||
typeof buffer_or_size === "number" ? new ArrayBuffer(buffer_or_size) : buffer_or_size;
|
||||
|
||||
this.data_view = new DataView(this.buffer);
|
||||
}
|
||||
}
|
||||
|
@ -40,8 +40,12 @@ export class BufferCursor extends AbstractArrayBufferCursor {
|
||||
}
|
||||
|
||||
take(size: number): BufferCursor {
|
||||
const offset = this.offset + this.position;
|
||||
const wrapper = new BufferCursor(this.buffer, this.endianness, offset, size);
|
||||
const wrapper = new BufferCursor(
|
||||
this.buffer,
|
||||
this.endianness,
|
||||
this.absolute_position - this.buffer.byteOffset,
|
||||
size,
|
||||
);
|
||||
this._position += size;
|
||||
return wrapper;
|
||||
}
|
||||
|
338
src/core/data_formats/parsing/quest/Quest.ts
Normal file
338
src/core/data_formats/parsing/quest/Quest.ts
Normal file
@ -0,0 +1,338 @@
|
||||
import { Vec3 } from "../../vector";
|
||||
import { npc_data, NpcType, NpcTypeData } from "./npc_types";
|
||||
import { id_to_object_type, object_data, ObjectType, ObjectTypeData } from "./object_types";
|
||||
import { DatEvent, DatUnknown, NPC_BYTE_SIZE, OBJECT_BYTE_SIZE } from "./dat";
|
||||
import { Episode } from "./Episode";
|
||||
import { Segment } from "../../asm/instructions";
|
||||
import { get_npc_type } from "./get_npc_type";
|
||||
import { ArrayBufferBlock } from "../../block/ArrayBufferBlock";
|
||||
import { assert } from "../../../util";
|
||||
import { Endianness } from "../../block/Endianness";
|
||||
|
||||
const DEFAULT_SCALE: Vec3 = Object.freeze({ x: 1, y: 1, z: 1 });
|
||||
|
||||
export class Quest {
|
||||
constructor(
|
||||
public id: number,
|
||||
public language: number,
|
||||
public name: string,
|
||||
public short_description: string,
|
||||
public long_description: string,
|
||||
public episode: Episode,
|
||||
readonly objects: readonly QuestObject[],
|
||||
readonly npcs: readonly QuestNpc[],
|
||||
readonly events: QuestEvent[],
|
||||
/**
|
||||
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
|
||||
*/
|
||||
readonly dat_unknowns: DatUnknown[],
|
||||
readonly object_code: readonly Segment[],
|
||||
readonly shop_items: number[],
|
||||
readonly map_designations: Map<number, number>,
|
||||
) {}
|
||||
}
|
||||
|
||||
export type EntityTypeData = NpcTypeData | ObjectTypeData;
|
||||
|
||||
export type EntityType = NpcType | ObjectType;
|
||||
|
||||
export interface QuestEntity<Type extends EntityType = EntityType> {
|
||||
area_id: number;
|
||||
readonly data: ArrayBufferBlock;
|
||||
type: Type;
|
||||
section_id: number;
|
||||
position: Vec3;
|
||||
rotation: Vec3;
|
||||
}
|
||||
|
||||
export class QuestNpc implements QuestEntity<NpcType> {
|
||||
episode: Episode;
|
||||
area_id: number;
|
||||
readonly data: ArrayBufferBlock;
|
||||
|
||||
get type(): NpcType {
|
||||
return get_npc_type(this.episode, this.type_id, this.regular, this.skin, this.area_id);
|
||||
}
|
||||
|
||||
set type(type: NpcType) {
|
||||
const data = npc_data(type);
|
||||
|
||||
if (data.episode != undefined) {
|
||||
this.episode = data.episode;
|
||||
}
|
||||
|
||||
this.type_id = data.type_id ?? 0;
|
||||
this.regular = data.regular ?? true;
|
||||
this.skin = data.skin ?? 0;
|
||||
|
||||
if (data.area_ids.length > 0 && !data.area_ids.includes(this.area_id)) {
|
||||
this.area_id = data.area_ids[0];
|
||||
}
|
||||
}
|
||||
|
||||
get type_id(): number {
|
||||
return this.data.get_u16(0);
|
||||
}
|
||||
|
||||
set type_id(type_id: number) {
|
||||
this.data.set_u16(0, type_id);
|
||||
}
|
||||
|
||||
get section_id(): number {
|
||||
return this.data.get_u16(12);
|
||||
}
|
||||
|
||||
set section_id(section_id: number) {
|
||||
this.data.set_u16(12, section_id);
|
||||
}
|
||||
|
||||
get wave(): number {
|
||||
return this.data.get_u16(14);
|
||||
}
|
||||
|
||||
set wave(wave: number) {
|
||||
this.data.set_u16(14, wave);
|
||||
}
|
||||
|
||||
get wave_2(): number {
|
||||
return this.data.get_u32(16);
|
||||
}
|
||||
|
||||
set wave_2(wave_2: number) {
|
||||
this.data.set_u32(16, wave_2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Section-relative position.
|
||||
*/
|
||||
get position(): Vec3 {
|
||||
return {
|
||||
x: this.data.get_f32(20),
|
||||
y: this.data.get_f32(24),
|
||||
z: this.data.get_f32(28),
|
||||
};
|
||||
}
|
||||
|
||||
set position(position: Vec3) {
|
||||
this.data.set_f32(20, position.x);
|
||||
this.data.set_f32(24, position.y);
|
||||
this.data.set_f32(28, position.z);
|
||||
}
|
||||
|
||||
get rotation(): Vec3 {
|
||||
return {
|
||||
x: (this.data.get_i32(32) / 0xffff) * 2 * Math.PI,
|
||||
y: (this.data.get_i32(36) / 0xffff) * 2 * Math.PI,
|
||||
z: (this.data.get_i32(40) / 0xffff) * 2 * Math.PI,
|
||||
};
|
||||
}
|
||||
|
||||
set rotation(rotation: Vec3) {
|
||||
this.data.set_i32(32, Math.round((rotation.x / (2 * Math.PI)) * 0xffff));
|
||||
this.data.set_i32(36, Math.round((rotation.y / (2 * Math.PI)) * 0xffff));
|
||||
this.data.set_i32(40, Math.round((rotation.z / (2 * Math.PI)) * 0xffff));
|
||||
}
|
||||
|
||||
/**
|
||||
* Seemingly 3 floats, not sure what they represent.
|
||||
* The y component is used to help determine what the NpcType is.
|
||||
*/
|
||||
get scale(): Vec3 {
|
||||
return {
|
||||
x: this.data.get_f32(44),
|
||||
y: this.data.get_f32(48),
|
||||
z: this.data.get_f32(52),
|
||||
};
|
||||
}
|
||||
|
||||
set scale(scale: Vec3) {
|
||||
this.data.set_f32(44, scale.x);
|
||||
this.data.set_f32(48, scale.y);
|
||||
this.data.set_f32(52, scale.z);
|
||||
}
|
||||
|
||||
get regular(): boolean {
|
||||
return Math.abs(this.data.get_f32(48) - 1) > 0.00001;
|
||||
}
|
||||
|
||||
set regular(regular: boolean) {
|
||||
this.data.set_i32(48, (this.data.get_i32(48) & ~0x800000) | (regular ? 0 : 0x800000));
|
||||
}
|
||||
|
||||
get npc_id(): number {
|
||||
return this.data.get_f32(56);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only seems to be valid for non-enemies.
|
||||
*/
|
||||
get script_label(): number {
|
||||
return Math.round(this.data.get_f32(60));
|
||||
}
|
||||
|
||||
get skin(): number {
|
||||
return this.data.get_u32(64);
|
||||
}
|
||||
|
||||
set skin(skin: number) {
|
||||
this.data.set_u32(64, skin);
|
||||
}
|
||||
|
||||
constructor(episode: Episode, area_id: number, data: ArrayBufferBlock) {
|
||||
assert(
|
||||
data.size === NPC_BYTE_SIZE,
|
||||
() => `Data size should be ${NPC_BYTE_SIZE} but was ${data.size}.`,
|
||||
);
|
||||
|
||||
this.episode = episode;
|
||||
this.area_id = area_id;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static create(type: NpcType, area_id: number, wave: number): QuestNpc {
|
||||
const npc = new QuestNpc(
|
||||
Episode.I,
|
||||
area_id,
|
||||
new ArrayBufferBlock(NPC_BYTE_SIZE, Endianness.Little),
|
||||
);
|
||||
|
||||
// Set scale before type because type will change it.
|
||||
npc.scale = DEFAULT_SCALE;
|
||||
npc.type = type;
|
||||
// Set area_id after type, because you might want to overwrite the area_id that type has
|
||||
// determined.
|
||||
npc.area_id = area_id;
|
||||
npc.wave = wave;
|
||||
npc.wave_2 = wave;
|
||||
|
||||
return npc;
|
||||
}
|
||||
}
|
||||
|
||||
export class QuestObject implements QuestEntity<ObjectType> {
|
||||
area_id: number;
|
||||
readonly data: ArrayBufferBlock;
|
||||
|
||||
get type(): ObjectType {
|
||||
return id_to_object_type(this.type_id);
|
||||
}
|
||||
|
||||
set type(type: ObjectType) {
|
||||
this.type_id = object_data(type).type_id ?? 0;
|
||||
}
|
||||
|
||||
get type_id(): number {
|
||||
return this.data.get_u16(0);
|
||||
}
|
||||
|
||||
set type_id(type_id: number) {
|
||||
this.data.set_u16(0, type_id);
|
||||
}
|
||||
|
||||
get id(): number {
|
||||
return this.data.get_u16(8);
|
||||
}
|
||||
|
||||
get group_id(): number {
|
||||
return this.data.get_u16(10);
|
||||
}
|
||||
|
||||
get section_id(): number {
|
||||
return this.data.get_u16(12);
|
||||
}
|
||||
|
||||
set section_id(section_id: number) {
|
||||
this.data.set_u16(12, section_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Section-relative position.
|
||||
*/
|
||||
get position(): Vec3 {
|
||||
return {
|
||||
x: this.data.get_f32(16),
|
||||
y: this.data.get_f32(20),
|
||||
z: this.data.get_f32(24),
|
||||
};
|
||||
}
|
||||
|
||||
set position(position: Vec3) {
|
||||
this.data.set_f32(16, position.x);
|
||||
this.data.set_f32(20, position.y);
|
||||
this.data.set_f32(24, position.z);
|
||||
}
|
||||
|
||||
get rotation(): Vec3 {
|
||||
return {
|
||||
x: (this.data.get_i32(28) / 0xffff) * 2 * Math.PI,
|
||||
y: (this.data.get_i32(32) / 0xffff) * 2 * Math.PI,
|
||||
z: (this.data.get_i32(36) / 0xffff) * 2 * Math.PI,
|
||||
};
|
||||
}
|
||||
|
||||
set rotation(rotation: Vec3) {
|
||||
this.data.set_i32(28, Math.round((rotation.x / (2 * Math.PI)) * 0xffff));
|
||||
this.data.set_i32(32, Math.round((rotation.y / (2 * Math.PI)) * 0xffff));
|
||||
this.data.set_i32(36, Math.round((rotation.z / (2 * Math.PI)) * 0xffff));
|
||||
}
|
||||
|
||||
get script_label(): number | undefined {
|
||||
switch (this.type) {
|
||||
case ObjectType.ScriptCollision:
|
||||
case ObjectType.ForestConsole:
|
||||
case ObjectType.TalkLinkToSupport:
|
||||
return this.data.get_u32(52);
|
||||
case ObjectType.RicoMessagePod:
|
||||
return this.data.get_u32(56);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
get script_label_2(): number | undefined {
|
||||
switch (this.type) {
|
||||
case ObjectType.RicoMessagePod:
|
||||
return this.data.get_u32(60);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(area_id: number, data: ArrayBufferBlock) {
|
||||
assert(
|
||||
data.size === OBJECT_BYTE_SIZE,
|
||||
() => `Data size should be ${OBJECT_BYTE_SIZE} but was ${data.size}.`,
|
||||
);
|
||||
|
||||
this.area_id = area_id;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static create(type: ObjectType, area_id: number): QuestObject {
|
||||
const obj = new QuestObject(
|
||||
area_id,
|
||||
new ArrayBufferBlock(OBJECT_BYTE_SIZE, Endianness.Little),
|
||||
);
|
||||
|
||||
obj.type = type;
|
||||
// Set area_id after type, because you might want to overwrite the area_id that type has
|
||||
// determined.
|
||||
obj.area_id = area_id;
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
export type QuestEvent = DatEvent;
|
||||
|
||||
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);
|
||||
}
|
@ -12,14 +12,14 @@ const PC_OBJECT_CODE_OFFSET = 920;
|
||||
const BB_OBJECT_CODE_OFFSET = 4652;
|
||||
|
||||
export type BinFile = {
|
||||
readonly quest_id: number;
|
||||
readonly language: number;
|
||||
readonly quest_name: string;
|
||||
readonly short_description: string;
|
||||
readonly long_description: string;
|
||||
readonly object_code: ArrayBuffer;
|
||||
readonly label_offsets: readonly number[];
|
||||
readonly shop_items: readonly number[];
|
||||
quest_id: number;
|
||||
language: number;
|
||||
quest_name: string;
|
||||
short_description: string;
|
||||
long_description: string;
|
||||
object_code: ArrayBuffer;
|
||||
readonly label_offsets: number[];
|
||||
readonly shop_items: number[];
|
||||
};
|
||||
|
||||
export function parse_bin(cursor: Cursor): { bin: BinFile; format: BinFormat } {
|
||||
|
@ -2,7 +2,7 @@ import { Endianness } from "../../block/Endianness";
|
||||
import { prs_decompress } from "../../compression/prs/decompress";
|
||||
import { BufferCursor } from "../../block/cursor/BufferCursor";
|
||||
import { ResizableBlockCursor } from "../../block/cursor/ResizableBlockCursor";
|
||||
import { DatFile, parse_dat, write_dat } from "./dat";
|
||||
import { parse_dat, write_dat } from "./dat";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
/**
|
||||
@ -37,30 +37,15 @@ test("parse, modify and write DAT", () => {
|
||||
const test_parsed = parse_dat(orig_dat);
|
||||
orig_dat.seek_start(0);
|
||||
|
||||
const test_updated: DatFile = {
|
||||
...test_parsed,
|
||||
objs: test_parsed.objs.map((obj, i) => {
|
||||
if (i === 9) {
|
||||
return {
|
||||
...obj,
|
||||
position: {
|
||||
x: 13,
|
||||
y: 17,
|
||||
z: 19,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}),
|
||||
};
|
||||
const test_obj_array = new Float32Array(test_parsed.objs[9].data);
|
||||
test_obj_array[4] = 13;
|
||||
test_obj_array[5] = 17;
|
||||
test_obj_array[6] = 19;
|
||||
|
||||
const test_dat = new ResizableBlockCursor(write_dat(test_updated));
|
||||
const test_dat = new ResizableBlockCursor(write_dat(test_parsed));
|
||||
|
||||
expect(test_dat.size).toBe(orig_dat.size);
|
||||
|
||||
let match = true;
|
||||
|
||||
while (orig_dat.bytes_left) {
|
||||
if (orig_dat.position === 16 + 9 * 68 + 16) {
|
||||
orig_dat.seek(12);
|
||||
@ -68,11 +53,17 @@ test("parse, modify and write DAT", () => {
|
||||
expect(test_dat.f32()).toBe(13);
|
||||
expect(test_dat.f32()).toBe(17);
|
||||
expect(test_dat.f32()).toBe(19);
|
||||
} else if (test_dat.u8() !== orig_dat.u8()) {
|
||||
match = false;
|
||||
break;
|
||||
} else {
|
||||
const test_byte = test_dat.u8();
|
||||
const orig_byte = orig_dat.u8();
|
||||
|
||||
if (test_byte !== orig_byte) {
|
||||
throw new Error(
|
||||
`Byte ${
|
||||
test_dat.position - 1
|
||||
} didn't match, expected ${orig_byte}, got ${test_byte}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(match).toBe(true);
|
||||
});
|
||||
|
@ -6,52 +6,33 @@ import { ResizableBlock } from "../../block/ResizableBlock";
|
||||
import { WritableCursor } from "../../block/cursor/WritableCursor";
|
||||
import { assert } from "../../../util";
|
||||
import { LogManager } from "../../../Logger";
|
||||
import { Vec3 } from "../../vector";
|
||||
import { ArrayBufferCursor } from "../../block/cursor/ArrayBufferCursor";
|
||||
|
||||
const logger = LogManager.get("core/data_formats/parsing/quest/dat");
|
||||
|
||||
const OBJECT_SIZE = 68;
|
||||
const NPC_SIZE = 72;
|
||||
export const OBJECT_BYTE_SIZE = 68;
|
||||
export const NPC_BYTE_SIZE = 72;
|
||||
|
||||
export type DatFile = {
|
||||
readonly objs: readonly DatObject[];
|
||||
readonly npcs: readonly DatNpc[];
|
||||
readonly events: readonly DatEvent[];
|
||||
readonly unknowns: readonly DatUnknown[];
|
||||
readonly objs: DatEntity[];
|
||||
readonly npcs: DatEntity[];
|
||||
readonly events: DatEvent[];
|
||||
readonly unknowns: DatUnknown[];
|
||||
};
|
||||
|
||||
export type DatEntity = {
|
||||
readonly type_id: number;
|
||||
readonly section_id: number;
|
||||
readonly position: Vec3;
|
||||
readonly rotation: Vec3;
|
||||
readonly area_id: number;
|
||||
readonly unknown: readonly number[][];
|
||||
};
|
||||
|
||||
export type DatObject = DatEntity & {
|
||||
readonly id: number;
|
||||
readonly group_id: number;
|
||||
readonly properties: readonly number[];
|
||||
};
|
||||
|
||||
export type DatNpc = DatEntity & {
|
||||
readonly wave: number;
|
||||
readonly wave2: number;
|
||||
readonly scale: Vec3;
|
||||
readonly npc_id: number;
|
||||
readonly script_label: number;
|
||||
readonly roaming: number;
|
||||
area_id: number;
|
||||
readonly data: ArrayBuffer;
|
||||
};
|
||||
|
||||
export type DatEvent = {
|
||||
readonly id: number;
|
||||
readonly section_id: number;
|
||||
readonly wave: number;
|
||||
readonly delay: number;
|
||||
readonly actions: readonly DatEventAction[];
|
||||
readonly area_id: number;
|
||||
readonly unknown: number;
|
||||
id: number;
|
||||
section_id: number;
|
||||
wave: number;
|
||||
delay: number;
|
||||
readonly actions: DatEventAction[];
|
||||
area_id: number;
|
||||
unknown: number;
|
||||
};
|
||||
|
||||
export enum DatEventActionType {
|
||||
@ -97,8 +78,8 @@ export type DatUnknown = {
|
||||
};
|
||||
|
||||
export function parse_dat(cursor: Cursor): DatFile {
|
||||
const objs: DatObject[] = [];
|
||||
const npcs: DatNpc[] = [];
|
||||
const objs: DatEntity[] = [];
|
||||
const npcs: DatEntity[] = [];
|
||||
const events: DatEvent[] = [];
|
||||
const unknowns: DatUnknown[] = [];
|
||||
|
||||
@ -122,9 +103,9 @@ export function parse_dat(cursor: Cursor): DatFile {
|
||||
const entities_cursor = cursor.take(entities_size);
|
||||
|
||||
if (entity_type === 1) {
|
||||
parse_objects(entities_cursor, area_id, objs);
|
||||
parse_entities(entities_cursor, area_id, objs, OBJECT_BYTE_SIZE);
|
||||
} else if (entity_type === 2) {
|
||||
parse_npcs(entities_cursor, area_id, npcs);
|
||||
parse_entities(entities_cursor, area_id, npcs, NPC_BYTE_SIZE);
|
||||
} else if (entity_type === 3) {
|
||||
parse_events(entities_cursor, area_id, events);
|
||||
} else {
|
||||
@ -151,16 +132,16 @@ export function parse_dat(cursor: Cursor): DatFile {
|
||||
|
||||
export function write_dat({ objs, npcs, events, unknowns }: DatFile): ResizableBlock {
|
||||
const block = new ResizableBlock(
|
||||
objs.length * (16 + OBJECT_SIZE) +
|
||||
npcs.length * (16 + NPC_SIZE) +
|
||||
objs.length * (16 + OBJECT_BYTE_SIZE) +
|
||||
npcs.length * (16 + NPC_BYTE_SIZE) +
|
||||
unknowns.reduce((a, b) => a + b.total_size, 0),
|
||||
Endianness.Little,
|
||||
);
|
||||
const cursor = new ResizableBlockCursor(block);
|
||||
|
||||
write_objects(cursor, objs);
|
||||
write_entities(cursor, objs, 1, OBJECT_BYTE_SIZE);
|
||||
|
||||
write_npcs(cursor, npcs);
|
||||
write_entities(cursor, npcs, 2, NPC_BYTE_SIZE);
|
||||
|
||||
write_events(cursor, events);
|
||||
|
||||
@ -181,78 +162,18 @@ export function write_dat({ objs, npcs, events, unknowns }: DatFile): ResizableB
|
||||
return block;
|
||||
}
|
||||
|
||||
function parse_objects(cursor: Cursor, area_id: number, objs: DatObject[]): void {
|
||||
const object_count = Math.floor(cursor.size / OBJECT_SIZE);
|
||||
function parse_entities(
|
||||
cursor: Cursor,
|
||||
area_id: number,
|
||||
entities: DatEntity[],
|
||||
entity_size: number,
|
||||
): void {
|
||||
const entity_count = Math.floor(cursor.size / entity_size);
|
||||
|
||||
for (let i = 0; i < object_count; ++i) {
|
||||
const type_id = cursor.u16();
|
||||
const unknown1 = cursor.u8_array(6);
|
||||
const id = cursor.u16();
|
||||
const group_id = cursor.u16();
|
||||
const section_id = cursor.u16();
|
||||
const unknown2 = cursor.u8_array(2);
|
||||
const position = cursor.vec3_f32();
|
||||
const rotation = {
|
||||
x: (cursor.i32() / 0xffff) * 2 * Math.PI,
|
||||
y: (cursor.i32() / 0xffff) * 2 * Math.PI,
|
||||
z: (cursor.i32() / 0xffff) * 2 * Math.PI,
|
||||
};
|
||||
const properties = [
|
||||
cursor.f32(),
|
||||
cursor.f32(),
|
||||
cursor.f32(),
|
||||
cursor.u32(),
|
||||
cursor.u32(),
|
||||
cursor.u32(),
|
||||
cursor.u32(),
|
||||
];
|
||||
|
||||
objs.push({
|
||||
type_id,
|
||||
id,
|
||||
group_id,
|
||||
section_id,
|
||||
position,
|
||||
rotation,
|
||||
properties,
|
||||
for (let i = 0; i < entity_count; ++i) {
|
||||
entities.push({
|
||||
area_id,
|
||||
unknown: [unknown1, unknown2],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function parse_npcs(cursor: Cursor, area_id: number, npcs: DatNpc[]): void {
|
||||
const npc_count = Math.floor(cursor.size / NPC_SIZE);
|
||||
|
||||
for (let i = 0; i < npc_count; ++i) {
|
||||
const type_id = cursor.u16();
|
||||
const unknown1 = cursor.u8_array(10);
|
||||
const section_id = cursor.u16();
|
||||
const wave = cursor.u16();
|
||||
const wave2 = cursor.u32();
|
||||
const position = cursor.vec3_f32();
|
||||
const rotation_x = (cursor.i32() / 0xffff) * 2 * Math.PI;
|
||||
const rotation_y = (cursor.i32() / 0xffff) * 2 * Math.PI;
|
||||
const rotation_z = (cursor.i32() / 0xffff) * 2 * Math.PI;
|
||||
const scale = cursor.vec3_f32();
|
||||
const npc_id = cursor.f32();
|
||||
const script_label = cursor.f32();
|
||||
const roaming = cursor.u32();
|
||||
const unknown2 = cursor.u8_array(4);
|
||||
|
||||
npcs.push({
|
||||
type_id,
|
||||
section_id,
|
||||
wave,
|
||||
wave2,
|
||||
position,
|
||||
rotation: { x: rotation_x, y: rotation_y, z: rotation_z },
|
||||
scale,
|
||||
npc_id,
|
||||
script_label,
|
||||
roaming,
|
||||
area_id,
|
||||
unknown: [unknown1, unknown2],
|
||||
data: cursor.array_buffer(entity_size),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -375,112 +296,43 @@ function parse_event_actions(cursor: Cursor): DatEventAction[] {
|
||||
return actions;
|
||||
}
|
||||
|
||||
function write_objects(cursor: WritableCursor, objs: readonly DatObject[]): void {
|
||||
const grouped_objs = groupBy(objs, obj => obj.area_id);
|
||||
const obj_area_ids = Object.keys(grouped_objs)
|
||||
function write_entities(
|
||||
cursor: WritableCursor,
|
||||
entities: readonly DatEntity[],
|
||||
entity_type: number,
|
||||
entity_size: number,
|
||||
): void {
|
||||
const grouped_entities = groupBy(entities, entity => entity.area_id);
|
||||
const entity_area_ids = Object.keys(grouped_entities)
|
||||
.map(key => parseInt(key, 10))
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
for (const area_id of obj_area_ids) {
|
||||
const area_objs = grouped_objs[area_id];
|
||||
const entities_size = area_objs.length * OBJECT_SIZE;
|
||||
cursor.write_u32(1); // Entity type
|
||||
for (const area_id of entity_area_ids) {
|
||||
const area_entities = grouped_entities[area_id];
|
||||
const entities_size = area_entities.length * entity_size;
|
||||
cursor.write_u32(entity_type);
|
||||
cursor.write_u32(entities_size + 16);
|
||||
cursor.write_u32(area_id);
|
||||
cursor.write_u32(entities_size);
|
||||
const start_pos = cursor.position;
|
||||
|
||||
for (const obj of area_objs) {
|
||||
for (const entity of area_entities) {
|
||||
assert(
|
||||
obj.unknown.length === 2,
|
||||
() => `unknown should be of length 2, was ${obj.unknown.length}`,
|
||||
entity.data.byteLength === entity_size,
|
||||
() =>
|
||||
`Malformed entity in area ${area_id}, data array was of length ${entity.data.byteLength} instead of expected ${entity_size}.`,
|
||||
);
|
||||
|
||||
cursor.write_u16(obj.type_id);
|
||||
|
||||
assert(
|
||||
obj.unknown[0].length === 6,
|
||||
() => `unknown[0] should be of length 6, was ${obj.unknown[0].length}`,
|
||||
);
|
||||
|
||||
cursor.write_u8_array(obj.unknown[0]);
|
||||
cursor.write_u16(obj.id);
|
||||
cursor.write_u16(obj.group_id);
|
||||
cursor.write_u16(obj.section_id);
|
||||
|
||||
assert(
|
||||
obj.unknown[1].length === 2,
|
||||
() => `unknown[1] should be of length 2, was ${obj.unknown[1].length}`,
|
||||
);
|
||||
|
||||
cursor.write_u8_array(obj.unknown[1]);
|
||||
cursor.write_vec3_f32(obj.position);
|
||||
cursor.write_i32(Math.round((obj.rotation.x / (2 * Math.PI)) * 0xffff));
|
||||
cursor.write_i32(Math.round((obj.rotation.y / (2 * Math.PI)) * 0xffff));
|
||||
cursor.write_i32(Math.round((obj.rotation.z / (2 * Math.PI)) * 0xffff));
|
||||
|
||||
assert(
|
||||
obj.properties.length === 7,
|
||||
() => `properties should be of length 7, was ${obj.properties.length}`,
|
||||
);
|
||||
|
||||
cursor.write_f32(obj.properties[0]);
|
||||
cursor.write_f32(obj.properties[1]);
|
||||
cursor.write_f32(obj.properties[2]);
|
||||
cursor.write_u32(obj.properties[3]);
|
||||
cursor.write_u32(obj.properties[4]);
|
||||
cursor.write_u32(obj.properties[5]);
|
||||
cursor.write_u32(obj.properties[6]);
|
||||
cursor.write_cursor(new ArrayBufferCursor(entity.data, cursor.endianness));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function write_npcs(cursor: WritableCursor, npcs: readonly DatNpc[]): void {
|
||||
const grouped_npcs = groupBy(npcs, npc => npc.area_id);
|
||||
const npc_area_ids = Object.keys(grouped_npcs)
|
||||
.map(key => parseInt(key, 10))
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
for (const area_id of npc_area_ids) {
|
||||
const area_npcs = grouped_npcs[area_id];
|
||||
const entities_size = area_npcs.length * NPC_SIZE;
|
||||
cursor.write_u32(2); // Entity type
|
||||
cursor.write_u32(entities_size + 16);
|
||||
cursor.write_u32(area_id);
|
||||
cursor.write_u32(entities_size);
|
||||
|
||||
for (const npc of area_npcs) {
|
||||
assert(
|
||||
npc.unknown.length === 2,
|
||||
() => `unknown should be of length 2, was ${npc.unknown.length}`,
|
||||
);
|
||||
|
||||
cursor.write_u16(npc.type_id);
|
||||
|
||||
assert(
|
||||
npc.unknown[0].length === 10,
|
||||
() => `unknown[0] should be of length 10, was ${npc.unknown[0].length}`,
|
||||
);
|
||||
|
||||
cursor.write_u8_array(npc.unknown[0]);
|
||||
cursor.write_u16(npc.section_id);
|
||||
cursor.write_u16(npc.wave);
|
||||
cursor.write_u32(npc.wave2);
|
||||
cursor.write_vec3_f32(npc.position);
|
||||
cursor.write_i32(Math.round((npc.rotation.x / (2 * Math.PI)) * 0xffff));
|
||||
cursor.write_i32(Math.round((npc.rotation.y / (2 * Math.PI)) * 0xffff));
|
||||
cursor.write_i32(Math.round((npc.rotation.z / (2 * Math.PI)) * 0xffff));
|
||||
cursor.write_vec3_f32(npc.scale);
|
||||
cursor.write_f32(npc.npc_id);
|
||||
cursor.write_f32(npc.script_label);
|
||||
cursor.write_u32(npc.roaming);
|
||||
|
||||
assert(
|
||||
npc.unknown[1].length === 4,
|
||||
() => `unknown[1] should be of length 4, was ${npc.unknown[1].length}`,
|
||||
);
|
||||
|
||||
cursor.write_u8_array(npc.unknown[1]);
|
||||
}
|
||||
assert(
|
||||
cursor.position === start_pos + entities_size,
|
||||
() =>
|
||||
`Wrote ${
|
||||
cursor.position - start_pos
|
||||
} bytes of entity data instead of expected ${entities_size} bytes for area ${area_id}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,72 +0,0 @@
|
||||
import { Vec3 } from "../../vector";
|
||||
import { npc_data, NpcType, NpcTypeData } from "./npc_types";
|
||||
import { object_data, ObjectType, ObjectTypeData } from "./object_types";
|
||||
import { DatEvent } from "./dat";
|
||||
|
||||
export type QuestNpc = {
|
||||
readonly type: NpcType;
|
||||
readonly area_id: number;
|
||||
readonly section_id: number;
|
||||
readonly wave: number;
|
||||
readonly pso_wave2: number;
|
||||
/**
|
||||
* Section-relative position
|
||||
*/
|
||||
readonly position: Vec3;
|
||||
readonly rotation: Vec3;
|
||||
/**
|
||||
* Seemingly 3 floats, not sure what they represent.
|
||||
* The y component is used to help determine what the NpcType is.
|
||||
*/
|
||||
readonly scale: Vec3;
|
||||
/**
|
||||
* Data of which the purpose hasn't been discovered yet.
|
||||
*/
|
||||
readonly unknown: readonly number[][];
|
||||
readonly pso_type_id: number;
|
||||
readonly npc_id: number;
|
||||
/**
|
||||
* Only seems to be valid for non-enemies.
|
||||
*/
|
||||
readonly script_label: number;
|
||||
readonly pso_roaming: number;
|
||||
};
|
||||
|
||||
export type QuestObject = {
|
||||
readonly type: ObjectType;
|
||||
readonly id: number;
|
||||
readonly group_id: number;
|
||||
readonly area_id: number;
|
||||
readonly section_id: number;
|
||||
/**
|
||||
* Section-relative position
|
||||
*/
|
||||
readonly position: Vec3;
|
||||
readonly rotation: Vec3;
|
||||
/**
|
||||
* Properties that differ per object type.
|
||||
*/
|
||||
readonly properties: Map<string, number>;
|
||||
/**
|
||||
* Data of which the purpose hasn't been discovered yet.
|
||||
*/
|
||||
readonly unknown: readonly number[][];
|
||||
};
|
||||
|
||||
export type QuestEvent = DatEvent;
|
||||
|
||||
export type EntityTypeData = NpcTypeData | ObjectTypeData;
|
||||
|
||||
export type EntityType = NpcType | ObjectType;
|
||||
|
||||
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);
|
||||
}
|
281
src/core/data_formats/parsing/quest/get_npc_type.ts
Normal file
281
src/core/data_formats/parsing/quest/get_npc_type.ts
Normal file
@ -0,0 +1,281 @@
|
||||
import { NpcType } from "./npc_types";
|
||||
|
||||
// TODO: detect Mothmant, St. Rappy, Hallo Rappy, Egg Rappy, Death Gunner, Bulk and Recon.
|
||||
export function get_npc_type(
|
||||
episode: number,
|
||||
type_id: number,
|
||||
regular: boolean,
|
||||
skin: number,
|
||||
area_id: number,
|
||||
): NpcType {
|
||||
switch (`${type_id}, ${skin % 3}, ${episode}`) {
|
||||
case `${0x044}, 0, 1`:
|
||||
return NpcType.Booma;
|
||||
case `${0x044}, 1, 1`:
|
||||
return NpcType.Gobooma;
|
||||
case `${0x044}, 2, 1`:
|
||||
return NpcType.Gigobooma;
|
||||
|
||||
case `${0x063}, 0, 1`:
|
||||
return NpcType.EvilShark;
|
||||
case `${0x063}, 1, 1`:
|
||||
return NpcType.PalShark;
|
||||
case `${0x063}, 2, 1`:
|
||||
return NpcType.GuilShark;
|
||||
|
||||
case `${0x0a6}, 0, 1`:
|
||||
return NpcType.Dimenian;
|
||||
case `${0x0a6}, 0, 2`:
|
||||
return NpcType.Dimenian2;
|
||||
case `${0x0a6}, 1, 1`:
|
||||
return NpcType.LaDimenian;
|
||||
case `${0x0a6}, 1, 2`:
|
||||
return NpcType.LaDimenian2;
|
||||
case `${0x0a6}, 2, 1`:
|
||||
return NpcType.SoDimenian;
|
||||
case `${0x0a6}, 2, 2`:
|
||||
return NpcType.SoDimenian2;
|
||||
|
||||
case `${0x0d6}, 0, 2`:
|
||||
return NpcType.Mericarol;
|
||||
case `${0x0d6}, 1, 2`:
|
||||
return NpcType.Mericus;
|
||||
case `${0x0d6}, 2, 2`:
|
||||
return NpcType.Merikle;
|
||||
|
||||
case `${0x115}, 0, 4`:
|
||||
return NpcType.Boota;
|
||||
case `${0x115}, 1, 4`:
|
||||
return NpcType.ZeBoota;
|
||||
case `${0x115}, 2, 4`:
|
||||
return NpcType.BaBoota;
|
||||
case `${0x117}, 0, 4`:
|
||||
return NpcType.Goran;
|
||||
case `${0x117}, 1, 4`:
|
||||
return NpcType.PyroGoran;
|
||||
case `${0x117}, 2, 4`:
|
||||
return NpcType.GoranDetonator;
|
||||
}
|
||||
|
||||
switch (`${type_id}, ${skin % 2}, ${episode}`) {
|
||||
case `${0x040}, 0, 1`:
|
||||
return NpcType.Hildebear;
|
||||
case `${0x040}, 0, 2`:
|
||||
return NpcType.Hildebear2;
|
||||
case `${0x040}, 1, 1`:
|
||||
return NpcType.Hildeblue;
|
||||
case `${0x040}, 1, 2`:
|
||||
return NpcType.Hildeblue2;
|
||||
case `${0x041}, 0, 1`:
|
||||
return NpcType.RagRappy;
|
||||
case `${0x041}, 0, 2`:
|
||||
return NpcType.RagRappy2;
|
||||
case `${0x041}, 0, 4`:
|
||||
return NpcType.SandRappy;
|
||||
case `${0x041}, 1, 1`:
|
||||
return NpcType.AlRappy;
|
||||
case `${0x041}, 1, 2`:
|
||||
return NpcType.LoveRappy;
|
||||
case `${0x041}, 1, 4`:
|
||||
return NpcType.DelRappy;
|
||||
|
||||
case `${0x080}, 0, 1`:
|
||||
return NpcType.Dubchic;
|
||||
case `${0x080}, 0, 2`:
|
||||
return NpcType.Dubchic2;
|
||||
case `${0x080}, 1, 1`:
|
||||
return NpcType.Gilchic;
|
||||
case `${0x080}, 1, 2`:
|
||||
return NpcType.Gilchic2;
|
||||
|
||||
case `${0x0d4}, 0, 2`:
|
||||
return NpcType.SinowBerill;
|
||||
case `${0x0d4}, 1, 2`:
|
||||
return NpcType.SinowSpigell;
|
||||
case `${0x0d5}, 0, 2`:
|
||||
return NpcType.Merillia;
|
||||
case `${0x0d5}, 1, 2`:
|
||||
return NpcType.Meriltas;
|
||||
case `${0x0d7}, 0, 2`:
|
||||
return NpcType.UlGibbon;
|
||||
case `${0x0d7}, 1, 2`:
|
||||
return NpcType.ZolGibbon;
|
||||
|
||||
case `${0x0dd}, 0, 2`:
|
||||
return NpcType.Dolmolm;
|
||||
case `${0x0dd}, 1, 2`:
|
||||
return NpcType.Dolmdarl;
|
||||
case `${0x0e0}, 0, 2`:
|
||||
return area_id > 15 ? NpcType.Epsilon : NpcType.SinowZoa;
|
||||
case `${0x0e0}, 1, 2`:
|
||||
return area_id > 15 ? NpcType.Epsilon : NpcType.SinowZele;
|
||||
|
||||
case `${0x112}, 0, 4`:
|
||||
return NpcType.MerissaA;
|
||||
case `${0x112}, 1, 4`:
|
||||
return NpcType.MerissaAA;
|
||||
case `${0x114}, 0, 4`:
|
||||
return NpcType.Zu;
|
||||
case `${0x114}, 1, 4`:
|
||||
return NpcType.Pazuzu;
|
||||
case `${0x116}, 0, 4`:
|
||||
return NpcType.Dorphon;
|
||||
case `${0x116}, 1, 4`:
|
||||
return NpcType.DorphonEclair;
|
||||
case `${0x119}, 0, 4`:
|
||||
return regular ? NpcType.SaintMilion : NpcType.Kondrieu;
|
||||
case `${0x119}, 1, 4`:
|
||||
return regular ? NpcType.Shambertin : NpcType.Kondrieu;
|
||||
}
|
||||
|
||||
switch (`${type_id}, ${episode}`) {
|
||||
case `${0x042}, 1`:
|
||||
return NpcType.Monest;
|
||||
case `${0x042}, 2`:
|
||||
return NpcType.Monest2;
|
||||
case `${0x043}, 1`:
|
||||
return regular ? NpcType.SavageWolf : NpcType.BarbarousWolf;
|
||||
case `${0x043}, 2`:
|
||||
return regular ? NpcType.SavageWolf2 : NpcType.BarbarousWolf2;
|
||||
|
||||
case `${0x060}, 1`:
|
||||
return NpcType.GrassAssassin;
|
||||
case `${0x060}, 2`:
|
||||
return NpcType.GrassAssassin2;
|
||||
case `${0x061}, 1`:
|
||||
return area_id > 15 ? NpcType.DelLily : regular ? NpcType.PoisonLily : NpcType.NarLily;
|
||||
case `${0x061}, 2`:
|
||||
return area_id > 15
|
||||
? NpcType.DelLily
|
||||
: regular
|
||||
? NpcType.PoisonLily2
|
||||
: NpcType.NarLily2;
|
||||
case `${0x062}, 1`:
|
||||
return NpcType.NanoDragon;
|
||||
case `${0x064}, 1`:
|
||||
return regular ? NpcType.PofuillySlime : NpcType.PouillySlime;
|
||||
case `${0x065}, 1`:
|
||||
return NpcType.PanArms;
|
||||
case `${0x065}, 2`:
|
||||
return NpcType.PanArms2;
|
||||
|
||||
case `${0x081}, 1`:
|
||||
return NpcType.Garanz;
|
||||
case `${0x081}, 2`:
|
||||
return NpcType.Garanz2;
|
||||
case `${0x082}, 1`:
|
||||
return regular ? NpcType.SinowBeat : NpcType.SinowGold;
|
||||
case `${0x083}, 1`:
|
||||
return NpcType.Canadine;
|
||||
case `${0x084}, 1`:
|
||||
return NpcType.Canane;
|
||||
case `${0x085}, 1`:
|
||||
return NpcType.Dubswitch;
|
||||
case `${0x085}, 2`:
|
||||
return NpcType.Dubswitch2;
|
||||
|
||||
case `${0x0a0}, 1`:
|
||||
return NpcType.Delsaber;
|
||||
case `${0x0a0}, 2`:
|
||||
return NpcType.Delsaber2;
|
||||
case `${0x0a1}, 1`:
|
||||
return NpcType.ChaosSorcerer;
|
||||
case `${0x0a1}, 2`:
|
||||
return NpcType.ChaosSorcerer2;
|
||||
case `${0x0a2}, 1`:
|
||||
return NpcType.DarkGunner;
|
||||
case `${0x0a4}, 1`:
|
||||
return NpcType.ChaosBringer;
|
||||
case `${0x0a5}, 1`:
|
||||
return NpcType.DarkBelra;
|
||||
case `${0x0a5}, 2`:
|
||||
return NpcType.DarkBelra2;
|
||||
case `${0x0a7}, 1`:
|
||||
return NpcType.Bulclaw;
|
||||
case `${0x0a8}, 1`:
|
||||
return NpcType.Claw;
|
||||
|
||||
case `${0x0c0}, 1`:
|
||||
return NpcType.Dragon;
|
||||
case `${0x0c0}, 2`:
|
||||
return NpcType.GalGryphon;
|
||||
case `${0x0c1}, 1`:
|
||||
return NpcType.DeRolLe;
|
||||
case `${0x0c2}, 1`:
|
||||
return NpcType.VolOptPart1;
|
||||
case `${0x0c5}, 1`:
|
||||
return NpcType.VolOptPart2;
|
||||
case `${0x0c8}, 1`:
|
||||
return NpcType.DarkFalz;
|
||||
case `${0x0ca}, 2`:
|
||||
return NpcType.OlgaFlow;
|
||||
case `${0x0cb}, 2`:
|
||||
return NpcType.BarbaRay;
|
||||
case `${0x0cc}, 2`:
|
||||
return NpcType.GolDragon;
|
||||
|
||||
case `${0x0d8}, 2`:
|
||||
return NpcType.Gibbles;
|
||||
case `${0x0d9}, 2`:
|
||||
return NpcType.Gee;
|
||||
case `${0x0da}, 2`:
|
||||
return NpcType.GiGue;
|
||||
|
||||
case `${0x0db}, 2`:
|
||||
return NpcType.Deldepth;
|
||||
case `${0x0dc}, 2`:
|
||||
return NpcType.Delbiter;
|
||||
case `${0x0de}, 2`:
|
||||
return NpcType.Morfos;
|
||||
case `${0x0df}, 2`:
|
||||
return NpcType.Recobox;
|
||||
case `${0x0e1}, 2`:
|
||||
return NpcType.IllGill;
|
||||
|
||||
case `${0x110}, 4`:
|
||||
return NpcType.Astark;
|
||||
case `${0x111}, 4`:
|
||||
return regular ? NpcType.SatelliteLizard : NpcType.Yowie;
|
||||
case `${0x113}, 4`:
|
||||
return NpcType.Girtablulu;
|
||||
}
|
||||
|
||||
switch (type_id) {
|
||||
case 0x004:
|
||||
return NpcType.FemaleFat;
|
||||
case 0x005:
|
||||
return NpcType.FemaleMacho;
|
||||
case 0x007:
|
||||
return NpcType.FemaleTall;
|
||||
case 0x00a:
|
||||
return NpcType.MaleDwarf;
|
||||
case 0x00b:
|
||||
return NpcType.MaleFat;
|
||||
case 0x00c:
|
||||
return NpcType.MaleMacho;
|
||||
case 0x00d:
|
||||
return NpcType.MaleOld;
|
||||
case 0x019:
|
||||
return NpcType.BlueSoldier;
|
||||
case 0x01a:
|
||||
return NpcType.RedSoldier;
|
||||
case 0x01b:
|
||||
return NpcType.Principal;
|
||||
case 0x01c:
|
||||
return NpcType.Tekker;
|
||||
case 0x01d:
|
||||
return NpcType.GuildLady;
|
||||
case 0x01e:
|
||||
return NpcType.Scientist;
|
||||
case 0x01f:
|
||||
return NpcType.Nurse;
|
||||
case 0x020:
|
||||
return NpcType.Irene;
|
||||
case 0x0f1:
|
||||
return NpcType.ItemShop;
|
||||
case 0x0fe:
|
||||
return NpcType.Nurse2;
|
||||
}
|
||||
|
||||
return NpcType.Unknown;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { InstructionSegment, Segment, SegmentType } from "../../asm/instructions";
|
||||
import { InstructionSegment, SegmentType } from "../../asm/instructions";
|
||||
import { OP_SET_EPISODE } from "../../asm/opcodes";
|
||||
import { prs_compress } from "../../compression/prs/compress";
|
||||
import { prs_decompress } from "../../compression/prs/decompress";
|
||||
@ -7,41 +7,20 @@ import { Cursor } from "../../block/cursor/Cursor";
|
||||
import { ResizableBlockCursor } from "../../block/cursor/ResizableBlockCursor";
|
||||
import { Endianness } from "../../block/Endianness";
|
||||
import { parse_bin, write_bin } from "./bin";
|
||||
import { DatNpc, DatObject, DatUnknown, parse_dat, write_dat } from "./dat";
|
||||
import { QuestEvent, QuestNpc, QuestObject } from "./entities";
|
||||
import { DatEntity, parse_dat, write_dat } from "./dat";
|
||||
import { Quest, QuestNpc, QuestObject } from "./Quest";
|
||||
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 { npc_data, NpcType } from "./npc_types";
|
||||
import { reinterpret_f32_as_i32, reinterpret_i32_as_f32 } from "../../../primitive_conversion";
|
||||
import { LogManager } from "../../../Logger";
|
||||
import { parse_object_code, write_object_code } from "./object_code";
|
||||
import { get_map_designations } from "../../asm/data_flow_analysis/get_map_designations";
|
||||
import { basename } from "../../../util";
|
||||
import { version_to_bin_format } from "./BinFormat";
|
||||
import { Version } from "./Version";
|
||||
import { ArrayBufferBlock } from "../../block/ArrayBufferBlock";
|
||||
|
||||
const logger = LogManager.get("core/data_formats/parsing/quest");
|
||||
|
||||
export type Quest = {
|
||||
readonly id: number;
|
||||
readonly language: number;
|
||||
readonly name: string;
|
||||
readonly short_description: string;
|
||||
readonly long_description: string;
|
||||
readonly episode: Episode;
|
||||
readonly objects: readonly QuestObject[];
|
||||
readonly npcs: readonly QuestNpc[];
|
||||
readonly events: readonly QuestEvent[];
|
||||
/**
|
||||
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
|
||||
*/
|
||||
readonly dat_unknowns: readonly DatUnknown[];
|
||||
readonly object_code: readonly Segment[];
|
||||
readonly shop_items: readonly number[];
|
||||
readonly map_designations: Map<number, number>;
|
||||
};
|
||||
|
||||
export function parse_bin_dat_to_quest(
|
||||
bin_cursor: Cursor,
|
||||
dat_cursor: Cursor,
|
||||
@ -54,6 +33,8 @@ export function parse_bin_dat_to_quest(
|
||||
const dat_decompressed = prs_decompress(dat_cursor);
|
||||
const dat = parse_dat(dat_decompressed);
|
||||
const objects = parse_obj_data(dat.objs);
|
||||
// Initialize NPCs with random episode and correct it later.
|
||||
const npcs = parse_npc_data(Episode.I, dat.npcs);
|
||||
|
||||
// Extract episode and map designations from object code.
|
||||
let episode = Episode.I;
|
||||
@ -62,7 +43,7 @@ export function parse_bin_dat_to_quest(
|
||||
const object_code = parse_object_code(
|
||||
bin.object_code,
|
||||
bin.label_offsets,
|
||||
extract_script_entry_points(objects, dat.npcs),
|
||||
extract_script_entry_points(objects, npcs),
|
||||
lenient,
|
||||
format,
|
||||
);
|
||||
@ -83,6 +64,11 @@ export function parse_bin_dat_to_quest(
|
||||
|
||||
if (label_0_segment) {
|
||||
episode = get_episode(label_0_segment);
|
||||
|
||||
for (const npc of npcs) {
|
||||
npc.episode = episode;
|
||||
}
|
||||
|
||||
map_designations = get_map_designations(instruction_segments, label_0_segment);
|
||||
} else {
|
||||
logger.warn(`No instruction for label 0 found.`);
|
||||
@ -91,21 +77,21 @@ export function parse_bin_dat_to_quest(
|
||||
logger.warn("File contains no instruction labels.");
|
||||
}
|
||||
|
||||
return {
|
||||
id: bin.quest_id,
|
||||
language: bin.language,
|
||||
name: bin.quest_name,
|
||||
short_description: bin.short_description,
|
||||
long_description: bin.long_description,
|
||||
return new Quest(
|
||||
bin.quest_id,
|
||||
bin.language,
|
||||
bin.quest_name,
|
||||
bin.short_description,
|
||||
bin.long_description,
|
||||
episode,
|
||||
objects,
|
||||
npcs: parse_npc_data(episode, dat.npcs),
|
||||
events: dat.events,
|
||||
dat_unknowns: dat.unknowns,
|
||||
npcs,
|
||||
dat.events,
|
||||
dat.unknowns,
|
||||
object_code,
|
||||
shop_items: bin.shop_items,
|
||||
bin.shop_items,
|
||||
map_designations,
|
||||
};
|
||||
);
|
||||
}
|
||||
|
||||
export function parse_qst_to_quest(
|
||||
@ -235,18 +221,18 @@ function get_episode(func_0_segment: InstructionSegment): Episode {
|
||||
|
||||
function extract_script_entry_points(
|
||||
objects: readonly QuestObject[],
|
||||
npcs: readonly DatNpc[],
|
||||
npcs: readonly QuestNpc[],
|
||||
): number[] {
|
||||
const entry_points = new Set([0]);
|
||||
|
||||
for (const obj of objects) {
|
||||
const entry_point = obj.properties.get("script_label");
|
||||
const entry_point = obj.script_label;
|
||||
|
||||
if (entry_point != undefined) {
|
||||
entry_points.add(entry_point);
|
||||
}
|
||||
|
||||
const entry_point_2 = obj.properties.get("script_label_2");
|
||||
const entry_point_2 = obj.script_label_2;
|
||||
|
||||
if (entry_point_2 != undefined) {
|
||||
entry_points.add(entry_point_2);
|
||||
@ -254,403 +240,43 @@ function extract_script_entry_points(
|
||||
}
|
||||
|
||||
for (const npc of npcs) {
|
||||
entry_points.add(Math.round(npc.script_label));
|
||||
entry_points.add(npc.script_label);
|
||||
}
|
||||
|
||||
return [...entry_points];
|
||||
}
|
||||
|
||||
function parse_obj_data(objs: readonly DatObject[]): QuestObject[] {
|
||||
return objs.map(obj_data => {
|
||||
const type = pso_id_to_object_type(obj_data.type_id);
|
||||
|
||||
return {
|
||||
type,
|
||||
id: obj_data.id,
|
||||
group_id: obj_data.group_id,
|
||||
area_id: obj_data.area_id,
|
||||
section_id: obj_data.section_id,
|
||||
position: obj_data.position,
|
||||
rotation: obj_data.rotation,
|
||||
properties: new Map(
|
||||
obj_data.properties.map((value, index) => {
|
||||
if (
|
||||
index === 3 &&
|
||||
(type === ObjectType.ScriptCollision ||
|
||||
type === ObjectType.ForestConsole ||
|
||||
type === ObjectType.TalkLinkToSupport)
|
||||
) {
|
||||
return ["script_label", value];
|
||||
} else if (index === 4 && type === ObjectType.RicoMessagePod) {
|
||||
return ["script_label", value];
|
||||
} else if (index === 5 && type === ObjectType.RicoMessagePod) {
|
||||
return ["script_label_2", value];
|
||||
} else {
|
||||
return [`property_${index}`, value];
|
||||
}
|
||||
}),
|
||||
function parse_obj_data(objs: readonly DatEntity[]): QuestObject[] {
|
||||
return objs.map(
|
||||
obj_data =>
|
||||
new QuestObject(
|
||||
obj_data.area_id,
|
||||
new ArrayBufferBlock(obj_data.data, Endianness.Little),
|
||||
),
|
||||
unknown: obj_data.unknown,
|
||||
};
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
function parse_npc_data(episode: number, npcs: readonly DatNpc[]): QuestNpc[] {
|
||||
return npcs.map(npc_data => {
|
||||
return {
|
||||
type: get_npc_type(episode, npc_data),
|
||||
area_id: npc_data.area_id,
|
||||
section_id: npc_data.section_id,
|
||||
wave: npc_data.wave,
|
||||
pso_wave2: npc_data.wave2,
|
||||
position: npc_data.position,
|
||||
rotation: npc_data.rotation,
|
||||
scale: npc_data.scale,
|
||||
unknown: npc_data.unknown,
|
||||
pso_type_id: npc_data.type_id,
|
||||
npc_id: npc_data.npc_id,
|
||||
script_label: Math.round(npc_data.script_label),
|
||||
pso_roaming: npc_data.roaming,
|
||||
};
|
||||
});
|
||||
function parse_npc_data(episode: number, npcs: readonly DatEntity[]): QuestNpc[] {
|
||||
return npcs.map(
|
||||
npc_data =>
|
||||
new QuestNpc(
|
||||
episode,
|
||||
npc_data.area_id,
|
||||
new ArrayBufferBlock(npc_data.data, Endianness.Little),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: detect Mothmant, St. Rappy, Hallo Rappy, Egg Rappy, Death Gunner, Bulk and Recon.
|
||||
function get_npc_type(episode: number, { type_id, scale, roaming, area_id }: DatNpc): NpcType {
|
||||
const regular = Math.abs(scale.y - 1) > 0.00001;
|
||||
|
||||
switch (`${type_id}, ${roaming % 3}, ${episode}`) {
|
||||
case `${0x044}, 0, 1`:
|
||||
return NpcType.Booma;
|
||||
case `${0x044}, 1, 1`:
|
||||
return NpcType.Gobooma;
|
||||
case `${0x044}, 2, 1`:
|
||||
return NpcType.Gigobooma;
|
||||
|
||||
case `${0x063}, 0, 1`:
|
||||
return NpcType.EvilShark;
|
||||
case `${0x063}, 1, 1`:
|
||||
return NpcType.PalShark;
|
||||
case `${0x063}, 2, 1`:
|
||||
return NpcType.GuilShark;
|
||||
|
||||
case `${0x0a6}, 0, 1`:
|
||||
return NpcType.Dimenian;
|
||||
case `${0x0a6}, 0, 2`:
|
||||
return NpcType.Dimenian2;
|
||||
case `${0x0a6}, 1, 1`:
|
||||
return NpcType.LaDimenian;
|
||||
case `${0x0a6}, 1, 2`:
|
||||
return NpcType.LaDimenian2;
|
||||
case `${0x0a6}, 2, 1`:
|
||||
return NpcType.SoDimenian;
|
||||
case `${0x0a6}, 2, 2`:
|
||||
return NpcType.SoDimenian2;
|
||||
|
||||
case `${0x0d6}, 0, 2`:
|
||||
return NpcType.Mericarol;
|
||||
case `${0x0d6}, 1, 2`:
|
||||
return NpcType.Mericus;
|
||||
case `${0x0d6}, 2, 2`:
|
||||
return NpcType.Merikle;
|
||||
|
||||
case `${0x115}, 0, 4`:
|
||||
return NpcType.Boota;
|
||||
case `${0x115}, 1, 4`:
|
||||
return NpcType.ZeBoota;
|
||||
case `${0x115}, 2, 4`:
|
||||
return NpcType.BaBoota;
|
||||
case `${0x117}, 0, 4`:
|
||||
return NpcType.Goran;
|
||||
case `${0x117}, 1, 4`:
|
||||
return NpcType.PyroGoran;
|
||||
case `${0x117}, 2, 4`:
|
||||
return NpcType.GoranDetonator;
|
||||
}
|
||||
|
||||
switch (`${type_id}, ${roaming % 2}, ${episode}`) {
|
||||
case `${0x040}, 0, 1`:
|
||||
return NpcType.Hildebear;
|
||||
case `${0x040}, 0, 2`:
|
||||
return NpcType.Hildebear2;
|
||||
case `${0x040}, 1, 1`:
|
||||
return NpcType.Hildeblue;
|
||||
case `${0x040}, 1, 2`:
|
||||
return NpcType.Hildeblue2;
|
||||
case `${0x041}, 0, 1`:
|
||||
return NpcType.RagRappy;
|
||||
case `${0x041}, 0, 2`:
|
||||
return NpcType.RagRappy2;
|
||||
case `${0x041}, 0, 4`:
|
||||
return NpcType.SandRappy;
|
||||
case `${0x041}, 1, 1`:
|
||||
return NpcType.AlRappy;
|
||||
case `${0x041}, 1, 2`:
|
||||
return NpcType.LoveRappy;
|
||||
case `${0x041}, 1, 4`:
|
||||
return NpcType.DelRappy;
|
||||
|
||||
case `${0x080}, 0, 1`:
|
||||
return NpcType.Dubchic;
|
||||
case `${0x080}, 0, 2`:
|
||||
return NpcType.Dubchic2;
|
||||
case `${0x080}, 1, 1`:
|
||||
return NpcType.Gilchic;
|
||||
case `${0x080}, 1, 2`:
|
||||
return NpcType.Gilchic2;
|
||||
|
||||
case `${0x0d4}, 0, 2`:
|
||||
return NpcType.SinowBerill;
|
||||
case `${0x0d4}, 1, 2`:
|
||||
return NpcType.SinowSpigell;
|
||||
case `${0x0d5}, 0, 2`:
|
||||
return NpcType.Merillia;
|
||||
case `${0x0d5}, 1, 2`:
|
||||
return NpcType.Meriltas;
|
||||
case `${0x0d7}, 0, 2`:
|
||||
return NpcType.UlGibbon;
|
||||
case `${0x0d7}, 1, 2`:
|
||||
return NpcType.ZolGibbon;
|
||||
|
||||
case `${0x0dd}, 0, 2`:
|
||||
return NpcType.Dolmolm;
|
||||
case `${0x0dd}, 1, 2`:
|
||||
return NpcType.Dolmdarl;
|
||||
case `${0x0e0}, 0, 2`:
|
||||
return area_id > 15 ? NpcType.Epsilon : NpcType.SinowZoa;
|
||||
case `${0x0e0}, 1, 2`:
|
||||
return area_id > 15 ? NpcType.Epsilon : NpcType.SinowZele;
|
||||
|
||||
case `${0x112}, 0, 4`:
|
||||
return NpcType.MerissaA;
|
||||
case `${0x112}, 1, 4`:
|
||||
return NpcType.MerissaAA;
|
||||
case `${0x114}, 0, 4`:
|
||||
return NpcType.Zu;
|
||||
case `${0x114}, 1, 4`:
|
||||
return NpcType.Pazuzu;
|
||||
case `${0x116}, 0, 4`:
|
||||
return NpcType.Dorphon;
|
||||
case `${0x116}, 1, 4`:
|
||||
return NpcType.DorphonEclair;
|
||||
case `${0x119}, 0, 4`:
|
||||
return regular ? NpcType.SaintMilion : NpcType.Kondrieu;
|
||||
case `${0x119}, 1, 4`:
|
||||
return regular ? NpcType.Shambertin : NpcType.Kondrieu;
|
||||
}
|
||||
|
||||
switch (`${type_id}, ${episode}`) {
|
||||
case `${0x042}, 1`:
|
||||
return NpcType.Monest;
|
||||
case `${0x042}, 2`:
|
||||
return NpcType.Monest2;
|
||||
case `${0x043}, 1`:
|
||||
return regular ? NpcType.SavageWolf : NpcType.BarbarousWolf;
|
||||
case `${0x043}, 2`:
|
||||
return regular ? NpcType.SavageWolf2 : NpcType.BarbarousWolf2;
|
||||
|
||||
case `${0x060}, 1`:
|
||||
return NpcType.GrassAssassin;
|
||||
case `${0x060}, 2`:
|
||||
return NpcType.GrassAssassin2;
|
||||
case `${0x061}, 1`:
|
||||
return area_id > 15 ? NpcType.DelLily : regular ? NpcType.PoisonLily : NpcType.NarLily;
|
||||
case `${0x061}, 2`:
|
||||
return area_id > 15
|
||||
? NpcType.DelLily
|
||||
: regular
|
||||
? NpcType.PoisonLily2
|
||||
: NpcType.NarLily2;
|
||||
case `${0x062}, 1`:
|
||||
return NpcType.NanoDragon;
|
||||
case `${0x064}, 1`:
|
||||
return regular ? NpcType.PofuillySlime : NpcType.PouillySlime;
|
||||
case `${0x065}, 1`:
|
||||
return NpcType.PanArms;
|
||||
case `${0x065}, 2`:
|
||||
return NpcType.PanArms2;
|
||||
|
||||
case `${0x081}, 1`:
|
||||
return NpcType.Garanz;
|
||||
case `${0x081}, 2`:
|
||||
return NpcType.Garanz2;
|
||||
case `${0x082}, 1`:
|
||||
return regular ? NpcType.SinowBeat : NpcType.SinowGold;
|
||||
case `${0x083}, 1`:
|
||||
return NpcType.Canadine;
|
||||
case `${0x084}, 1`:
|
||||
return NpcType.Canane;
|
||||
case `${0x085}, 1`:
|
||||
return NpcType.Dubswitch;
|
||||
case `${0x085}, 2`:
|
||||
return NpcType.Dubswitch2;
|
||||
|
||||
case `${0x0a0}, 1`:
|
||||
return NpcType.Delsaber;
|
||||
case `${0x0a0}, 2`:
|
||||
return NpcType.Delsaber2;
|
||||
case `${0x0a1}, 1`:
|
||||
return NpcType.ChaosSorcerer;
|
||||
case `${0x0a1}, 2`:
|
||||
return NpcType.ChaosSorcerer2;
|
||||
case `${0x0a2}, 1`:
|
||||
return NpcType.DarkGunner;
|
||||
case `${0x0a4}, 1`:
|
||||
return NpcType.ChaosBringer;
|
||||
case `${0x0a5}, 1`:
|
||||
return NpcType.DarkBelra;
|
||||
case `${0x0a5}, 2`:
|
||||
return NpcType.DarkBelra2;
|
||||
case `${0x0a7}, 1`:
|
||||
return NpcType.Bulclaw;
|
||||
case `${0x0a8}, 1`:
|
||||
return NpcType.Claw;
|
||||
|
||||
case `${0x0c0}, 1`:
|
||||
return NpcType.Dragon;
|
||||
case `${0x0c0}, 2`:
|
||||
return NpcType.GalGryphon;
|
||||
case `${0x0c1}, 1`:
|
||||
return NpcType.DeRolLe;
|
||||
case `${0x0c2}, 1`:
|
||||
return NpcType.VolOptPart1;
|
||||
case `${0x0c5}, 1`:
|
||||
return NpcType.VolOptPart2;
|
||||
case `${0x0c8}, 1`:
|
||||
return NpcType.DarkFalz;
|
||||
case `${0x0ca}, 2`:
|
||||
return NpcType.OlgaFlow;
|
||||
case `${0x0cb}, 2`:
|
||||
return NpcType.BarbaRay;
|
||||
case `${0x0cc}, 2`:
|
||||
return NpcType.GolDragon;
|
||||
|
||||
case `${0x0d8}, 2`:
|
||||
return NpcType.Gibbles;
|
||||
case `${0x0d9}, 2`:
|
||||
return NpcType.Gee;
|
||||
case `${0x0da}, 2`:
|
||||
return NpcType.GiGue;
|
||||
|
||||
case `${0x0db}, 2`:
|
||||
return NpcType.Deldepth;
|
||||
case `${0x0dc}, 2`:
|
||||
return NpcType.Delbiter;
|
||||
case `${0x0de}, 2`:
|
||||
return NpcType.Morfos;
|
||||
case `${0x0df}, 2`:
|
||||
return NpcType.Recobox;
|
||||
case `${0x0e1}, 2`:
|
||||
return NpcType.IllGill;
|
||||
|
||||
case `${0x110}, 4`:
|
||||
return NpcType.Astark;
|
||||
case `${0x111}, 4`:
|
||||
return regular ? NpcType.SatelliteLizard : NpcType.Yowie;
|
||||
case `${0x113}, 4`:
|
||||
return NpcType.Girtablulu;
|
||||
}
|
||||
|
||||
switch (type_id) {
|
||||
case 0x004:
|
||||
return NpcType.FemaleFat;
|
||||
case 0x005:
|
||||
return NpcType.FemaleMacho;
|
||||
case 0x007:
|
||||
return NpcType.FemaleTall;
|
||||
case 0x00a:
|
||||
return NpcType.MaleDwarf;
|
||||
case 0x00b:
|
||||
return NpcType.MaleFat;
|
||||
case 0x00c:
|
||||
return NpcType.MaleMacho;
|
||||
case 0x00d:
|
||||
return NpcType.MaleOld;
|
||||
case 0x019:
|
||||
return NpcType.BlueSoldier;
|
||||
case 0x01a:
|
||||
return NpcType.RedSoldier;
|
||||
case 0x01b:
|
||||
return NpcType.Principal;
|
||||
case 0x01c:
|
||||
return NpcType.Tekker;
|
||||
case 0x01d:
|
||||
return NpcType.GuildLady;
|
||||
case 0x01e:
|
||||
return NpcType.Scientist;
|
||||
case 0x01f:
|
||||
return NpcType.Nurse;
|
||||
case 0x020:
|
||||
return NpcType.Irene;
|
||||
case 0x0f1:
|
||||
return NpcType.ItemShop;
|
||||
case 0x0fe:
|
||||
return NpcType.Nurse2;
|
||||
}
|
||||
|
||||
return NpcType.Unknown;
|
||||
function objects_to_dat_data(objects: readonly QuestObject[]): DatEntity[] {
|
||||
return objects.map(object => ({
|
||||
area_id: object.area_id,
|
||||
data: object.data.backing_buffer,
|
||||
}));
|
||||
}
|
||||
|
||||
function objects_to_dat_data(objects: readonly QuestObject[]): DatObject[] {
|
||||
return objects.map(object => {
|
||||
const props = [...object.properties.values()];
|
||||
const props_target_len = 7;
|
||||
|
||||
// Truncate or pad property list if it is not the correct length.
|
||||
if (props.length > props_target_len) {
|
||||
logger.warn(
|
||||
`Object #${object.id} has too many properties. Truncating property list to length of ${props_target_len}.`,
|
||||
);
|
||||
props.splice(props_target_len);
|
||||
} else if (props.length < props_target_len) {
|
||||
const to_add = props_target_len - props.length;
|
||||
for (let i = 0; i < to_add; i++) {
|
||||
props.push(0);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type_id: object_data(object.type).pso_id!,
|
||||
id: object.id,
|
||||
group_id: object.group_id,
|
||||
section_id: object.section_id,
|
||||
position: object.position,
|
||||
rotation: object.rotation,
|
||||
properties: props,
|
||||
area_id: object.area_id,
|
||||
unknown: object.unknown,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function npcs_to_dat_data(npcs: readonly QuestNpc[]): DatNpc[] {
|
||||
return npcs.map(npc => {
|
||||
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;
|
||||
|
||||
const scale_y = reinterpret_i32_as_f32(
|
||||
(reinterpret_f32_as_i32(npc.scale.y) & ~0x800000) | (regular ? 0 : 0x800000),
|
||||
);
|
||||
|
||||
const scale = { x: npc.scale.x, y: scale_y, z: npc.scale.z };
|
||||
|
||||
return {
|
||||
type_id,
|
||||
wave: npc.wave,
|
||||
wave2: npc.pso_wave2,
|
||||
section_id: npc.section_id,
|
||||
position: npc.position,
|
||||
rotation: npc.rotation,
|
||||
scale,
|
||||
npc_id: npc.npc_id,
|
||||
script_label: npc.script_label,
|
||||
roaming,
|
||||
area_id: npc.area_id,
|
||||
unknown: npc.unknown,
|
||||
};
|
||||
});
|
||||
function npcs_to_dat_data(npcs: readonly QuestNpc[]): DatEntity[] {
|
||||
return npcs.map(npc => ({
|
||||
area_id: npc.area_id,
|
||||
data: npc.data.backing_buffer,
|
||||
}));
|
||||
}
|
||||
|
@ -210,18 +210,18 @@ export type NpcTypeData = {
|
||||
/**
|
||||
* Type ID used by the game.
|
||||
*/
|
||||
readonly pso_type_id?: number;
|
||||
readonly type_id?: number;
|
||||
/**
|
||||
* Roaming value used by the game.
|
||||
* Skin value used by the game.
|
||||
*/
|
||||
readonly pso_roaming?: number;
|
||||
readonly skin?: 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;
|
||||
readonly regular?: boolean;
|
||||
};
|
||||
|
||||
export const NPC_TYPES: NpcType[] = [];
|
||||
@ -258,9 +258,9 @@ function define_npc_type_data(
|
||||
enemy: boolean,
|
||||
rare_type: NpcType | undefined,
|
||||
area_ids: number[],
|
||||
pso_type_id: number | undefined,
|
||||
pso_roaming: number | undefined,
|
||||
pso_regular: boolean | undefined,
|
||||
type_id: number | undefined,
|
||||
skin: number | undefined,
|
||||
regular: boolean | undefined,
|
||||
): void {
|
||||
NPC_TYPES.push(npc_type);
|
||||
|
||||
@ -276,9 +276,9 @@ function define_npc_type_data(
|
||||
enemy,
|
||||
rare_type,
|
||||
area_ids,
|
||||
pso_type_id,
|
||||
pso_roaming,
|
||||
pso_regular,
|
||||
type_id,
|
||||
skin,
|
||||
regular,
|
||||
});
|
||||
|
||||
if (episode) {
|
||||
|
@ -291,7 +291,7 @@ export type ObjectTypeData = {
|
||||
* This array can be indexed with an {@link Episode} value.
|
||||
*/
|
||||
readonly area_ids: number[][];
|
||||
readonly pso_id?: number;
|
||||
readonly type_id?: number;
|
||||
};
|
||||
|
||||
export const OBJECT_TYPES: ObjectType[] = [];
|
||||
@ -300,8 +300,8 @@ export function object_data(type: ObjectType): ObjectTypeData {
|
||||
return OBJECT_TYPE_DATA[type];
|
||||
}
|
||||
|
||||
export function pso_id_to_object_type(psoId: number): ObjectType {
|
||||
switch (psoId) {
|
||||
export function id_to_object_type(id: number): ObjectType {
|
||||
switch (id) {
|
||||
default:
|
||||
return ObjectType.Unknown;
|
||||
|
||||
@ -870,7 +870,7 @@ const OBJECT_TYPE_DATA: ObjectTypeData[] = [];
|
||||
|
||||
function define_object_type_data(
|
||||
object_type: ObjectType,
|
||||
pso_id: number | undefined,
|
||||
type_id: number | undefined,
|
||||
name: string,
|
||||
area_ids: [Episode, number[]][],
|
||||
): void {
|
||||
@ -885,7 +885,7 @@ function define_object_type_data(
|
||||
OBJECT_TYPE_DATA[object_type] = Object.freeze({
|
||||
name,
|
||||
area_ids: area_ids_per_episode,
|
||||
pso_id,
|
||||
type_id,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -130,3 +130,7 @@ export function number_to_hex_string(num: number, min_len: number = 8): string {
|
||||
export function browser_supports_webassembly(): boolean {
|
||||
return typeof window === "object" && typeof window.WebAssembly === "object";
|
||||
}
|
||||
|
||||
export function is_promise(value: unknown): value is Promise<any> {
|
||||
return value && typeof value === "object" && "then" in value && "finally" in value;
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Action } from "../../core/undo/Action";
|
||||
import { QuestEntityModel } from "../model/QuestEntityModel";
|
||||
import { entity_data } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { QuestModel } from "../model/QuestModel";
|
||||
import { QuestEditorStore } from "../stores/QuestEditorStore";
|
||||
import { entity_data } from "../../core/data_formats/parsing/quest/Quest";
|
||||
|
||||
export class CreateEntityAction implements Action {
|
||||
readonly description: string;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Action } from "../../core/undo/Action";
|
||||
import { QuestEntityModel } from "../model/QuestEntityModel";
|
||||
import { entity_data } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { QuestEditorStore } from "../stores/QuestEditorStore";
|
||||
import { QuestModel } from "../model/QuestModel";
|
||||
import { entity_data } from "../../core/data_formats/parsing/quest/Quest";
|
||||
|
||||
export class RemoveEntityAction implements Action {
|
||||
readonly description: string;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Action } from "../../core/undo/Action";
|
||||
import { QuestEntityModel } from "../model/QuestEntityModel";
|
||||
import { entity_data } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { Euler } from "three";
|
||||
import { QuestEditorStore } from "../stores/QuestEditorStore";
|
||||
import { entity_data } from "../../core/data_formats/parsing/quest/Quest";
|
||||
|
||||
export class RotateEntityAction implements Action {
|
||||
readonly description: string;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Action } from "../../core/undo/Action";
|
||||
import { QuestEntityModel } from "../model/QuestEntityModel";
|
||||
import { entity_data } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { SectionModel } from "../model/SectionModel";
|
||||
import { Vector3 } from "three";
|
||||
import { QuestEditorStore } from "../stores/QuestEditorStore";
|
||||
import { entity_data } from "../../core/data_formats/parsing/quest/Quest";
|
||||
|
||||
export class TranslateEntityAction implements Action {
|
||||
readonly description: string;
|
||||
|
@ -2,11 +2,11 @@ import { with_disposer } from "../../../test/src/core/observables/disposable_hel
|
||||
import { GuiStore } from "../../core/stores/GuiStore";
|
||||
import { create_area_store } from "../../../test/src/quest_editor/stores/store_creation";
|
||||
import { QuestEditorStore } from "../stores/QuestEditorStore";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import { DebugController } from "./DebugController";
|
||||
import { LogStore } from "../stores/LogStore";
|
||||
import { create_new_quest } from "../stores/quest_creation";
|
||||
import { next_animation_frame } from "../../../test/src/utils";
|
||||
import { load_default_quest_model, next_animation_frame } from "../../../test/src/utils";
|
||||
import { disassemble } from "../scripting/disassembly";
|
||||
import { assemble } from "../scripting/assembly";
|
||||
|
||||
test("Some widgets should only be enabled when a quest is loaded.", async () =>
|
||||
with_disposer(async disposer => {
|
||||
@ -21,7 +21,7 @@ test("Some widgets should only be enabled when a quest is loaded.", async () =>
|
||||
expect(ctrl.can_debug.val).toBe(false);
|
||||
expect(ctrl.can_step.val).toBe(false);
|
||||
|
||||
await quest_editor_store.set_current_quest(create_new_quest(area_store, Episode.I));
|
||||
await quest_editor_store.set_current_quest(load_default_quest_model(area_store));
|
||||
|
||||
expect(ctrl.can_debug.val).toBe(true);
|
||||
expect(ctrl.can_step.val).toBe(false);
|
||||
@ -37,26 +37,41 @@ test("Debugging controls should be enabled and disabled at the right times.", as
|
||||
);
|
||||
const ctrl = disposer.add(new DebugController(gui_store, quest_editor_store, log_store));
|
||||
|
||||
await quest_editor_store.set_current_quest(create_new_quest(area_store, Episode.I));
|
||||
const quest = load_default_quest_model(area_store);
|
||||
// Disassemble and reassemble the IR to ensure we have source locations in the final IR.
|
||||
quest.object_code.splice(
|
||||
0,
|
||||
Infinity,
|
||||
...assemble(disassemble(quest.object_code)).object_code,
|
||||
);
|
||||
|
||||
await quest_editor_store.set_current_quest(quest);
|
||||
|
||||
// Before starting we can't step or stop.
|
||||
expect(ctrl.can_step.val).toBe(false);
|
||||
expect(ctrl.can_stop.val).toBe(false);
|
||||
|
||||
ctrl.debug();
|
||||
await next_animation_frame();
|
||||
|
||||
// When all threads have yielded, all we can do is stop.
|
||||
expect(ctrl.can_step.val).toBe(false);
|
||||
expect(ctrl.can_stop.val).toBe(true);
|
||||
|
||||
ctrl.stop();
|
||||
|
||||
// After stopping we can't step or stop anymore.
|
||||
expect(ctrl.can_step.val).toBe(false);
|
||||
expect(ctrl.can_stop.val).toBe(false);
|
||||
|
||||
quest_editor_store.quest_runner.set_breakpoint(5);
|
||||
// After hitting a breakpoint, we can step and stop.
|
||||
expect(quest_editor_store.quest_runner.set_breakpoint(5)).toBe(true);
|
||||
|
||||
ctrl.debug();
|
||||
await next_animation_frame();
|
||||
|
||||
expect(quest_editor_store.quest_runner.pause_location.val).toBe(5);
|
||||
|
||||
expect(ctrl.can_step.val).toBe(true);
|
||||
expect(ctrl.can_stop.val).toBe(true);
|
||||
}));
|
||||
|
@ -4,11 +4,10 @@ import {
|
||||
create_quest_editor_store,
|
||||
} from "../../../test/src/quest_editor/stores/store_creation";
|
||||
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
|
||||
import { create_new_quest } from "../stores/quest_creation";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import { Vector3 } from "three";
|
||||
import { euler } from "../model/euler";
|
||||
import { deg_to_rad } from "../../core/math";
|
||||
import { load_default_quest_model } from "../../../test/src/utils";
|
||||
|
||||
test("When input values change, this should be reflected in the selected entity.", () =>
|
||||
with_disposer(disposer => {
|
||||
@ -16,7 +15,7 @@ test("When input values change, this should be reflected in the selected entity.
|
||||
const store = create_quest_editor_store(disposer, area_store);
|
||||
const ctrl = new EntityInfoController(store);
|
||||
|
||||
const quest = create_new_quest(area_store, Episode.I);
|
||||
const quest = load_default_quest_model(area_store);
|
||||
const entity = quest.objects.get(0);
|
||||
entity.set_position(new Vector3(0, 0, 0));
|
||||
entity.set_rotation(euler(0, 0, 0));
|
||||
|
@ -2,13 +2,13 @@ import { Controller } from "../../core/controllers/Controller";
|
||||
import { QuestEditorStore } from "../stores/QuestEditorStore";
|
||||
import { Property } from "../../core/observable/property/Property";
|
||||
import { QuestNpcModel } from "../model/QuestNpcModel";
|
||||
import { entity_data } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { property } from "../../core/observable";
|
||||
import { Euler, Vector3 } from "three";
|
||||
import { deg_to_rad } from "../../core/math";
|
||||
import { TranslateEntityAction } from "../actions/TranslateEntityAction";
|
||||
import { RotateEntityAction } from "../actions/RotateEntityAction";
|
||||
import { euler } from "../model/euler";
|
||||
import { entity_data } from "../../core/data_formats/parsing/quest/Quest";
|
||||
|
||||
const DUMMY_VECTOR = Object.freeze(new Vector3());
|
||||
const DUMMY_EULER = Object.freeze(new Euler());
|
||||
|
@ -6,14 +6,22 @@ import {
|
||||
import { QuestEditorToolBarController } from "./QuestEditorToolBarController";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
|
||||
import { QuestLoader } from "../loading/QuestLoader";
|
||||
import { FileSystemHttpClient } from "../../../test/src/core/FileSystemHttpClient";
|
||||
|
||||
test("Some widgets should only be enabled when a quest is loaded.", async () =>
|
||||
with_disposer(async disposer => {
|
||||
const quest_loader = disposer.add(new QuestLoader(new FileSystemHttpClient()));
|
||||
const gui_store = disposer.add(new GuiStore());
|
||||
const area_store = create_area_store(disposer);
|
||||
const quest_editor_store = create_quest_editor_store(disposer, area_store);
|
||||
const ctrl = disposer.add(
|
||||
new QuestEditorToolBarController(gui_store, area_store, quest_editor_store),
|
||||
new QuestEditorToolBarController(
|
||||
quest_loader,
|
||||
gui_store,
|
||||
area_store,
|
||||
quest_editor_store,
|
||||
),
|
||||
);
|
||||
|
||||
expect(ctrl.can_save.val).toBe(false);
|
||||
|
@ -7,12 +7,10 @@ import { Property } from "../../core/observable/property/Property";
|
||||
import { undo_manager } from "../../core/undo/UndoManager";
|
||||
import { Controller } from "../../core/controllers/Controller";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import { create_new_quest } from "../stores/quest_creation";
|
||||
import { open_files, read_file } from "../../core/files";
|
||||
import {
|
||||
parse_bin_dat_to_quest,
|
||||
parse_qst_to_quest,
|
||||
Quest,
|
||||
write_quest_qst,
|
||||
} from "../../core/data_formats/parsing/quest";
|
||||
import { ArrayBufferCursor } from "../../core/data_formats/block/cursor/ArrayBufferCursor";
|
||||
@ -24,6 +22,8 @@ import { Version } from "../../core/data_formats/parsing/quest/Version";
|
||||
import { WritableProperty } from "../../core/observable/property/WritableProperty";
|
||||
import { failure, Result } from "../../core/Result";
|
||||
import { Severity } from "../../core/Severity";
|
||||
import { Quest } from "../../core/data_formats/parsing/quest/Quest";
|
||||
import { QuestLoader } from "../loading/QuestLoader";
|
||||
|
||||
const logger = LogManager.get("quest_editor/controllers/QuestEditorToolBarController");
|
||||
|
||||
@ -55,6 +55,7 @@ export class QuestEditorToolBarController extends Controller {
|
||||
readonly version: Property<Version> = this._version;
|
||||
|
||||
constructor(
|
||||
private readonly quest_loader: QuestLoader,
|
||||
gui_store: GuiStore,
|
||||
private readonly area_store: AreaStore,
|
||||
private readonly quest_editor_store: QuestEditorStore,
|
||||
@ -107,7 +108,7 @@ export class QuestEditorToolBarController extends Controller {
|
||||
this.disposables(
|
||||
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-O", async () => {
|
||||
const files = await open_files({ accept: ".bin, .dat, .qst", multiple: true });
|
||||
this.parse_files(files);
|
||||
await this.parse_files(files);
|
||||
}),
|
||||
|
||||
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-Shift-S", this.save_as_clicked),
|
||||
@ -129,7 +130,12 @@ export class QuestEditorToolBarController extends Controller {
|
||||
create_new_quest = async (episode: Episode): Promise<void> => {
|
||||
this.set_filename("");
|
||||
this.set_version(Version.BB);
|
||||
this.quest_editor_store.set_current_quest(create_new_quest(this.area_store, episode));
|
||||
await this.quest_editor_store.set_current_quest(
|
||||
convert_quest_to_model(
|
||||
this.area_store,
|
||||
await this.quest_loader.load_default_quest(episode),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
parse_files = async (files: File[]): Promise<void> => {
|
||||
|
@ -3,9 +3,8 @@ import {
|
||||
create_quest_editor_store,
|
||||
} from "../../../test/src/quest_editor/stores/store_creation";
|
||||
import { QuestInfoController } from "./QuestInfoController";
|
||||
import { create_new_quest } from "../stores/quest_creation";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
|
||||
import { load_default_quest_model } from "../../../test/src/utils";
|
||||
|
||||
test("When a property's input value changes, this should be reflected in the current quest object and the undo stack.", async () =>
|
||||
with_disposer(async disposer => {
|
||||
@ -13,7 +12,7 @@ test("When a property's input value changes, this should be reflected in the cur
|
||||
const store = create_quest_editor_store(disposer, area_store);
|
||||
const ctrl = disposer.add(new QuestInfoController(store));
|
||||
|
||||
await store.set_current_quest(create_new_quest(area_store, Episode.I));
|
||||
await store.set_current_quest(load_default_quest_model(area_store));
|
||||
|
||||
ctrl.set_id(3004);
|
||||
expect(store.current_quest.val!.id.val).toBe(3004);
|
||||
|
@ -4,10 +4,9 @@ import {
|
||||
create_area_store,
|
||||
create_quest_editor_store,
|
||||
} from "../../../test/src/quest_editor/stores/store_creation";
|
||||
import { create_new_quest } from "../stores/quest_creation";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
|
||||
import { undo_manager } from "../../core/undo/UndoManager";
|
||||
import { load_default_quest_model } from "../../../test/src/utils";
|
||||
|
||||
test("Renders correctly without an entity selected.", () => {
|
||||
with_disposer(disposer => {
|
||||
@ -19,7 +18,7 @@ test("Renders correctly without an entity selected.", () => {
|
||||
|
||||
expect(view.element).toMatchSnapshot('should render a "No entity selected." view');
|
||||
|
||||
store.set_current_quest(create_new_quest(area_store, Episode.I));
|
||||
store.set_current_quest(load_default_quest_model(area_store));
|
||||
|
||||
expect(view.element).toMatchSnapshot('should render a "No entity selected." view');
|
||||
});
|
||||
@ -33,7 +32,7 @@ test("Renders correctly with an entity selected.", () => {
|
||||
new EntityInfoView(disposer.add(new EntityInfoController(store))),
|
||||
);
|
||||
|
||||
const quest = create_new_quest(area_store, Episode.I);
|
||||
const quest = load_default_quest_model(area_store);
|
||||
store.set_current_quest(quest);
|
||||
store.set_selected_entity(quest.npcs.get(0));
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { bind_children_to, div, img, span } from "../../core/gui/dom";
|
||||
import "./EntityListView.css";
|
||||
import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/Quest";
|
||||
import { entity_dnd_source } from "./entity_dnd";
|
||||
import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty";
|
||||
import { list_property } from "../../core/observable";
|
||||
|
@ -5,8 +5,7 @@ import {
|
||||
create_quest_editor_store,
|
||||
} from "../../../test/src/quest_editor/stores/store_creation";
|
||||
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
|
||||
import { create_new_quest } from "../stores/quest_creation";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import { load_default_quest_model } from "../../../test/src/utils";
|
||||
|
||||
test("Renders correctly without a current quest.", () =>
|
||||
with_disposer(disposer => {
|
||||
@ -22,7 +21,7 @@ test("Renders correctly with a current quest.", () =>
|
||||
const store = create_quest_editor_store(disposer, area_store);
|
||||
const view = disposer.add(new NpcCountsView(disposer.add(new NpcCountsController(store))));
|
||||
|
||||
store.set_current_quest(create_new_quest(area_store, Episode.I));
|
||||
store.set_current_quest(load_default_quest_model(area_store));
|
||||
|
||||
expect(view.element).toMatchSnapshot("Should render a table with NPC names and counts.");
|
||||
}));
|
||||
|
@ -5,9 +5,12 @@ import { create_area_store } from "../../../test/src/quest_editor/stores/store_c
|
||||
import { QuestEditorStore } from "../stores/QuestEditorStore";
|
||||
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
|
||||
import { LogStore } from "../stores/LogStore";
|
||||
import { QuestLoader } from "../loading/QuestLoader";
|
||||
import { StubHttpClient } from "../../core/HttpClient";
|
||||
|
||||
test("Renders correctly.", () =>
|
||||
with_disposer(disposer => {
|
||||
const quest_loader = disposer.add(new QuestLoader(new StubHttpClient()));
|
||||
const gui_store = disposer.add(new GuiStore());
|
||||
const area_store = create_area_store(disposer);
|
||||
const log_store = disposer.add(new LogStore());
|
||||
@ -17,7 +20,12 @@ test("Renders correctly.", () =>
|
||||
const tool_bar = disposer.add(
|
||||
new QuestEditorToolBarView(
|
||||
disposer.add(
|
||||
new QuestEditorToolBarController(gui_store, area_store, quest_editor_store),
|
||||
new QuestEditorToolBarController(
|
||||
quest_loader,
|
||||
gui_store,
|
||||
area_store,
|
||||
quest_editor_store,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { QuestInfoController } from "../controllers/QuestInfoController";
|
||||
import { undo_manager } from "../../core/undo/UndoManager";
|
||||
import { QuestInfoView } from "./QuestInfoView";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import {
|
||||
create_area_store,
|
||||
create_quest_editor_store,
|
||||
} from "../../../test/src/quest_editor/stores/store_creation";
|
||||
import { create_new_quest } from "../stores/quest_creation";
|
||||
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
|
||||
import { load_default_quest_model } from "../../../test/src/utils";
|
||||
|
||||
test("Renders correctly without a current quest.", () =>
|
||||
with_disposer(disposer => {
|
||||
@ -26,7 +25,7 @@ test("Renders correctly with a current quest.", () =>
|
||||
const store = create_quest_editor_store(disposer);
|
||||
const view = disposer.add(new QuestInfoView(disposer.add(new QuestInfoController(store))));
|
||||
|
||||
await store.set_current_quest(create_new_quest(area_store, Episode.I));
|
||||
await store.set_current_quest(load_default_quest_model(area_store));
|
||||
|
||||
expect(view.element).toMatchSnapshot("should render property inputs");
|
||||
}));
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/Quest";
|
||||
import { Disposable } from "../../core/observable/Disposable";
|
||||
import { Vector2 } from "three";
|
||||
import { div } from "../../core/gui/dom";
|
||||
|
@ -30,6 +30,7 @@ import { EventsController } from "./controllers/EventsController";
|
||||
import { DebugView } from "./gui/DebugView";
|
||||
import { DebugController } from "./controllers/DebugController";
|
||||
import { LogStore } from "./stores/LogStore";
|
||||
import { QuestLoader } from "./loading/QuestLoader";
|
||||
|
||||
export function initialize_quest_editor(
|
||||
http_client: HttpClient,
|
||||
@ -39,6 +40,7 @@ export function initialize_quest_editor(
|
||||
const disposer = new Disposer();
|
||||
|
||||
// Asset Loaders
|
||||
const quest_loader = disposer.add(new QuestLoader(http_client));
|
||||
const area_asset_loader = disposer.add(new AreaAssetLoader(http_client));
|
||||
const entity_asset_loader = disposer.add(new EntityAssetLoader(http_client));
|
||||
|
||||
@ -65,7 +67,12 @@ export function initialize_quest_editor(
|
||||
disposer.add(
|
||||
new QuestEditorToolBarView(
|
||||
disposer.add(
|
||||
new QuestEditorToolBarController(gui_store, area_store, quest_editor_store),
|
||||
new QuestEditorToolBarController(
|
||||
quest_loader,
|
||||
gui_store,
|
||||
area_store,
|
||||
quest_editor_store,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
entity_type_to_string,
|
||||
EntityType,
|
||||
is_npc_type,
|
||||
} from "../../core/data_formats/parsing/quest/entities";
|
||||
} from "../../core/data_formats/parsing/quest/Quest";
|
||||
import { HttpClient } from "../../core/HttpClient";
|
||||
import { Disposable } from "../../core/observable/Disposable";
|
||||
import { LogManager } from "../../core/Logger";
|
||||
@ -424,13 +424,13 @@ function entity_type_to_url(type: EntityType, asset_type: AssetType, no?: number
|
||||
case ObjectType.FallingRock:
|
||||
case ObjectType.DesertFixedTypeBoxBreakableCrystals:
|
||||
case ObjectType.BeeHive:
|
||||
return `/objects/${object_data(type).pso_id}${no_str}.nj`;
|
||||
return `/objects/${object_data(type).type_id}${no_str}.nj`;
|
||||
|
||||
default:
|
||||
return `/objects/${object_data(type).pso_id}${no_str}.xj`;
|
||||
return `/objects/${object_data(type).type_id}${no_str}.xj`;
|
||||
}
|
||||
} else {
|
||||
return `/objects/${object_data(type).pso_id}${no_str}.xvm`;
|
||||
return `/objects/${object_data(type).type_id}${no_str}.xvm`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
37
src/quest_editor/loading/QuestLoader.ts
Normal file
37
src/quest_editor/loading/QuestLoader.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { Disposable } from "../../core/observable/Disposable";
|
||||
import { LoadingCache } from "./LoadingCache";
|
||||
import { HttpClient } from "../../core/HttpClient";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import { DisposablePromise } from "../../core/DisposablePromise";
|
||||
import { parse_qst_to_quest } from "../../core/data_formats/parsing/quest";
|
||||
import { ArrayBufferCursor } from "../../core/data_formats/block/cursor/ArrayBufferCursor";
|
||||
import { Endianness } from "../../core/data_formats/block/Endianness";
|
||||
import { assert } from "../../core/util";
|
||||
import { Quest } from "../../core/data_formats/parsing/quest/Quest";
|
||||
|
||||
export class QuestLoader implements Disposable {
|
||||
private readonly cache = new LoadingCache<string, ArrayBuffer>();
|
||||
|
||||
constructor(private readonly http_client: HttpClient) {}
|
||||
|
||||
load_default_quest(episode: Episode): DisposablePromise<Quest> {
|
||||
if (episode === Episode.II) throw new Error("Episode II not yet supported.");
|
||||
if (episode === Episode.IV) throw new Error("Episode IV not yet supported.");
|
||||
|
||||
return this.load_quest("/defaults/default_ep_1.qst");
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.cache.dispose();
|
||||
}
|
||||
|
||||
private load_quest(path: string): DisposablePromise<Quest> {
|
||||
return this.cache
|
||||
.get_or_set(path, () => this.http_client.get(`/quests${path}`).array_buffer())
|
||||
.then(buffer => {
|
||||
const result = parse_qst_to_quest(new ArrayBufferCursor(buffer, Endianness.Little));
|
||||
assert(result, () => `Quest "${path}" can't be parsed.`);
|
||||
return result.quest;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { QuestNpcModel } from "./QuestNpcModel";
|
||||
import { npc_data, NpcType } from "../../core/data_formats/parsing/quest/npc_types";
|
||||
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import { Vector3 } from "three";
|
||||
import { SectionModel } from "./SectionModel";
|
||||
@ -8,6 +8,7 @@ import { AreaStore } from "../stores/AreaStore";
|
||||
import { StubHttpClient } from "../../core/HttpClient";
|
||||
import { AreaAssetLoader } from "../loading/AreaAssetLoader";
|
||||
import { euler } from "./euler";
|
||||
import { QuestNpc } from "../../core/data_formats/parsing/quest/Quest";
|
||||
|
||||
const area_store = new AreaStore(new AreaAssetLoader(new StubHttpClient()));
|
||||
|
||||
@ -43,19 +44,9 @@ test("After changing section, world position should change accordingly.", () =>
|
||||
});
|
||||
|
||||
function create_entity(): QuestEntityModel {
|
||||
return new QuestNpcModel(
|
||||
NpcType.AlRappy,
|
||||
npc_data(NpcType.AlRappy).pso_type_id!,
|
||||
1,
|
||||
undefined,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
area_store.get_area(Episode.I, 0).id,
|
||||
20,
|
||||
new Vector3(5, 5, 5),
|
||||
euler(0, 0, 0),
|
||||
new Vector3(1, 1, 1),
|
||||
[Array(10).fill(0xdead), Array(4).fill(0xdead)],
|
||||
const entity = new QuestNpcModel(
|
||||
QuestNpc.create(NpcType.AlRappy, area_store.get_area(Episode.I, 0).id, 0),
|
||||
);
|
||||
entity.set_position(new Vector3(5, 5, 5));
|
||||
return entity;
|
||||
}
|
||||
|
@ -1,18 +1,21 @@
|
||||
import { EntityType } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { EntityType, QuestEntity } from "../../core/data_formats/parsing/quest/Quest";
|
||||
import { Property } from "../../core/observable/property/Property";
|
||||
import { property } from "../../core/observable";
|
||||
import { WritableProperty } from "../../core/observable/property/WritableProperty";
|
||||
import { SectionModel } from "./SectionModel";
|
||||
import { Euler, Quaternion, Vector3 } from "three";
|
||||
import { floor_mod } from "../../core/math";
|
||||
import { defined, require_integer } from "../../core/util";
|
||||
import { euler_from_quat } from "./euler";
|
||||
import { euler, euler_from_quat } from "./euler";
|
||||
import { vec3_to_threejs } from "../../core/rendering/conversion";
|
||||
|
||||
// These quaternions are used as temporary variables to avoid memory allocation.
|
||||
const q1 = new Quaternion();
|
||||
const q2 = new Quaternion();
|
||||
|
||||
export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
|
||||
export abstract class QuestEntityModel<
|
||||
Type extends EntityType = EntityType,
|
||||
Entity extends QuestEntity<Type> = QuestEntity<Type>
|
||||
> {
|
||||
private readonly _section_id: WritableProperty<number>;
|
||||
private readonly _section: WritableProperty<SectionModel | undefined> = property(undefined);
|
||||
private readonly _position: WritableProperty<Vector3>;
|
||||
@ -20,9 +23,19 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
|
||||
private readonly _rotation: WritableProperty<Euler>;
|
||||
private readonly _world_rotation: WritableProperty<Euler>;
|
||||
|
||||
readonly type: Type;
|
||||
/**
|
||||
* Many modifications done to the underlying entity directly will not be reflected in this
|
||||
* model's properties.
|
||||
*/
|
||||
readonly entity: Entity;
|
||||
|
||||
readonly area_id: number;
|
||||
get type(): Type {
|
||||
return this.entity.type;
|
||||
}
|
||||
|
||||
get area_id(): number {
|
||||
return this.entity.area_id;
|
||||
}
|
||||
|
||||
readonly section_id: Property<number>;
|
||||
|
||||
@ -42,32 +55,25 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
|
||||
|
||||
readonly world_rotation: Property<Euler>;
|
||||
|
||||
protected constructor(
|
||||
type: Type,
|
||||
area_id: number,
|
||||
section_id: number,
|
||||
position: Vector3,
|
||||
rotation: Euler,
|
||||
) {
|
||||
defined(type, "type");
|
||||
require_integer(area_id, "area_id");
|
||||
require_integer(section_id, "section_id");
|
||||
defined(position, "position");
|
||||
defined(rotation, "rotation");
|
||||
protected constructor(entity: Entity) {
|
||||
this.entity = entity;
|
||||
|
||||
this.type = type;
|
||||
this.area_id = area_id;
|
||||
this.section = this._section;
|
||||
|
||||
this._section_id = property(section_id);
|
||||
this._section_id = property(entity.section_id);
|
||||
this.section_id = this._section_id;
|
||||
|
||||
const position = vec3_to_threejs(entity.position);
|
||||
|
||||
this._position = property(position);
|
||||
this.position = this._position;
|
||||
|
||||
this._world_position = property(position);
|
||||
this.world_position = this._world_position;
|
||||
|
||||
const { x: rot_x, y: rot_y, z: rot_z } = entity.rotation;
|
||||
const rotation = euler(rot_x, rot_y, rot_z);
|
||||
|
||||
this._rotation = property(rotation);
|
||||
this.rotation = this._rotation;
|
||||
|
||||
@ -80,6 +86,8 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
|
||||
throw new Error(`Quest entities can't be moved across areas.`);
|
||||
}
|
||||
|
||||
this.entity.section_id = section.id;
|
||||
|
||||
this._section.val = section;
|
||||
this._section_id.val = section.id;
|
||||
|
||||
@ -90,18 +98,15 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
|
||||
}
|
||||
|
||||
set_position(pos: Vector3): this {
|
||||
this.entity.position = pos;
|
||||
|
||||
this._position.val = pos;
|
||||
|
||||
const section = this.section.val;
|
||||
|
||||
if (section) {
|
||||
this._world_position.val = pos
|
||||
.clone()
|
||||
.applyEuler(section.rotation)
|
||||
.add(section.position);
|
||||
} else {
|
||||
this._world_position.val = pos;
|
||||
}
|
||||
this._world_position.val = section
|
||||
? pos.clone().applyEuler(section.rotation).add(section.position)
|
||||
: pos;
|
||||
|
||||
return this;
|
||||
}
|
||||
@ -111,14 +116,12 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
|
||||
|
||||
const section = this.section.val;
|
||||
|
||||
if (section) {
|
||||
this._position.val = pos
|
||||
.clone()
|
||||
.sub(section.position)
|
||||
.applyEuler(section.inverse_rotation);
|
||||
} else {
|
||||
this._position.val = pos;
|
||||
}
|
||||
const rel_pos = section
|
||||
? pos.clone().sub(section.position).applyEuler(section.inverse_rotation)
|
||||
: pos;
|
||||
|
||||
this.entity.position = rel_pos;
|
||||
this._position.val = rel_pos;
|
||||
|
||||
return this;
|
||||
}
|
||||
@ -126,6 +129,8 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
|
||||
set_rotation(rot: Euler): this {
|
||||
floor_mod_euler(rot);
|
||||
|
||||
this.entity.rotation = rot;
|
||||
|
||||
this._rotation.val = rot;
|
||||
|
||||
const section = this.section.val;
|
||||
@ -148,16 +153,21 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
|
||||
|
||||
const section = this.section.val;
|
||||
|
||||
let rel_rot: Euler;
|
||||
|
||||
if (section) {
|
||||
q1.setFromEuler(rot);
|
||||
q2.setFromEuler(section.rotation);
|
||||
q2.inverse();
|
||||
|
||||
this._rotation.val = floor_mod_euler(euler_from_quat(q1.multiply(q2)));
|
||||
rel_rot = floor_mod_euler(euler_from_quat(q1.multiply(q2)));
|
||||
} else {
|
||||
this._rotation.val = rot;
|
||||
rel_rot = rot;
|
||||
}
|
||||
|
||||
this.entity.rotation = rel_rot;
|
||||
this._rotation.val = rel_rot;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import { AreaVariantModel } from "./AreaVariantModel";
|
||||
import { ListProperty } from "../../core/observable/property/list/ListProperty";
|
||||
import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty";
|
||||
import { QuestEntityModel } from "./QuestEntityModel";
|
||||
import { entity_type_to_string } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { entity_type_to_string } from "../../core/data_formats/parsing/quest/Quest";
|
||||
import { QuestEventDagModel } from "./QuestEventDagModel";
|
||||
import { assert, defined, require_array } from "../../core/util";
|
||||
import { AreaStore } from "../stores/AreaStore";
|
||||
|
@ -1,109 +1,30 @@
|
||||
import { QuestEntityModel } from "./QuestEntityModel";
|
||||
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
|
||||
import { Euler, Vector3 } from "three";
|
||||
import { WritableProperty } from "../../core/observable/property/WritableProperty";
|
||||
import { Property } from "../../core/observable/property/Property";
|
||||
import {
|
||||
assert,
|
||||
defined,
|
||||
require_finite,
|
||||
require_integer,
|
||||
require_non_negative_integer,
|
||||
} from "../../core/util";
|
||||
import { defined } from "../../core/util";
|
||||
import { property } from "../../core/observable";
|
||||
import { WaveModel } from "./WaveModel";
|
||||
import { QuestNpc } from "../../core/data_formats/parsing/quest/Quest";
|
||||
|
||||
export class QuestNpcModel extends QuestEntityModel<NpcType> {
|
||||
export class QuestNpcModel extends QuestEntityModel<NpcType, QuestNpc> {
|
||||
private readonly _wave: WritableProperty<WaveModel | undefined>;
|
||||
private readonly _pso_wave2: number;
|
||||
|
||||
readonly pso_type_id: number;
|
||||
readonly npc_id: number;
|
||||
readonly wave: Property<WaveModel | undefined>;
|
||||
readonly pso_wave2: Property<number>;
|
||||
|
||||
readonly script_label: number;
|
||||
readonly pso_roaming: number;
|
||||
readonly scale: Vector3;
|
||||
/**
|
||||
* Data of which the purpose hasn't been discovered yet.
|
||||
*/
|
||||
readonly unknown: readonly number[][];
|
||||
constructor(npc: QuestNpc, wave?: WaveModel) {
|
||||
defined(npc, "npc");
|
||||
|
||||
constructor(
|
||||
type: NpcType,
|
||||
pso_type_id: number,
|
||||
npc_id: number,
|
||||
wave: WaveModel | undefined,
|
||||
pso_wave2: number,
|
||||
script_label: number,
|
||||
pso_roaming: number,
|
||||
area_id: number,
|
||||
section_id: number,
|
||||
position: Vector3,
|
||||
rotation: Euler,
|
||||
scale: Vector3,
|
||||
unknown: readonly number[][],
|
||||
) {
|
||||
require_integer(pso_type_id, "pso_type_id");
|
||||
require_finite(npc_id, "npc_id");
|
||||
require_non_negative_integer(pso_wave2, "pso_wave2");
|
||||
require_integer(script_label, "script_label");
|
||||
require_integer(pso_roaming, "pso_roaming");
|
||||
defined(scale, "scale");
|
||||
defined(unknown, "unknown");
|
||||
assert(unknown.length === 2, () => `unknown should be of length 2, was ${unknown.length}.`);
|
||||
assert(
|
||||
unknown[0].length === 10,
|
||||
() => `unknown[0] should be of length 10, was ${unknown[0].length}`,
|
||||
);
|
||||
assert(
|
||||
unknown[1].length === 4,
|
||||
() => `unknown[1] should be of length 4, was ${unknown[1].length}`,
|
||||
);
|
||||
|
||||
super(type, area_id, section_id, position, rotation);
|
||||
|
||||
this.pso_type_id = pso_type_id;
|
||||
this.npc_id = npc_id;
|
||||
super(npc);
|
||||
|
||||
this._wave = property(wave);
|
||||
this.wave = this._wave;
|
||||
|
||||
// pso_wave2 should stay as it is originally until wave or wave.id changes.
|
||||
const orig_wave = {
|
||||
id: wave?.id?.val,
|
||||
section_id: wave?.section_id?.val,
|
||||
area_id: wave?.area_id?.val,
|
||||
};
|
||||
this._pso_wave2 = pso_wave2;
|
||||
this.pso_wave2 = this.wave.flat_map(wave => {
|
||||
if (wave == undefined) {
|
||||
return property(orig_wave.id == undefined ? this._pso_wave2 : 0);
|
||||
} else if (
|
||||
orig_wave.id === wave.id.val &&
|
||||
orig_wave.section_id === wave.section_id.val &&
|
||||
orig_wave.area_id === wave.area_id.val
|
||||
) {
|
||||
return wave.id.map(wave_id => {
|
||||
if (orig_wave.id === wave_id) {
|
||||
return this._pso_wave2;
|
||||
} else {
|
||||
return wave_id;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return wave.id;
|
||||
}
|
||||
});
|
||||
|
||||
this.script_label = script_label;
|
||||
this.pso_roaming = pso_roaming;
|
||||
this.unknown = unknown;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
set_wave(wave?: WaveModel): this {
|
||||
const wave_id = wave?.id?.val ?? 0;
|
||||
this.entity.wave = wave_id;
|
||||
this.entity.wave_2 = wave_id;
|
||||
this._wave.val = wave;
|
||||
return this;
|
||||
}
|
||||
|
@ -1,32 +1,12 @@
|
||||
import { QuestEntityModel } from "./QuestEntityModel";
|
||||
import { ObjectType } from "../../core/data_formats/parsing/quest/object_types";
|
||||
import { Euler, Vector3 } from "three";
|
||||
import { QuestObject } from "../../core/data_formats/parsing/quest/Quest";
|
||||
import { defined } from "../../core/util";
|
||||
|
||||
export class QuestObjectModel extends QuestEntityModel<ObjectType> {
|
||||
readonly id: number;
|
||||
readonly group_id: number;
|
||||
readonly properties: Map<string, number>;
|
||||
/**
|
||||
* Data of which the purpose hasn't been discovered yet.
|
||||
*/
|
||||
readonly unknown: readonly number[][];
|
||||
export class QuestObjectModel extends QuestEntityModel<ObjectType, QuestObject> {
|
||||
constructor(object: QuestObject) {
|
||||
defined(object, "object");
|
||||
|
||||
constructor(
|
||||
type: ObjectType,
|
||||
id: number,
|
||||
group_id: number,
|
||||
area_id: number,
|
||||
section_id: number,
|
||||
position: Vector3,
|
||||
rotation: Euler,
|
||||
properties: Map<string, number>,
|
||||
unknown: readonly number[][],
|
||||
) {
|
||||
super(type, area_id, section_id, position, rotation);
|
||||
|
||||
this.id = id;
|
||||
this.group_id = group_id;
|
||||
this.properties = properties;
|
||||
this.unknown = unknown;
|
||||
super(object);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
Scene,
|
||||
Vector3,
|
||||
} from "three";
|
||||
import { EntityType } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { EntityType } from "../../core/data_formats/parsing/quest/Quest";
|
||||
import { create_entity_type_mesh } from "./conversion/entities";
|
||||
import { sequential } from "../../core/sequential";
|
||||
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
} 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";
|
||||
import { entity_type_to_string } from "../../core/data_formats/parsing/quest/Quest";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import { AreaVariantModel } from "../model/AreaVariantModel";
|
||||
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
|
||||
|
@ -7,8 +7,12 @@ import { AreaUserData } from "./conversion/areas";
|
||||
import { SectionModel } from "../model/SectionModel";
|
||||
import { Disposable } from "../../core/observable/Disposable";
|
||||
import { Disposer } from "../../core/observable/Disposer";
|
||||
import { EntityType, is_npc_type } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { npc_data } from "../../core/data_formats/parsing/quest/npc_types";
|
||||
import {
|
||||
EntityType,
|
||||
is_npc_type,
|
||||
QuestNpc,
|
||||
QuestObject,
|
||||
} from "../../core/data_formats/parsing/quest/Quest";
|
||||
import {
|
||||
add_entity_dnd_listener,
|
||||
EntityDragEvent,
|
||||
@ -18,7 +22,6 @@ import { QuestObjectModel } from "../model/QuestObjectModel";
|
||||
import { AreaModel } from "../model/AreaModel";
|
||||
import { QuestModel } from "../model/QuestModel";
|
||||
import { QuestEditorStore } from "../stores/QuestEditorStore";
|
||||
import { euler } from "../model/euler";
|
||||
import { CreateEntityAction } from "../actions/CreateEntityAction";
|
||||
import { RotateEntityAction } from "../actions/RotateEntityAction";
|
||||
import { RemoveEntityAction } from "../actions/RemoveEntityAction";
|
||||
@ -637,44 +640,14 @@ class CreationState implements State {
|
||||
}
|
||||
|
||||
if (is_npc_type(evt.entity_type)) {
|
||||
const data = npc_data(evt.entity_type);
|
||||
const wave = quest_editor_store.selected_wave.val;
|
||||
|
||||
this.entity = new QuestNpcModel(
|
||||
evt.entity_type,
|
||||
data.pso_type_id!,
|
||||
0,
|
||||
quest_editor_store.selected_wave.val,
|
||||
0,
|
||||
0,
|
||||
data.pso_roaming!,
|
||||
area.id,
|
||||
0,
|
||||
new Vector3(0, 0, 0),
|
||||
euler(0, 0, 0),
|
||||
new Vector3(1, 1, 1),
|
||||
// TODO: do the following values make sense?
|
||||
[
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
],
|
||||
QuestNpc.create(evt.entity_type, area.id, wave?.id.val ?? 0),
|
||||
wave,
|
||||
);
|
||||
} else {
|
||||
this.entity = new QuestObjectModel(
|
||||
evt.entity_type,
|
||||
0,
|
||||
0,
|
||||
area.id,
|
||||
0,
|
||||
new Vector3(0, 0, 0),
|
||||
euler(0, 0, 0),
|
||||
// TODO: which default properties?
|
||||
new Map(),
|
||||
// TODO: do the following values make sense?
|
||||
[
|
||||
[0, 0, 0, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
);
|
||||
this.entity = new QuestObjectModel(QuestObject.create(evt.entity_type, area.id));
|
||||
}
|
||||
|
||||
translate_entity_horizontally(
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
entity_type_to_string,
|
||||
EntityType,
|
||||
is_npc_type,
|
||||
} from "../../../core/data_formats/parsing/quest/entities";
|
||||
} from "../../../core/data_formats/parsing/quest/Quest";
|
||||
|
||||
export enum ColorType {
|
||||
Normal,
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { Quest } from "../../core/data_formats/parsing/quest";
|
||||
import { QuestModel } from "../model/QuestModel";
|
||||
import { QuestObjectModel } from "../model/QuestObjectModel";
|
||||
import { vec3_to_threejs } from "../../core/rendering/conversion";
|
||||
import { QuestNpcModel } from "../model/QuestNpcModel";
|
||||
import { QuestEventModel } from "../model/QuestEventModel";
|
||||
import {
|
||||
@ -16,11 +14,10 @@ import {
|
||||
QuestEventActionUnlockModel,
|
||||
} from "../model/QuestEventActionModel";
|
||||
import { QuestEventDagModel } from "../model/QuestEventDagModel";
|
||||
import { QuestEvent, QuestNpc } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { Quest, QuestEvent, QuestNpc } from "../../core/data_formats/parsing/quest/Quest";
|
||||
import { clone_segment } from "../../core/data_formats/asm/instructions";
|
||||
import { AreaStore } from "./AreaStore";
|
||||
import { LogManager } from "../../core/Logger";
|
||||
import { euler } from "../model/euler";
|
||||
import { WaveModel } from "../model/WaveModel";
|
||||
|
||||
const logger = LogManager.get("quest_editor/stores/model_conversion");
|
||||
@ -37,20 +34,7 @@ export function convert_quest_to_model(area_store: AreaStore, quest: Quest): Que
|
||||
quest.long_description,
|
||||
quest.episode,
|
||||
quest.map_designations,
|
||||
quest.objects.map(
|
||||
obj =>
|
||||
new QuestObjectModel(
|
||||
obj.type,
|
||||
obj.id,
|
||||
obj.group_id,
|
||||
obj.area_id,
|
||||
obj.section_id,
|
||||
vec3_to_threejs(obj.position),
|
||||
euler(obj.rotation.x, obj.rotation.y, obj.rotation.z),
|
||||
obj.properties,
|
||||
obj.unknown,
|
||||
),
|
||||
),
|
||||
quest.objects.map(obj => new QuestObjectModel(obj)),
|
||||
quest.npcs.map(npc => convert_npc_to_model(wave_cache, npc)),
|
||||
build_event_dags(wave_cache, quest.events),
|
||||
quest.dat_unknowns,
|
||||
@ -63,21 +47,7 @@ function convert_npc_to_model(wave_cache: Map<string, WaveModel>, npc: QuestNpc)
|
||||
const wave =
|
||||
npc.wave === 0 ? undefined : get_wave(wave_cache, npc.area_id, npc.section_id, npc.wave);
|
||||
|
||||
return new QuestNpcModel(
|
||||
npc.type,
|
||||
npc.pso_type_id,
|
||||
npc.npc_id,
|
||||
wave,
|
||||
npc.pso_wave2,
|
||||
npc.script_label,
|
||||
npc.pso_roaming,
|
||||
npc.area_id,
|
||||
npc.section_id,
|
||||
vec3_to_threejs(npc.position),
|
||||
euler(npc.rotation.x, npc.rotation.y, npc.rotation.z),
|
||||
vec3_to_threejs(npc.scale),
|
||||
npc.unknown,
|
||||
);
|
||||
return new QuestNpcModel(npc, wave);
|
||||
}
|
||||
|
||||
function get_wave(
|
||||
@ -227,32 +197,8 @@ export function convert_quest_from_model(quest: QuestModel): Quest {
|
||||
short_description: quest.short_description.val,
|
||||
long_description: quest.long_description.val,
|
||||
episode: quest.episode,
|
||||
objects: quest.objects.val.map(obj => ({
|
||||
type: obj.type,
|
||||
area_id: obj.area_id,
|
||||
section_id: obj.section_id.val,
|
||||
position: obj.position.val.clone(),
|
||||
rotation: obj.rotation.val.clone(),
|
||||
unknown: obj.unknown,
|
||||
id: obj.id,
|
||||
group_id: obj.group_id,
|
||||
properties: new Map(obj.properties),
|
||||
})),
|
||||
npcs: quest.npcs.val.map(npc => ({
|
||||
type: npc.type,
|
||||
area_id: npc.area_id,
|
||||
section_id: npc.section_id.val,
|
||||
wave: npc.wave.val?.id.val ?? 0,
|
||||
pso_wave2: npc.pso_wave2.val,
|
||||
position: npc.position.val.clone(),
|
||||
rotation: npc.rotation.val.clone(),
|
||||
scale: npc.scale.clone(),
|
||||
unknown: npc.unknown,
|
||||
pso_type_id: npc.pso_type_id,
|
||||
npc_id: npc.npc_id,
|
||||
script_label: npc.script_label,
|
||||
pso_roaming: npc.pso_roaming,
|
||||
})),
|
||||
objects: quest.objects.val.map(obj => obj.entity),
|
||||
npcs: quest.npcs.val.map(npc => npc.entity),
|
||||
events: convert_quest_events_from_model(quest.event_dags),
|
||||
dat_unknowns: quest.dat_unknowns.map(unk => ({ ...unk })),
|
||||
object_code: quest.object_code.map(seg => clone_segment(seg)),
|
||||
|
@ -1,942 +0,0 @@
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import { QuestModel } from "../model/QuestModel";
|
||||
import { ObjectType } from "../../core/data_formats/parsing/quest/object_types";
|
||||
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
|
||||
import { QuestObjectModel } from "../model/QuestObjectModel";
|
||||
import { QuestNpcModel } from "../model/QuestNpcModel";
|
||||
import { Euler, Vector3 } from "three";
|
||||
import { QuestEventDagModel } from "../model/QuestEventDagModel";
|
||||
import { AreaStore } from "./AreaStore";
|
||||
import { assemble } from "../scripting/assembly";
|
||||
import { Segment } from "../../core/data_formats/asm/instructions";
|
||||
import { euler } from "../model/euler";
|
||||
|
||||
export function create_new_quest(area_store: AreaStore, episode: Episode): QuestModel {
|
||||
if (episode === Episode.II) throw new Error("Episode II not yet supported.");
|
||||
if (episode === Episode.IV) throw new Error("Episode IV not yet supported.");
|
||||
|
||||
return new QuestModel(
|
||||
area_store,
|
||||
0,
|
||||
0,
|
||||
"Untitled",
|
||||
"Created with phantasmal.world.",
|
||||
"Created with phantasmal.world.",
|
||||
episode,
|
||||
new Map().set(0, 0),
|
||||
create_default_objects(),
|
||||
create_default_npcs(),
|
||||
create_default_event_dags(),
|
||||
[],
|
||||
create_default_object_code(),
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
function create_default_objects(): QuestObjectModel[] {
|
||||
return [
|
||||
new QuestObjectModel(
|
||||
ObjectType.MenuActivation,
|
||||
16384,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-16.313568115234375, 3, -579.5118408203125),
|
||||
euler(0.0009587526218325454, 0, 0),
|
||||
new Map([
|
||||
["property_0", 1],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 1],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365279104],
|
||||
]),
|
||||
[
|
||||
[2, 0, 0, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.MenuActivation,
|
||||
16385,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-393.07318115234375, 10, -12.964752197265625),
|
||||
euler(0, 0, 0),
|
||||
new Map([
|
||||
["property_0", 9.183549615799121e-41],
|
||||
["property_1", 1.0000011920928955],
|
||||
["property_2", 1],
|
||||
["property_3", 0],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365279264],
|
||||
]),
|
||||
[
|
||||
[2, 0, 1, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.MenuActivation,
|
||||
16386,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-458.60699462890625, 10, -51.270660400390625),
|
||||
euler(0, 0, 0),
|
||||
new Map([
|
||||
["property_0", 1],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 2],
|
||||
["property_4", 65536],
|
||||
["property_5", 10],
|
||||
["property_6", 2365279424],
|
||||
]),
|
||||
[
|
||||
[2, 0, 2, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.MenuActivation,
|
||||
16387,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-430.19696044921875, 10, -24.490447998046875),
|
||||
euler(0, 0, 0),
|
||||
new Map([
|
||||
["property_0", 1],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 3],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365279584],
|
||||
]),
|
||||
[
|
||||
[2, 0, 3, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.PlayerSet,
|
||||
16388,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(0.995330810546875, 0, -37.0010986328125),
|
||||
euler(0, 4.712460886831327, 0),
|
||||
new Map([
|
||||
["property_0", 0],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 0],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365279744],
|
||||
]),
|
||||
[
|
||||
[2, 0, 4, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.PlayerSet,
|
||||
16389,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(3.0009307861328125, 0, -23.99688720703125),
|
||||
euler(0, 4.859725289544806, 0),
|
||||
new Map([
|
||||
["property_0", 1.000000238418579],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 0],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365279904],
|
||||
]),
|
||||
[
|
||||
[2, 0, 5, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.PlayerSet,
|
||||
16390,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(2.0015106201171875, 0, -50.00386047363281),
|
||||
euler(0, 4.565196484117848, 0),
|
||||
new Map([
|
||||
["property_0", 2.000002384185791],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 0],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365280064],
|
||||
]),
|
||||
[
|
||||
[2, 0, 6, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.PlayerSet,
|
||||
16391,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(4.9973907470703125, 0, -61.99664306640625),
|
||||
euler(0, 4.368843947166543, 0),
|
||||
new Map([
|
||||
["property_0", 3.0000007152557373],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 65536],
|
||||
["property_4", 10],
|
||||
["property_5", 0],
|
||||
["property_6", 2365280224],
|
||||
]),
|
||||
[
|
||||
[2, 0, 7, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.MainRagolTeleporter,
|
||||
18264,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(132.00314331054688, 1.000000238418579, -265.002197265625),
|
||||
euler(0, 0.49088134237826325, 0),
|
||||
new Map([
|
||||
["property_0", 1.000000238418579],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 1],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365227216],
|
||||
]),
|
||||
[
|
||||
[0, 0, 87, 7, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.PrincipalWarp,
|
||||
16393,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-228, 0, -2020.99951171875),
|
||||
euler(0, 2.9452880542695796, 0),
|
||||
new Map([
|
||||
["property_0", -10.000004768371582],
|
||||
["property_1", 0],
|
||||
["property_2", -30.000030517578125],
|
||||
["property_3", 0],
|
||||
["property_4", 65536],
|
||||
["property_5", 65536],
|
||||
["property_6", 2365280688],
|
||||
]),
|
||||
[
|
||||
[2, 0, 9, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.MenuActivation,
|
||||
16394,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-41.000030517578125, 0, 42.37322998046875),
|
||||
euler(0, 0, 0),
|
||||
new Map([
|
||||
["property_0", 1],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 4],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365280992],
|
||||
]),
|
||||
[
|
||||
[2, 0, 10, 0, 0, 0],
|
||||
[1, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.MenuActivation,
|
||||
16395,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-479.21673583984375, 8.781256675720215, -322.465576171875),
|
||||
euler(6.28328118244177, 0.0009587526218325454, 0),
|
||||
new Map([
|
||||
["property_0", 1],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 5],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365281152],
|
||||
]),
|
||||
[
|
||||
[2, 0, 11, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.PrincipalWarp,
|
||||
16396,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-228, 0, -351.0015869140625),
|
||||
euler(0, 0, 0),
|
||||
new Map([
|
||||
["property_0", 10.000006675720215],
|
||||
["property_1", 0],
|
||||
["property_2", -1760.0010986328125],
|
||||
["property_3", 32768],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365281312],
|
||||
]),
|
||||
[
|
||||
[2, 0, 12, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.TelepipeLocation,
|
||||
16397,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-561.88232421875, 0, -406.8829345703125),
|
||||
euler(0, 0, 0),
|
||||
new Map([
|
||||
["property_0", 1],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 0],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365281616],
|
||||
]),
|
||||
[
|
||||
[2, 0, 13, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.TelepipeLocation,
|
||||
16398,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-547.8557739257812, 0, -444.8822326660156),
|
||||
euler(0, 0, 0),
|
||||
new Map([
|
||||
["property_0", 1],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 1],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365281808],
|
||||
]),
|
||||
[
|
||||
[2, 0, 14, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.TelepipeLocation,
|
||||
16399,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-486.441650390625, 0, -497.4501647949219),
|
||||
euler(0, 0, 0),
|
||||
new Map([
|
||||
["property_0", 9.183549615799121e-41],
|
||||
["property_1", 1.0000011920928955],
|
||||
["property_2", 1],
|
||||
["property_3", 3],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365282000],
|
||||
]),
|
||||
[
|
||||
[2, 0, 15, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.TelepipeLocation,
|
||||
16400,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-522.4052734375, 0, -474.1882629394531),
|
||||
euler(0, 0, 0),
|
||||
new Map([
|
||||
["property_0", 1],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 2],
|
||||
["property_4", 65536],
|
||||
["property_5", 10],
|
||||
["property_6", 2365282192],
|
||||
]),
|
||||
[
|
||||
[2, 0, 16, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.MedicalCenterDoor,
|
||||
16401,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-34.49853515625, 0, -384.4951171875),
|
||||
euler(0, 5.497871034636549, 0),
|
||||
new Map([
|
||||
["property_0", 3.0000007152557373],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 1],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365282384],
|
||||
]),
|
||||
[
|
||||
[2, 0, 17, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.ShopDoor,
|
||||
16402,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-393.0031433105469, 0, -143.49981689453125),
|
||||
euler(0, 3.141640591220885, 0),
|
||||
new Map([
|
||||
["property_0", 3.0000007152557373],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 1],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365282640],
|
||||
]),
|
||||
[
|
||||
[2, 0, 18, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.MenuActivation,
|
||||
16403,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-355.17462158203125, 0, -43.15193176269531),
|
||||
euler(0, 0, 0),
|
||||
new Map([
|
||||
["property_0", 1.000000238418579],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 6],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365282896],
|
||||
]),
|
||||
[
|
||||
[2, 0, 19, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.HuntersGuildDoor,
|
||||
16404,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-43.00239562988281, 0, -118.00120544433594),
|
||||
euler(0, 3.141640591220885, 0),
|
||||
new Map([
|
||||
["property_0", 3.0000007152557373],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 1],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365283056],
|
||||
]),
|
||||
[
|
||||
[2, 0, 20, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.TeleporterDoor,
|
||||
16405,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(26.000823974609375, 0, -265.99810791015625),
|
||||
euler(0, 3.141640591220885, 0),
|
||||
new Map([
|
||||
["property_0", 3.0000007152557373],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 1],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365283312],
|
||||
]),
|
||||
[
|
||||
[2, 0, 21, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.PlayerSet,
|
||||
16406,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(57.81005859375, 0, -268.5472412109375),
|
||||
euler(0, 4.712460886831327, 0),
|
||||
new Map([
|
||||
["property_0", 0],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 1],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365283568],
|
||||
]),
|
||||
[
|
||||
[2, 0, 22, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.PlayerSet,
|
||||
16407,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(66.769287109375, 0, -252.3748779296875),
|
||||
euler(0, 4.712460886831327, 0),
|
||||
new Map([
|
||||
["property_0", 1.000000238418579],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 1],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365283728],
|
||||
]),
|
||||
[
|
||||
[2, 0, 23, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.PlayerSet,
|
||||
16408,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(67.36819458007812, 0, -284.9297180175781),
|
||||
euler(0, 4.712460886831327, 0),
|
||||
new Map([
|
||||
["property_0", 2.000000476837158],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 1],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365283888],
|
||||
]),
|
||||
[
|
||||
[2, 0, 24, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
new QuestObjectModel(
|
||||
ObjectType.PlayerSet,
|
||||
16409,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(77.10488891601562, 0, -269.2830505371094),
|
||||
euler(0, 4.712460886831327, 0),
|
||||
new Map([
|
||||
["property_0", 3.0000007152557373],
|
||||
["property_1", 1],
|
||||
["property_2", 1],
|
||||
["property_3", 1],
|
||||
["property_4", 0],
|
||||
["property_5", 0],
|
||||
["property_6", 2365284048],
|
||||
]),
|
||||
[
|
||||
[2, 0, 25, 0, 0, 0],
|
||||
[0, 0],
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function create_default_npcs(): QuestNpcModel[] {
|
||||
return [
|
||||
new QuestNpcModel(
|
||||
NpcType.GuildLady,
|
||||
29,
|
||||
1011.0010986328125,
|
||||
undefined,
|
||||
0,
|
||||
850,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
new Vector3(-49.0010986328125, 0, 50.996429443359375),
|
||||
euler(0, 2.3562304434156633, 0),
|
||||
new Vector3(0, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 86, 0, 0, 0, 0, 23, 87],
|
||||
[128, 238, 223, 176],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.FemaleFat,
|
||||
4,
|
||||
1016.0010986328125,
|
||||
undefined,
|
||||
0,
|
||||
100,
|
||||
1,
|
||||
0,
|
||||
20,
|
||||
new Vector3(167.99769592285156, 0, 83.99686431884766),
|
||||
euler(0, 3.927050739026106, 0),
|
||||
new Vector3(24.000009536743164, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 88, 0, 0, 0, 0, 23, 89],
|
||||
[128, 238, 232, 48],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.MaleDwarf,
|
||||
10,
|
||||
1015.0010986328125,
|
||||
undefined,
|
||||
0,
|
||||
90,
|
||||
1,
|
||||
0,
|
||||
20,
|
||||
new Vector3(156.0028839111328, 0, -49.99967575073242),
|
||||
euler(0, 5.497871034636549, 0),
|
||||
new Vector3(30.000009536743164, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 89, 0, 0, 0, 0, 23, 90],
|
||||
[128, 238, 236, 176],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.RedSoldier,
|
||||
26,
|
||||
1020.0010986328125,
|
||||
undefined,
|
||||
0,
|
||||
130,
|
||||
0,
|
||||
0,
|
||||
20,
|
||||
new Vector3(237.9988250732422, 0, -14.0001220703125),
|
||||
euler(0, 5.497871034636549, 0),
|
||||
new Vector3(0, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 90, 0, 0, 0, 0, 23, 91],
|
||||
[128, 238, 241, 48],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.BlueSoldier,
|
||||
25,
|
||||
1019.0010986328125,
|
||||
undefined,
|
||||
0,
|
||||
120,
|
||||
0,
|
||||
0,
|
||||
20,
|
||||
new Vector3(238.00379943847656, 0, 63.00413513183594),
|
||||
euler(0, 3.927050739026106, 0),
|
||||
new Vector3(0, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 91, 0, 0, 0, 0, 23, 92],
|
||||
[128, 238, 245, 176],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.FemaleMacho,
|
||||
5,
|
||||
1014.0010986328125,
|
||||
undefined,
|
||||
0,
|
||||
80,
|
||||
1,
|
||||
0,
|
||||
20,
|
||||
new Vector3(-2.001882553100586, 0, 35.0036506652832),
|
||||
euler(0, 3.141640591220885, 0),
|
||||
new Vector3(26.000009536743164, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 92, 0, 0, 0, 0, 23, 93],
|
||||
[128, 238, 250, 48],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.Scientist,
|
||||
30,
|
||||
1013.0010986328125,
|
||||
undefined,
|
||||
0,
|
||||
70,
|
||||
1,
|
||||
0,
|
||||
20,
|
||||
new Vector3(-147.0000457763672, 0, -7.996537208557129),
|
||||
euler(0, 2.577127047485882, 0),
|
||||
new Vector3(30.000009536743164, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 93, 0, 0, 0, 0, 23, 94],
|
||||
[128, 238, 254, 176],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.MaleOld,
|
||||
13,
|
||||
1012.0010986328125,
|
||||
undefined,
|
||||
0,
|
||||
60,
|
||||
1,
|
||||
0,
|
||||
20,
|
||||
new Vector3(-219.99710083007812, 0, -100.0008316040039),
|
||||
euler(0, 0, 0),
|
||||
new Vector3(30.000011444091797, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 94, 0, 0, 0, 0, 23, 95],
|
||||
[128, 239, 3, 48],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.GuildLady,
|
||||
29,
|
||||
1010.0010986328125,
|
||||
undefined,
|
||||
0,
|
||||
840,
|
||||
0,
|
||||
0,
|
||||
20,
|
||||
new Vector3(-262.5099792480469, 0, -24.53999900817871),
|
||||
euler(0, 1.963525369513053, 0),
|
||||
new Vector3(0, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 95, 0, 0, 0, 0, 23, 106],
|
||||
[128, 239, 100, 192],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.Tekker,
|
||||
28,
|
||||
1009,
|
||||
undefined,
|
||||
0,
|
||||
830,
|
||||
0,
|
||||
0,
|
||||
30,
|
||||
new Vector3(-43.70983123779297, 2.5999999046325684, -52.78248596191406),
|
||||
euler(0, 0.7854101478052212, 0),
|
||||
new Vector3(0, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 97, 0, 0, 0, 0, 23, 98],
|
||||
[128, 239, 16, 176],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.MaleMacho,
|
||||
12,
|
||||
1006,
|
||||
undefined,
|
||||
0,
|
||||
800,
|
||||
0,
|
||||
0,
|
||||
30,
|
||||
new Vector3(0.33990478515625, 2.5999999046325684, -84.71995544433594),
|
||||
euler(0, 0, 0),
|
||||
new Vector3(0, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 98, 0, 0, 0, 0, 23, 99],
|
||||
[128, 239, 21, 48],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.FemaleMacho,
|
||||
5,
|
||||
1008,
|
||||
undefined,
|
||||
0,
|
||||
820,
|
||||
0,
|
||||
0,
|
||||
30,
|
||||
new Vector3(43.87113952636719, 2.5999996662139893, -74.80299377441406),
|
||||
new Euler(0, -0.5645135437350027, 0, "ZXY"),
|
||||
new Vector3(0, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 99, 0, 0, 0, 0, 23, 100],
|
||||
[128, 239, 25, 176],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.MaleFat,
|
||||
11,
|
||||
1007.0010986328125,
|
||||
undefined,
|
||||
0,
|
||||
810,
|
||||
0,
|
||||
0,
|
||||
30,
|
||||
new Vector3(75.88380432128906, 2.5999996662139893, -42.69328308105469),
|
||||
new Euler(0, -1.0308508189943528, 0, "ZXY"),
|
||||
new Vector3(0, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 100, 0, 0, 0, 0, 23, 101],
|
||||
[128, 239, 30, 48],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.FemaleTall,
|
||||
7,
|
||||
1021.0010986328125,
|
||||
undefined,
|
||||
0,
|
||||
140,
|
||||
1,
|
||||
0,
|
||||
30,
|
||||
new Vector3(16.003997802734375, 0, 5.995697021484375),
|
||||
new Euler(0, -1.1781152217078317, 0, "ZXY"),
|
||||
new Vector3(22.000009536743164, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 101, 0, 0, 0, 0, 23, 102],
|
||||
[128, 239, 34, 176],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.Nurse,
|
||||
31,
|
||||
1017,
|
||||
undefined,
|
||||
0,
|
||||
860,
|
||||
0,
|
||||
0,
|
||||
40,
|
||||
new Vector3(0.3097381591796875, 3, -105.3865966796875),
|
||||
euler(0, 0, 0),
|
||||
new Vector3(0, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 102, 0, 0, 0, 0, 23, 103],
|
||||
[128, 239, 39, 48],
|
||||
],
|
||||
),
|
||||
new QuestNpcModel(
|
||||
NpcType.Nurse,
|
||||
31,
|
||||
1018.0010986328125,
|
||||
undefined,
|
||||
0,
|
||||
110,
|
||||
1,
|
||||
0,
|
||||
40,
|
||||
new Vector3(53.499176025390625, 0, -26.496688842773438),
|
||||
euler(0, 5.497871034636549, 0),
|
||||
new Vector3(18.000009536743164, 0, 0),
|
||||
[
|
||||
[0, 0, 7, 103, 0, 0, 0, 0, 23, 104],
|
||||
[128, 239, 43, 176],
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function create_default_event_dags(): Map<number, QuestEventDagModel> {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
function create_default_object_code(): Segment[] {
|
||||
return assemble(
|
||||
`.code
|
||||
|
||||
0:
|
||||
set_episode 0
|
||||
set_floor_handler 0, 150
|
||||
bb_map_designate 0, 0, 0, 0
|
||||
ret
|
||||
150:
|
||||
leti r60, 237
|
||||
leti r61, 0
|
||||
leti r62, 333
|
||||
leti r63, -15
|
||||
p_setpos 0, r60
|
||||
leti r60, 255
|
||||
leti r61, 0
|
||||
leti r62, 338
|
||||
leti r63, -43
|
||||
p_setpos 1, r60
|
||||
leti r60, 222
|
||||
leti r61, 0
|
||||
leti r62, 322
|
||||
leti r63, 25
|
||||
p_setpos 2, r60
|
||||
leti r60, 248
|
||||
leti r61, 0
|
||||
leti r62, 323
|
||||
leti r63, -20
|
||||
p_setpos 3, r60
|
||||
ret`.split("\n"),
|
||||
).object_code;
|
||||
}
|
@ -1,28 +1,29 @@
|
||||
import { Disposable } from "../../../../src/core/observable/Disposable";
|
||||
import { Disposer } from "../../../../src/core/observable/Disposer";
|
||||
import { is_promise } from "../../../../src/core/util";
|
||||
|
||||
export function with_disposable<D extends Disposable, T>(
|
||||
disposable: D,
|
||||
f: (disposable: D) => T | Promise<T>,
|
||||
f: (disposable: D) => T,
|
||||
): T {
|
||||
let is_promise = false;
|
||||
let return_promise = false;
|
||||
|
||||
try {
|
||||
const value = f(disposable);
|
||||
|
||||
if (value != undefined && "then" in value && "finally" in value) {
|
||||
is_promise = true;
|
||||
return (value.finally(() => disposable.dispose()) as any) as T;
|
||||
if (is_promise(value)) {
|
||||
return_promise = true;
|
||||
return (value.finally(() => disposable.dispose()) as unknown) as T;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
} finally {
|
||||
if (!is_promise) {
|
||||
if (!return_promise) {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function with_disposer<T>(f: (disposer: Disposer) => T | Promise<T>): T {
|
||||
export function with_disposer<T>(f: (disposer: Disposer) => T): T {
|
||||
return with_disposable(new Disposer(), f);
|
||||
}
|
||||
|
@ -1,6 +1,13 @@
|
||||
import * as fs from "fs";
|
||||
import { InstructionSegment, SegmentType } from "../../src/core/data_formats/asm/instructions";
|
||||
import { assemble } from "../../src/quest_editor/scripting/assembly";
|
||||
import { parse_qst_to_quest } from "../../src/core/data_formats/parsing/quest";
|
||||
import { BufferCursor } from "../../src/core/data_formats/block/cursor/BufferCursor";
|
||||
import { Endianness } from "../../src/core/data_formats/block/Endianness";
|
||||
import { Quest } from "../../src/core/data_formats/parsing/quest/Quest";
|
||||
import { QuestModel } from "../../src/quest_editor/model/QuestModel";
|
||||
import { AreaStore } from "../../src/quest_editor/stores/AreaStore";
|
||||
import { convert_quest_to_model } from "../../src/quest_editor/stores/model_conversion";
|
||||
|
||||
export async function timeout(millis: number): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
@ -59,6 +66,17 @@ export function get_qst_files(dir: string): [string, string][] {
|
||||
return files;
|
||||
}
|
||||
|
||||
export function load_default_quest_model(area_store: AreaStore): QuestModel {
|
||||
return convert_quest_to_model(
|
||||
area_store,
|
||||
load_qst_as_quest("assets/quests/defaults/default_ep_1.qst")!,
|
||||
);
|
||||
}
|
||||
|
||||
export function load_qst_as_quest(path: string): Quest | undefined {
|
||||
return parse_qst_to_quest(new BufferCursor(fs.readFileSync(path), Endianness.Little))?.quest;
|
||||
}
|
||||
|
||||
export function to_instructions(assembly: string, manual_stack?: boolean): InstructionSegment[] {
|
||||
const { object_code, warnings, errors } = assemble(assembly.split("\n"), manual_stack);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user