Separated most of quest domain models from quest parsing code and made quest parsing data structures more structured cloning-friendly. All areas for the quest's episode are now shown in the area selector with the number of entities in them in parentheses.

This commit is contained in:
Daan Vanden Bosch 2019-08-10 00:27:57 +02:00
parent 48f0d5157d
commit 2d551a1951
54 changed files with 2685 additions and 3472 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Editors
.idea
.vscode
# dependencies

View File

@ -2,5 +2,5 @@
"printWidth": 100,
"tabWidth": 4,
"singleQuote": false,
"trailingComma": "es5"
"trailingComma": "all"
}

View File

@ -31,6 +31,7 @@ Features that are in ***bold italics*** are planned and not yet implemented.
## Area Selection
- Dropdown menu to switch area
- Add new area
## Simple Quest Properties

View File

@ -1,4 +1,4 @@
import { Endianness } from "../..";
import { Endianness } from "../../Endianness";
import { Cursor } from "../../cursor/Cursor";
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";
import { WritableCursor } from "../../cursor/WritableCursor";

View File

@ -1,5 +1,5 @@
import { readFileSync } from "fs";
import { Endianness } from "../..";
import { Endianness } from "../../Endianness";
import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor";
import { BufferCursor } from "../../cursor/BufferCursor";
import { prs_compress } from "./compress";

View File

@ -1,4 +1,4 @@
import { Endianness } from "..";
import { Endianness } from "../Endianness";
import { Vec2, Vec3 } from "../vector";
import { Cursor } from "./Cursor";

View File

@ -1,4 +1,4 @@
import { Endianness } from "..";
import { Endianness } from "../Endianness";
import { AbstractWritableCursor } from "./AbstractWritableCursor";
import { WritableCursor } from "./WritableCursor";

View File

@ -1,4 +1,4 @@
import { Endianness } from "..";
import { Endianness } from "../Endianness";
import { AbstractCursor } from "./AbstractCursor";
import { Cursor } from "./Cursor";

View File

@ -1,4 +1,4 @@
import { Endianness } from "..";
import { Endianness } from "../Endianness";
import { enum_values } from "../../enums";
import { ResizableBuffer } from "../ResizableBuffer";
import { ArrayBufferCursor } from "./ArrayBufferCursor";

View File

@ -1,4 +1,4 @@
import { Endianness } from "..";
import { Endianness } from "../Endianness";
import { Vec3, Vec2 } from "../vector";
/**

View File

@ -1,4 +1,4 @@
import { Endianness } from "..";
import { Endianness } from "../Endianness";
import { ResizableBuffer } from "../ResizableBuffer";
import { ResizableBufferCursor } from "./ResizableBufferCursor";

View File

@ -1,4 +1,4 @@
import { Endianness } from "..";
import { Endianness } from "../Endianness";
import { ResizableBuffer } from "../ResizableBuffer";
import { AbstractWritableCursor } from "./AbstractWritableCursor";
import { WritableCursor } from "./WritableCursor";

View File

@ -1,4 +1,4 @@
import { Endianness } from "..";
import { Endianness } from "../Endianness";
import { enum_values } from "../../enums";
import { ResizableBuffer } from "../ResizableBuffer";
import { ArrayBufferCursor } from "./ArrayBufferCursor";

View File

@ -1,4 +1,4 @@
import { Endianness } from "..";
import { Endianness } from "../Endianness";
import { ArrayBufferCursor } from "../cursor/ArrayBufferCursor";
import { Cursor } from "../cursor/Cursor";

View File

@ -1,7 +1,7 @@
import { readFileSync } from "fs";
import { parse_area_collision_geometry } from "./area_collision_geometry";
import { BufferCursor } from "../cursor/BufferCursor";
import { Endianness } from "..";
import { Endianness } from "../Endianness";
test("parse_area_collision_geometry", () => {
const buf = readFileSync("test/resources/map_forest01c.rel");

View File

@ -1,7 +1,7 @@
import { parse_item_pmt } from "./itempmt";
import { readFileSync } from "fs";
import { BufferCursor } from "../cursor/BufferCursor";
import { Endianness } from "..";
import { Endianness } from "../Endianness";
test("parse_item_pmt", () => {
const buf = readFileSync("test/resources/ItemPMT.bin");

View File

@ -0,0 +1,13 @@
export enum Episode {
I = 1,
II = 2,
IV = 4,
}
export const EPISODES = [Episode.I, Episode.II, Episode.IV];
export function check_episode(episode: Episode): void {
if (Episode[episode] == undefined) {
throw new Error(`Invalid episode ${episode}.`);
}
}

View File

@ -0,0 +1,100 @@
import { Episode } from "./Episode";
export type Area = {
readonly id: number;
readonly name: string;
readonly order: number;
readonly area_variants: AreaVariant[];
};
export type AreaVariant = {
readonly id: number;
readonly area: Area;
};
export function get_areas_for_episode(episode: Episode): Area[] {
return AREAS[episode];
}
export function get_area_variant(
episode: Episode,
area_id: number,
variant_id: number,
): AreaVariant {
const area = AREAS[episode].find(area => area.id === area_id);
if (!area) throw new Error(`No area with id ${area_id}.`);
const variant = area.area_variants[variant_id];
if (!variant) throw new Error(`No area variant with id ${variant_id}.`);
return variant;
}
const AREAS: { [episode: number]: Area[] } = [];
function create_area(id: number, name: string, order: number, variants: number): Area {
const area: Area = { id, name, order, area_variants: [] };
for (let id = 0; id < variants; id++) {
area.area_variants.push({ id, area });
}
return area;
}
// The IDs match the PSO IDs for areas.
let order = 0;
AREAS[Episode.I] = [
create_area(0, "Pioneer II", order++, 1),
create_area(1, "Forest 1", order++, 1),
create_area(2, "Forest 2", order++, 1),
create_area(11, "Under the Dome", order++, 1),
create_area(3, "Cave 1", order++, 6),
create_area(4, "Cave 2", order++, 5),
create_area(5, "Cave 3", order++, 6),
create_area(12, "Underground Channel", order++, 1),
create_area(6, "Mine 1", order++, 6),
create_area(7, "Mine 2", order++, 6),
create_area(13, "Monitor Room", order++, 1),
create_area(8, "Ruins 1", order++, 5),
create_area(9, "Ruins 2", order++, 5),
create_area(10, "Ruins 3", order++, 5),
create_area(14, "Dark Falz", order++, 1),
create_area(15, "BA Ruins", order++, 3),
create_area(16, "BA Spaceship", order++, 3),
create_area(17, "Lobby", order++, 15),
];
order = 0;
AREAS[Episode.II] = [
create_area(0, "Lab", order++, 1),
create_area(1, "VR Temple Alpha", order++, 3),
create_area(2, "VR Temple Beta", order++, 3),
create_area(14, "VR Temple Final", order++, 1),
create_area(3, "VR Spaceship Alpha", order++, 3),
create_area(4, "VR Spaceship Beta", order++, 3),
create_area(15, "VR Spaceship Final", order++, 1),
create_area(5, "Central Control Area", order++, 1),
create_area(6, "Jungle Area East", order++, 1),
create_area(7, "Jungle Area North", order++, 1),
create_area(8, "Mountain Area", order++, 3),
create_area(9, "Seaside Area", order++, 1),
create_area(12, "Cliffs of Gal Da Val", order++, 1),
create_area(10, "Seabed Upper Levels", order++, 3),
create_area(11, "Seabed Lower Levels", order++, 3),
create_area(13, "Test Subject Disposal Area", order++, 1),
create_area(16, "Seaside Area at Night", order++, 1),
create_area(17, "Control Tower", order++, 5),
];
order = 0;
AREAS[Episode.IV] = [
create_area(0, "Pioneer II (Ep. IV)", order++, 1),
create_area(1, "Crater Route 1", order++, 1),
create_area(2, "Crater Route 2", order++, 1),
create_area(3, "Crater Route 3", order++, 1),
create_area(4, "Crater Route 4", order++, 1),
create_area(5, "Crater Interior", order++, 1),
create_area(6, "Subterranean Desert 1", order++, 3),
create_area(7, "Subterranean Desert 2", order++, 3),
create_area(8, "Subterranean Desert 3", order++, 3),
create_area(9, "Meteor Impact Site", order++, 1),
];

View File

@ -1,5 +1,5 @@
import { readFileSync } from "fs";
import { Endianness } from "../..";
import { Endianness } from "../../Endianness";
import { prs_decompress } from "../../compression/prs/decompress";
import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor";
import { BufferCursor } from "../../cursor/BufferCursor";

View File

@ -1,5 +1,5 @@
import Logger from "js-logger";
import { Endianness } from "../..";
import { Endianness } from "../../Endianness";
import { ControlFlowGraph } from "../../../scripting/data_flow_analysis/ControlFlowGraph";
import { register_value } from "../../../scripting/data_flow_analysis/register_value";
import { stack_value } from "../../../scripting/data_flow_analysis/stack_value";
@ -567,18 +567,19 @@ function parse_instructions_segment(
// Recurse on label drop-through.
if (next_label != undefined) {
// Find the first non-nop.
let last_opcode: Opcode | undefined;
// Find the first ret or jmp.
let drop_through = true;
for (let i = instructions.length - 1; i >= 0; i--) {
last_opcode = instructions[i].opcode;
const opcode = instructions[i].opcode;
if (last_opcode !== Opcode.NOP) {
if (opcode === Opcode.RET || opcode === Opcode.JMP) {
drop_through = false;
break;
}
}
if (last_opcode !== Opcode.RET && last_opcode !== Opcode.JMP) {
if (drop_through) {
parse_segment(
offset_to_segment,
label_holder,

View File

@ -1,4 +1,4 @@
import { Endianness } from "../..";
import { Endianness } from "../../Endianness";
import { prs_decompress } from "../../compression/prs/decompress";
import { BufferCursor } from "../../cursor/BufferCursor";
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";

View File

@ -1,6 +1,6 @@
import Logger from "js-logger";
import { groupBy } from "lodash";
import { Endianness } from "../..";
import { Endianness } from "../../Endianness";
import { Cursor } from "../../cursor/Cursor";
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";
import { ResizableBuffer } from "../../ResizableBuffer";

View File

@ -0,0 +1,51 @@
import { Vec3 } from "../../vector";
import { npc_data, NpcType, NpcTypeData } from "./npc_types";
import { object_data, ObjectType, ObjectTypeData } from "./object_types";
export type QuestEntity = QuestNpc | QuestObject;
export type QuestNpc = {
readonly type: NpcType;
readonly area_id: number;
readonly section_id: number;
/**
* Section-relative position
*/
readonly position: Vec3;
readonly rotation: Vec3;
readonly scale: Vec3;
/**
* Data of which the purpose hasn't been discovered yet.
*/
readonly unknown: number[][];
readonly pso_type_id: number;
readonly pso_skin: number;
};
export type QuestObject = {
readonly type: ObjectType;
readonly area_id: number;
readonly section_id: number;
/**
* Section-relative position
*/
readonly position: Vec3;
readonly rotation: Vec3;
readonly scale: Vec3;
/**
* Data of which the purpose hasn't been discovered yet.
*/
readonly unknown: number[][];
};
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 entity_data(type: EntityType): EntityTypeData {
return npc_data(type as NpcType) || object_data(type as ObjectType);
}

View File

@ -1,10 +1,10 @@
import { readFileSync } from "fs";
import { Endianness } from "../..";
import { Endianness } from "../../Endianness";
import { walk_qst_files } from "../../../../test/src/utils";
import { ObjectType, Quest } from "../../../domain";
import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor";
import { BufferCursor } from "../../cursor/BufferCursor";
import { parse_quest, write_quest_qst } from "../quest";
import { parse_quest, Quest, write_quest_qst } from "./index";
import { ObjectType } from "./object_types";
test("parse Towards the Future", () => {
const buffer = readFileSync("test/resources/quest118_e.qst");
@ -14,7 +14,7 @@ test("parse Towards the Future", () => {
expect(quest.name).toBe("Towards the Future");
expect(quest.short_description).toBe("Challenge the\nnew simulator.");
expect(quest.long_description).toBe(
"Client: Principal\nQuest: Wishes to have\nhunters challenge the\nnew simulator\nReward: ??? Meseta"
"Client: Principal\nQuest: Wishes to have\nhunters challenge the\nnew simulator\nReward: ??? Meseta",
);
expect(quest.episode).toBe(1);
expect(quest.objects.length).toBe(277);
@ -36,25 +36,25 @@ test("parse Towards the Future", () => {
});
/**
* Roundtrip tests.
* Round-trip tests.
* Parse a QST file, write the resulting Quest object to QST again, then parse that again.
* Then check whether the two Quest objects are equal.
*/
if (process.env["RUN_ALL_TESTS"] === "true") {
walk_qst_files(roundtrip_test);
walk_qst_files(round_trip_test);
} else {
const file_name_1 = "quest118_e.qst";
const path_1 = `test/resources/${file_name_1}`;
const buffer_1 = readFileSync(path_1);
roundtrip_test(path_1, file_name_1, buffer_1);
round_trip_test(path_1, file_name_1, buffer_1);
const file_name_2 = "quest27_e.qst";
const path_2 = `test/resources/${file_name_2}`;
const buffer_2 = readFileSync(path_2);
roundtrip_test(path_2, file_name_2, buffer_2);
round_trip_test(path_2, file_name_2, buffer_2);
}
function roundtrip_test(path: string, file_name: string, contents: Buffer): void {
function round_trip_test(path: string, file_name: string, contents: Buffer): void {
test(`parse_quest and write_quest_qst ${path}`, () => {
const orig_quest = parse_quest(new BufferCursor(contents, Endianness.Little))!;
const test_bin = write_quest_qst(orig_quest, file_name);
@ -72,7 +72,7 @@ function roundtrip_test(path: string, file_name: string, contents: Buffer): void
expect(test_obj.area_id).toBe(orig_obj.area_id);
expect(test_obj.section_id).toBe(orig_obj.section_id);
expect(test_obj.position).toEqual(orig_obj.position);
expect(test_obj.type.id).toBe(orig_obj.type.id);
expect(test_obj.type).toBe(orig_obj.type);
}
expect(test_quest.npcs.length).toBe(orig_quest.npcs.length);
@ -83,7 +83,7 @@ function roundtrip_test(path: string, file_name: string, contents: Buffer): void
expect(test_npc.area_id).toBe(orig_npc.area_id);
expect(test_npc.section_id).toBe(orig_npc.section_id);
expect(test_npc.position).toEqual(orig_npc.position);
expect(test_npc.type.id).toBe(orig_npc.type.id);
expect(test_npc.type).toBe(orig_npc.type);
}
expect(test_quest.area_variants.length).toBe(orig_quest.area_variants.length);
@ -91,8 +91,8 @@ function roundtrip_test(path: string, file_name: string, contents: Buffer): void
for (let i = 0; i < orig_quest.area_variants.length; i++) {
const orig_area_variant = orig_quest.area_variants[i];
const test_area_variant = test_quest.area_variants[i];
expect(test_area_variant.area.id).toBe(orig_area_variant.area.id);
expect(test_area_variant.id).toBe(orig_area_variant.id);
expect(test_area_variant.area.id).toBe(orig_area_variant.area.id);
}
expect(test_quest.object_code.length).toBe(orig_quest.object_code.length);

View File

@ -1,29 +1,47 @@
import Logger from "js-logger";
import { Endianness } from "../..";
import {
AreaVariant,
Episode,
NpcType,
ObjectType,
Quest,
QuestNpc,
QuestObject,
} from "../../../domain";
import { Instruction, InstructionSegment, SegmentType } from "../../../scripting/instructions";
Instruction,
InstructionSegment,
Segment,
SegmentType,
} from "../../../scripting/instructions";
import { Opcode } from "../../../scripting/opcodes";
import { area_store } from "../../../stores/AreaStore";
import { prs_compress } from "../../compression/prs/compress";
import { prs_decompress } from "../../compression/prs/decompress";
import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor";
import { Cursor } from "../../cursor/Cursor";
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";
import { Endianness } from "../../Endianness";
import { Vec3 } from "../../vector";
import { AreaVariant, get_area_variant } from "./areas";
import { BinFile, parse_bin, write_bin } from "./bin";
import { DatFile, DatNpc, DatObject, parse_dat, write_dat } from "./dat";
import { DatFile, DatNpc, DatObject, DatUnknown, parse_dat, write_dat } from "./dat";
import { QuestNpc, QuestObject } from "./entities";
import { Episode } from "./Episode";
import { object_data, pso_id_to_object_type } from "./object_types";
import { parse_qst, QstContainedFile, write_qst } from "./qst";
import { NpcType } from "./npc_types";
const logger = Logger.get("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: QuestObject[];
readonly npcs: QuestNpc[];
/**
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
*/
readonly dat_unknowns: DatUnknown[];
readonly object_code: Segment[];
readonly shop_items: number[];
readonly area_variants: AreaVariant[];
};
/**
* High level parsing function that delegates to lower level parsing functions.
*
@ -60,14 +78,14 @@ export function parse_quest(cursor: Cursor, lenient: boolean = false): Quest | u
}
const dat_decompressed = prs_decompress(
new ArrayBufferCursor(dat_file.data, Endianness.Little)
new ArrayBufferCursor(dat_file.data, Endianness.Little),
);
const dat = parse_dat(dat_decompressed);
const bin_decompressed = prs_decompress(
new ArrayBufferCursor(bin_file.data, Endianness.Little)
new ArrayBufferCursor(bin_file.data, Endianness.Little),
);
const bin = parse_bin(bin_decompressed, [0], lenient);
let episode = 1;
let episode = Episode.I;
let area_variants: AreaVariant[] = [];
if (bin.object_code.length) {
@ -82,7 +100,12 @@ export function parse_quest(cursor: Cursor, lenient: boolean = false): Quest | u
if (label_0_segment) {
episode = get_episode(label_0_segment.instructions);
area_variants = get_area_variants(dat, episode, label_0_segment.instructions, lenient);
area_variants = extract_area_variants(
dat,
episode,
label_0_segment.instructions,
lenient,
);
} else {
logger.warn(`No instruction for label 0 found.`);
}
@ -90,20 +113,20 @@ export function parse_quest(cursor: Cursor, lenient: boolean = false): Quest | u
logger.warn("File contains no instruction labels.");
}
return new Quest(
bin.quest_id,
bin.language,
bin.quest_name,
bin.short_description,
bin.long_description,
return {
id: bin.quest_id,
language: bin.language,
name: bin.quest_name,
short_description: bin.short_description,
long_description: bin.long_description,
episode,
objects: parse_obj_data(dat.objs),
npcs: parse_npc_data(episode, dat.npcs),
dat_unknowns: dat.unknowns,
object_code: bin.object_code,
shop_items: bin.shop_items,
area_variants,
parse_obj_data(dat.objs),
parse_npc_data(episode, dat.npcs),
dat.unknowns,
bin.object_code,
bin.shop_items
);
};
}
export function write_quest_qst(quest: Quest, file_name: string): ArrayBuffer {
@ -120,8 +143,8 @@ export function write_quest_qst(quest: Quest, file_name: string): ArrayBuffer {
quest.short_description,
quest.long_description,
quest.object_code,
quest.shop_items
)
quest.shop_items,
),
);
const ext_start = file_name.lastIndexOf(".");
const base_file_name =
@ -133,7 +156,7 @@ export function write_quest_qst(quest: Quest, file_name: string): ArrayBuffer {
name: base_file_name + ".dat",
id: quest.id,
data: prs_compress(
new ResizableBufferCursor(dat, Endianness.Little)
new ResizableBufferCursor(dat, Endianness.Little),
).array_buffer(),
},
{
@ -150,7 +173,7 @@ export function write_quest_qst(quest: Quest, file_name: string): ArrayBuffer {
*/
function get_episode(func_0_instructions: Instruction[]): Episode {
const set_episode = func_0_instructions.find(
instruction => instruction.opcode === Opcode.SET_EPISODE
instruction => instruction.opcode === Opcode.SET_EPISODE,
);
if (set_episode) {
@ -165,15 +188,15 @@ function get_episode(func_0_instructions: Instruction[]): Episode {
}
} else {
logger.debug("Function 0 has no set_episode instruction.");
return 1;
return Episode.I;
}
}
function get_area_variants(
function extract_area_variants(
dat: DatFile,
episode: number,
episode: Episode,
func_0_instructions: Instruction[],
lenient: boolean
lenient: boolean,
): AreaVariant[] {
// Add area variants that have npcs or objects even if there are no BB_Map_Designate instructions for them.
const area_variants = new Map<number, number>();
@ -187,7 +210,7 @@ function get_area_variants(
}
const bb_maps = func_0_instructions.filter(
instruction => instruction.opcode === Opcode.BB_MAP_DESIGNATE
instruction => instruction.opcode === Opcode.BB_MAP_DESIGNATE,
);
for (const bb_map of bb_maps) {
@ -196,11 +219,11 @@ function get_area_variants(
area_variants.set(area_id, variant_id);
}
const area_variants_array = new Array<AreaVariant>();
const area_variants_array: AreaVariant[] = [];
for (const [area_id, variant_id] of area_variants.entries()) {
try {
area_variants_array.push(area_store.get_variant(episode, area_id, variant_id));
area_variants_array.push(get_area_variant(episode, area_id, variant_id));
} catch (e) {
if (lenient) {
logger.error(`Unknown area variant.`, e);
@ -216,31 +239,31 @@ function get_area_variants(
function parse_obj_data(objs: DatObject[]): QuestObject[] {
return objs.map(obj_data => {
return new QuestObject(
ObjectType.from_pso_id(obj_data.type_id),
obj_data.area_id,
obj_data.section_id,
obj_data.position.clone(),
obj_data.rotation.clone(),
obj_data.scale.clone(),
obj_data.unknown
);
return {
type: pso_id_to_object_type(obj_data.type_id),
area_id: obj_data.area_id,
section_id: obj_data.section_id,
position: obj_data.position,
rotation: obj_data.rotation,
scale: obj_data.scale,
unknown: obj_data.unknown,
};
});
}
function parse_npc_data(episode: number, npcs: DatNpc[]): QuestNpc[] {
return npcs.map(npc_data => {
return new QuestNpc(
get_npc_type(episode, npc_data),
npc_data.type_id,
npc_data.skin,
npc_data.area_id,
npc_data.section_id,
npc_data.position.clone(),
npc_data.rotation.clone(),
npc_data.scale.clone(),
npc_data.unknown
);
return {
type: get_npc_type(episode, npc_data),
area_id: npc_data.area_id,
section_id: npc_data.section_id,
position: npc_data.position,
rotation: npc_data.rotation,
scale: npc_data.scale,
unknown: npc_data.unknown,
pso_type_id: npc_data.type_id,
pso_skin: npc_data.skin,
};
});
}
@ -522,11 +545,11 @@ function get_npc_type(episode: number, { type_id, scale, skin, area_id }: DatNpc
function objects_to_dat_data(objects: QuestObject[]): DatObject[] {
return objects.map(object => ({
type_id: object.type.pso_id!,
type_id: object_data(object.type).pso_id!,
section_id: object.section_id,
position: object.section_position.clone(),
rotation: object.rotation.clone(),
scale: object.scale.clone(),
position: object.position,
rotation: object.rotation,
scale: object.scale,
area_id: object.area_id,
unknown: object.unknown,
}));
@ -551,8 +574,8 @@ function npcs_to_dat_data(npcs: QuestNpc[]): DatNpc[] {
return {
type_id: type_data.type_id,
section_id: npc.section_id,
position: npc.section_position.clone(),
rotation: npc.rotation.clone(),
position: npc.position,
rotation: npc.rotation,
scale,
skin: type_data.skin,
area_id: npc.area_id,
@ -562,11 +585,11 @@ function npcs_to_dat_data(npcs: QuestNpc[]): DatNpc[] {
}
function npc_type_to_dat_data(
type: NpcType
type: NpcType,
): { type_id: number; skin: number; regular: boolean } | undefined {
switch (type) {
default:
throw new Error(`Unexpected type ${type.code}.`);
throw new Error(`Unexpected type ${NpcType[type]}.`);
case NpcType.Unknown:
return undefined;

View File

@ -0,0 +1,669 @@
import { Episode, check_episode } from "./Episode";
export enum NpcType {
//
// Unknown NPCs
//
Unknown,
//
// Friendly NPCs
//
FemaleFat,
FemaleMacho,
FemaleTall,
MaleDwarf,
MaleFat,
MaleMacho,
MaleOld,
BlueSoldier,
RedSoldier,
Principal,
Tekker,
GuildLady,
Scientist,
Nurse,
Irene,
ItemShop,
Nurse2,
//
// Enemy NPCs
//
// Episode I Forest
Hildebear,
Hildeblue,
RagRappy,
AlRappy,
Monest,
Mothmant,
SavageWolf,
BarbarousWolf,
Booma,
Gobooma,
Gigobooma,
Dragon,
// Episode I Caves
GrassAssassin,
PoisonLily,
NarLily,
NanoDragon,
EvilShark,
PalShark,
GuilShark,
PofuillySlime,
PouillySlime,
PanArms,
Migium,
Hidoom,
DeRolLe,
// Episode I Mines
Dubchic,
Gilchic,
Garanz,
SinowBeat,
SinowGold,
Canadine,
Canane,
Dubswitch,
VolOpt,
// Episode I Ruins
Delsaber,
ChaosSorcerer,
DarkGunner,
DeathGunner,
ChaosBringer,
DarkBelra,
Dimenian,
LaDimenian,
SoDimenian,
Bulclaw,
Bulk,
Claw,
DarkFalz,
// Episode II VR Temple
Hildebear2,
Hildeblue2,
RagRappy2,
LoveRappy,
StRappy,
HalloRappy,
EggRappy,
Monest2,
Mothmant2,
PoisonLily2,
NarLily2,
GrassAssassin2,
Dimenian2,
LaDimenian2,
SoDimenian2,
DarkBelra2,
BarbaRay,
// Episode II VR Spaceship
SavageWolf2,
BarbarousWolf2,
PanArms2,
Migium2,
Hidoom2,
Dubchic2,
Gilchic2,
Garanz2,
Dubswitch2,
Delsaber2,
ChaosSorcerer2,
GolDragon,
// Episode II Central Control Area
SinowBerill,
SinowSpigell,
Merillia,
Meriltas,
Mericarol,
Mericus,
Merikle,
UlGibbon,
ZolGibbon,
Gibbles,
Gee,
GiGue,
IllGill,
DelLily,
Epsilon,
GalGryphon,
// Episode II Seabed
Deldepth,
Delbiter,
Dolmolm,
Dolmdarl,
Morfos,
Recobox,
Recon,
SinowZoa,
SinowZele,
OlgaFlow,
// Episode IV
SandRappy,
DelRappy,
Astark,
SatelliteLizard,
Yowie,
MerissaA,
MerissaAA,
Girtablulu,
Zu,
Pazuzu,
Boota,
ZeBoota,
BaBoota,
Dorphon,
DorphonEclair,
Goran,
PyroGoran,
GoranDetonator,
SaintMilion,
Shambertin,
// Kondrieu should be last to make sure ObjectType does not overlap NpcType. See code below.
Kondrieu,
}
export type NpcTypeData = {
/**
* Unique name. E.g. a Delsaber would have (Ep. II) appended to its name.
*/
readonly name: string;
/**
* Name used in the game.
* Might conflict with other NPC names (e.g. Delsaber from ep. I and ep. II).
*/
readonly simple_name: string;
readonly ultimate_name: string;
readonly episode?: Episode;
readonly enemy: boolean;
readonly rare_type?: NpcType;
};
export const ENEMY_NPC_TYPES: NpcType[] = [];
export function npc_data(type: NpcType): NpcTypeData {
return NPC_TYPE_DATA[type];
}
/**
* Uniquely identifies an NPC. Tries to match on simple_name and ultimate_name.
*/
export function name_and_episode_to_npc_type(name: string, episode: Episode): NpcType | undefined {
check_episode(episode);
return EP_AND_NAME_TO_NPC_TYPE[episode]!.get(name);
}
const EP_AND_NAME_TO_NPC_TYPE = [
undefined,
new Map<string, NpcType>(),
new Map<string, NpcType>(),
undefined,
new Map<string, NpcType>(),
];
const NPC_TYPE_DATA: NpcTypeData[] = [];
function define_npc_type_data(
npc_type: NpcType,
name: string,
simple_name: string,
ultimate_name: string,
episode: Episode | undefined,
enemy: boolean,
rare_type?: NpcType,
): void {
if (episode) {
const map = EP_AND_NAME_TO_NPC_TYPE[episode];
if (map) {
map.set(simple_name, npc_type);
map.set(ultimate_name, npc_type);
}
}
NPC_TYPE_DATA[npc_type] = {
name,
simple_name,
ultimate_name,
episode,
enemy,
rare_type,
};
if (enemy) {
ENEMY_NPC_TYPES.push(npc_type);
}
}
//
// Unknown NPCs
//
define_npc_type_data(NpcType.Unknown, "Unknown", "Unknown", "Unknown", undefined, false);
//
// Friendly NPCs
//
define_npc_type_data(NpcType.FemaleFat, "Female Fat", "Female Fat", "Female Fat", undefined, false);
define_npc_type_data(
NpcType.FemaleMacho,
"Female Macho",
"Female Macho",
"Female Macho",
undefined,
false,
);
define_npc_type_data(
NpcType.FemaleTall,
"Female Tall",
"Female Tall",
"Female Tall",
undefined,
false,
);
define_npc_type_data(NpcType.MaleDwarf, "Male Dwarf", "Male Dwarf", "Male Dwarf", undefined, false);
define_npc_type_data(NpcType.MaleFat, "Male Fat", "Male Fat", "Male Fat", undefined, false);
define_npc_type_data(NpcType.MaleMacho, "Male Macho", "Male Macho", "Male Macho", undefined, false);
define_npc_type_data(NpcType.MaleOld, "Male Old", "Male Old", "Male Old", undefined, false);
define_npc_type_data(
NpcType.BlueSoldier,
"Blue Soldier",
"Blue Soldier",
"Blue Soldier",
undefined,
false,
);
define_npc_type_data(
NpcType.RedSoldier,
"Red Soldier",
"Red Soldier",
"Red Soldier",
undefined,
false,
);
define_npc_type_data(NpcType.Principal, "Principal", "Principal", "Principal", undefined, false);
define_npc_type_data(NpcType.Tekker, "Tekker", "Tekker", "Tekker", undefined, false);
define_npc_type_data(NpcType.GuildLady, "Guild Lady", "Guild Lady", "Guild Lady", undefined, false);
define_npc_type_data(NpcType.Scientist, "Scientist", "Scientist", "Scientist", undefined, false);
define_npc_type_data(NpcType.Nurse, "Nurse", "Nurse", "Nurse", undefined, false);
define_npc_type_data(NpcType.Irene, "Irene", "Irene", "Irene", undefined, false);
define_npc_type_data(NpcType.ItemShop, "Item Shop", "Item Shop", "Item Shop", undefined, false);
define_npc_type_data(NpcType.Nurse2, "Nurse (Ep. II);", "Nurse", "Nurse", 2, false);
//
// Enemy NPCs
//
// Episode I Forest
define_npc_type_data(
NpcType.Hildebear,
"Hildebear",
"Hildebear",
"Hildelt",
1,
true,
NpcType.Hildeblue,
);
define_npc_type_data(NpcType.Hildeblue, "Hildeblue", "Hildeblue", "Hildetorr", 1, true);
define_npc_type_data(
NpcType.RagRappy,
"Rag Rappy",
"Rag Rappy",
"El Rappy",
1,
true,
NpcType.AlRappy,
);
define_npc_type_data(NpcType.AlRappy, "Al Rappy", "Al Rappy", "Pal Rappy", 1, true);
define_npc_type_data(NpcType.Monest, "Monest", "Monest", "Mothvist", 1, true);
define_npc_type_data(NpcType.Mothmant, "Mothmant", "Mothmant", "Mothvert", 1, true);
define_npc_type_data(NpcType.SavageWolf, "Savage Wolf", "Savage Wolf", "Gulgus", 1, true);
define_npc_type_data(
NpcType.BarbarousWolf,
"Barbarous Wolf",
"Barbarous Wolf",
"Gulgus-Gue",
1,
true,
);
define_npc_type_data(NpcType.Booma, "Booma", "Booma", "Bartle", 1, true);
define_npc_type_data(NpcType.Gobooma, "Gobooma", "Gobooma", "Barble", 1, true);
define_npc_type_data(NpcType.Gigobooma, "Gigobooma", "Gigobooma", "Tollaw", 1, true);
define_npc_type_data(NpcType.Dragon, "Dragon", "Dragon", "Sil Dragon", 1, true);
// Episode I Caves
define_npc_type_data(
NpcType.GrassAssassin,
"Grass Assassin",
"Grass Assassin",
"Crimson Assassin",
1,
true,
);
define_npc_type_data(
NpcType.PoisonLily,
"Poison Lily",
"Poison Lily",
"Ob Lily",
1,
true,
NpcType.NarLily,
);
define_npc_type_data(NpcType.NarLily, "Nar Lily", "Nar Lily", "Mil Lily", 1, true);
define_npc_type_data(NpcType.NanoDragon, "Nano Dragon", "Nano Dragon", "Nano Dragon", 1, true);
define_npc_type_data(NpcType.EvilShark, "Evil Shark", "Evil Shark", "Vulmer", 1, true);
define_npc_type_data(NpcType.PalShark, "Pal Shark", "Pal Shark", "Govulmer", 1, true);
define_npc_type_data(NpcType.GuilShark, "Guil Shark", "Guil Shark", "Melqueek", 1, true);
define_npc_type_data(
NpcType.PofuillySlime,
"Pofuilly Slime",
"Pofuilly Slime",
"Pofuilly Slime",
1,
true,
NpcType.PouillySlime,
);
define_npc_type_data(
NpcType.PouillySlime,
"Pouilly Slime",
"Pouilly Slime",
"Pouilly Slime",
1,
true,
);
define_npc_type_data(NpcType.PanArms, "Pan Arms", "Pan Arms", "Pan Arms", 1, true);
define_npc_type_data(NpcType.Migium, "Migium", "Migium", "Migium", 1, true);
define_npc_type_data(NpcType.Hidoom, "Hidoom", "Hidoom", "Hidoom", 1, true);
define_npc_type_data(NpcType.DeRolLe, "De Rol Le", "De Rol Le", "Dal Ra Lie", 1, true);
// Episode I Mines
define_npc_type_data(NpcType.Dubchic, "Dubchic", "Dubchic", "Dubchich", 1, true);
define_npc_type_data(NpcType.Gilchic, "Gilchic", "Gilchic", "Gilchich", 1, true);
define_npc_type_data(NpcType.Garanz, "Garanz", "Garanz", "Baranz", 1, true);
define_npc_type_data(NpcType.SinowBeat, "Sinow Beat", "Sinow Beat", "Sinow Blue", 1, true);
define_npc_type_data(NpcType.SinowGold, "Sinow Gold", "Sinow Gold", "Sinow Red", 1, true);
define_npc_type_data(NpcType.Canadine, "Canadine", "Canadine", "Canabin", 1, true);
define_npc_type_data(NpcType.Canane, "Canane", "Canane", "Canune", 1, true);
define_npc_type_data(NpcType.Dubswitch, "Dubswitch", "Dubswitch", "Dubswitch", 1, true);
define_npc_type_data(NpcType.VolOpt, "Vol Opt", "Vol Opt", "Vol Opt ver.2", 1, true);
// Episode I Ruins
define_npc_type_data(NpcType.Delsaber, "Delsaber", "Delsaber", "Delsaber", 1, true);
define_npc_type_data(
NpcType.ChaosSorcerer,
"Chaos Sorcerer",
"Chaos Sorcerer",
"Gran Sorcerer",
1,
true,
);
define_npc_type_data(NpcType.DarkGunner, "Dark Gunner", "Dark Gunner", "Dark Gunner", 1, true);
define_npc_type_data(NpcType.DeathGunner, "Death Gunner", "Death Gunner", "Death Gunner", 1, true);
define_npc_type_data(
NpcType.ChaosBringer,
"Chaos Bringer",
"Chaos Bringer",
"Dark Bringer",
1,
true,
);
define_npc_type_data(NpcType.DarkBelra, "Dark Belra", "Dark Belra", "Indi Belra", 1, true);
define_npc_type_data(NpcType.Dimenian, "Dimenian", "Dimenian", "Arlan", 1, true);
define_npc_type_data(NpcType.LaDimenian, "La Dimenian", "La Dimenian", "Merlan", 1, true);
define_npc_type_data(NpcType.SoDimenian, "So Dimenian", "So Dimenian", "Del-D", 1, true);
define_npc_type_data(NpcType.Bulclaw, "Bulclaw", "Bulclaw", "Bulclaw", 1, true);
define_npc_type_data(NpcType.Bulk, "Bulk", "Bulk", "Bulk", 1, true);
define_npc_type_data(NpcType.Claw, "Claw", "Claw", "Claw", 1, true);
define_npc_type_data(NpcType.DarkFalz, "Dark Falz", "Dark Falz", "Dark Falz", 1, true);
// Episode II VR Temple
define_npc_type_data(
NpcType.Hildebear2,
"Hildebear (Ep. II);",
"Hildebear",
"Hildelt",
2,
true,
NpcType.Hildeblue2,
);
define_npc_type_data(NpcType.Hildeblue2, "Hildeblue (Ep. II);", "Hildeblue", "Hildetorr", 2, true);
define_npc_type_data(
NpcType.RagRappy2,
"Rag Rappy (Ep. II);",
"Rag Rappy",
"El Rappy",
2,
true,
NpcType.LoveRappy,
);
define_npc_type_data(NpcType.LoveRappy, "Love Rappy", "Love Rappy", "Love Rappy", 2, true);
define_npc_type_data(NpcType.StRappy, "St. Rappy", "St. Rappy", "St. Rappy", 2, true);
define_npc_type_data(NpcType.HalloRappy, "Hallo Rappy", "Hallo Rappy", "Hallo Rappy", 2, true);
define_npc_type_data(NpcType.EggRappy, "Egg Rappy", "Egg Rappy", "Egg Rappy", 2, true);
define_npc_type_data(NpcType.Monest2, "Monest (Ep. II);", "Monest", "Mothvist", 2, true);
define_npc_type_data(NpcType.Mothmant2, "Mothmant", "Mothmant", "Mothvert", 2, true);
define_npc_type_data(
NpcType.PoisonLily2,
"Poison Lily (Ep. II);",
"Poison Lily",
"Ob Lily",
2,
true,
NpcType.NarLily2,
);
define_npc_type_data(NpcType.NarLily2, "Nar Lily (Ep. II);", "Nar Lily", "Mil Lily", 2, true);
define_npc_type_data(
NpcType.GrassAssassin2,
"Grass Assassin (Ep. II);",
"Grass Assassin",
"Crimson Assassin",
2,
true,
);
define_npc_type_data(NpcType.Dimenian2, "Dimenian (Ep. II);", "Dimenian", "Arlan", 2, true);
define_npc_type_data(
NpcType.LaDimenian2,
"La Dimenian (Ep. II);",
"La Dimenian",
"Merlan",
2,
true,
);
define_npc_type_data(NpcType.SoDimenian2, "So Dimenian (Ep. II);", "So Dimenian", "Del-D", 2, true);
define_npc_type_data(
NpcType.DarkBelra2,
"Dark Belra (Ep. II);",
"Dark Belra",
"Indi Belra",
2,
true,
);
define_npc_type_data(NpcType.BarbaRay, "Barba Ray", "Barba Ray", "Barba Ray", 2, true);
// Episode II VR Spaceship
define_npc_type_data(
NpcType.SavageWolf2,
"Savage Wolf (Ep. II);",
"Savage Wolf",
"Gulgus",
2,
true,
);
define_npc_type_data(
NpcType.BarbarousWolf2,
"Barbarous Wolf (Ep. II);",
"Barbarous Wolf",
"Gulgus-Gue",
2,
true,
);
define_npc_type_data(NpcType.PanArms2, "Pan Arms (Ep. II);", "Pan Arms", "Pan Arms", 2, true);
define_npc_type_data(NpcType.Migium2, "Migium (Ep. II);", "Migium", "Migium", 2, true);
define_npc_type_data(NpcType.Hidoom2, "Hidoom (Ep. II);", "Hidoom", "Hidoom", 2, true);
define_npc_type_data(NpcType.Dubchic2, "Dubchic (Ep. II);", "Dubchic", "Dubchich", 2, true);
define_npc_type_data(NpcType.Gilchic2, "Gilchic (Ep. II);", "Gilchic", "Gilchich", 2, true);
define_npc_type_data(NpcType.Garanz2, "Garanz (Ep. II);", "Garanz", "Baranz", 2, true);
define_npc_type_data(NpcType.Dubswitch2, "Dubswitch (Ep. II);", "Dubswitch", "Dubswitch", 2, true);
define_npc_type_data(NpcType.Delsaber2, "Delsaber (Ep. II);", "Delsaber", "Delsaber", 2, true);
define_npc_type_data(
NpcType.ChaosSorcerer2,
"Chaos Sorcerer (Ep. II);",
"Chaos Sorcerer",
"Gran Sorcerer",
2,
true,
);
define_npc_type_data(NpcType.GolDragon, "Gol Dragon", "Gol Dragon", "Gol Dragon", 2, true);
// Episode II Central Control Area
define_npc_type_data(NpcType.SinowBerill, "Sinow Berill", "Sinow Berill", "Sinow Berill", 2, true);
define_npc_type_data(
NpcType.SinowSpigell,
"Sinow Spigell",
"Sinow Spigell",
"Sinow Spigell",
2,
true,
);
define_npc_type_data(NpcType.Merillia, "Merillia", "Merillia", "Merillia", 2, true);
define_npc_type_data(NpcType.Meriltas, "Meriltas", "Meriltas", "Meriltas", 2, true);
define_npc_type_data(NpcType.Mericarol, "Mericarol", "Mericarol", "Mericarol", 2, true);
define_npc_type_data(NpcType.Mericus, "Mericus", "Mericus", "Mericus", 2, true);
define_npc_type_data(NpcType.Merikle, "Merikle", "Merikle", "Merikle", 2, true);
define_npc_type_data(NpcType.UlGibbon, "Ul Gibbon", "Ul Gibbon", "Ul Gibbon", 2, true);
define_npc_type_data(NpcType.ZolGibbon, "Zol Gibbon", "Zol Gibbon", "Zol Gibbon", 2, true);
define_npc_type_data(NpcType.Gibbles, "Gibbles", "Gibbles", "Gibbles", 2, true);
define_npc_type_data(NpcType.Gee, "Gee", "Gee", "Gee", 2, true);
define_npc_type_data(NpcType.GiGue, "Gi Gue", "Gi Gue", "Gi Gue", 2, true);
define_npc_type_data(NpcType.IllGill, "Ill Gill", "Ill Gill", "Ill Gill", 2, true);
define_npc_type_data(NpcType.DelLily, "Del Lily", "Del Lily", "Del Lily", 2, true);
define_npc_type_data(NpcType.Epsilon, "Epsilon", "Epsilon", "Epsilon", 2, true);
define_npc_type_data(NpcType.GalGryphon, "Gal Gryphon", "Gal Gryphon", "Gal Gryphon", 2, true);
// Episode II Seabed
define_npc_type_data(NpcType.Deldepth, "Deldepth", "Deldepth", "Deldepth", 2, true);
define_npc_type_data(NpcType.Delbiter, "Delbiter", "Delbiter", "Delbiter", 2, true);
define_npc_type_data(NpcType.Dolmolm, "Dolmolm", "Dolmolm", "Dolmolm", 2, true);
define_npc_type_data(NpcType.Dolmdarl, "Dolmdarl", "Dolmdarl", "Dolmdarl", 2, true);
define_npc_type_data(NpcType.Morfos, "Morfos", "Morfos", "Morfos", 2, true);
define_npc_type_data(NpcType.Recobox, "Recobox", "Recobox", "Recobox", 2, true);
define_npc_type_data(NpcType.Recon, "Recon", "Recon", "Recon", 2, true);
define_npc_type_data(NpcType.SinowZoa, "Sinow Zoa", "Sinow Zoa", "Sinow Zoa", 2, true);
define_npc_type_data(NpcType.SinowZele, "Sinow Zele", "Sinow Zele", "Sinow Zele", 2, true);
define_npc_type_data(NpcType.OlgaFlow, "Olga Flow", "Olga Flow", "Olga Flow", 2, true);
// Episode IV
define_npc_type_data(
NpcType.SandRappy,
"Sand Rappy",
"Sand Rappy",
"Sand Rappy",
4,
true,
NpcType.DelRappy,
);
define_npc_type_data(NpcType.DelRappy, "Del Rappy", "Del Rappy", "Del Rappy", 4, true);
define_npc_type_data(NpcType.Astark, "Astark", "Astark", "Astark", 4, true);
define_npc_type_data(
NpcType.SatelliteLizard,
"Satellite Lizard",
"Satellite Lizard",
"Satellite Lizard",
4,
true,
);
define_npc_type_data(NpcType.Yowie, "Yowie", "Yowie", "Yowie", 4, true);
define_npc_type_data(
NpcType.MerissaA,
"Merissa A",
"Merissa A",
"Merissa A",
4,
true,
NpcType.MerissaAA,
);
define_npc_type_data(NpcType.MerissaAA, "Merissa AA", "Merissa AA", "Merissa AA", 4, true);
define_npc_type_data(NpcType.Girtablulu, "Girtablulu", "Girtablulu", "Girtablulu", 4, true);
define_npc_type_data(NpcType.Zu, "Zu", "Zu", "Zu", 4, true, NpcType.Pazuzu);
define_npc_type_data(NpcType.Pazuzu, "Pazuzu", "Pazuzu", "Pazuzu", 4, true);
define_npc_type_data(NpcType.Boota, "Boota", "Boota", "Boota", 4, true);
define_npc_type_data(NpcType.ZeBoota, "Ze Boota", "Ze Boota", "Ze Boota", 4, true);
define_npc_type_data(NpcType.BaBoota, "Ba Boota", "Ba Boota", "Ba Boota", 4, true);
define_npc_type_data(
NpcType.Dorphon,
"Dorphon",
"Dorphon",
"Dorphon",
4,
true,
NpcType.DorphonEclair,
);
define_npc_type_data(
NpcType.DorphonEclair,
"Dorphon Eclair",
"Dorphon Eclair",
"Dorphon Eclair",
4,
true,
);
define_npc_type_data(NpcType.Goran, "Goran", "Goran", "Goran", 4, true);
define_npc_type_data(NpcType.PyroGoran, "Pyro Goran", "Pyro Goran", "Pyro Goran", 4, true);
define_npc_type_data(
NpcType.GoranDetonator,
"Goran Detonator",
"Goran Detonator",
"Goran Detonator",
4,
true,
);
define_npc_type_data(
NpcType.SaintMilion,
"Saint-Milion",
"Saint-Milion",
"Saint-Milion",
4,
true,
NpcType.Kondrieu,
);
define_npc_type_data(
NpcType.Shambertin,
"Shambertin",
"Shambertin",
"Shambertin",
4,
true,
NpcType.Kondrieu,
);
define_npc_type_data(NpcType.Kondrieu, "Kondrieu", "Kondrieu", "Kondrieu", 4, true);

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { walk_qst_files } from "../../../../test/src/utils";
import { parse_qst, write_qst } from "./qst";
import { Endianness } from "../..";
import { Endianness } from "../../Endianness";
import { BufferCursor } from "../../cursor/BufferCursor";
import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor";

View File

@ -1,5 +1,5 @@
import Logger from "js-logger";
import { Endianness } from "../..";
import { Endianness } from "../../Endianness";
import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor";
import { Cursor } from "../../cursor/Cursor";
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";

View File

@ -1,5 +1,5 @@
import Logger from "js-logger";
import { Endianness } from "..";
import { Endianness } from "../Endianness";
import { Cursor } from "../cursor/Cursor";
import { parse_prc } from "./prc";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
import { action, computed, observable } from "mobx";
import { action, computed, IObservableArray, observable } from "mobx";
import { DatUnknown } from "../data_formats/parsing/quest/dat";
import { EntityType } from "../data_formats/parsing/quest/entities";
import { check_episode, Episode } from "../data_formats/parsing/quest/Episode";
import { Vec3 } from "../data_formats/vector";
import { enum_values } from "../enums";
import { Segment } from "../scripting/instructions";
import { ItemType } from "./items";
import { NpcType } from "./NpcType";
import { ObjectType } from "./ObjectType";
import { ObjectType } from "../data_formats/parsing/quest/object_types";
import { NpcType } from "../data_formats/parsing/quest/npc_types";
export * from "./items";
export * from "./NpcType";
export * from "./ObjectType";
export const RARE_ENEMY_PROB = 1 / 512;
export const KONDRIEU_PROB = 1 / 10;
@ -20,20 +20,6 @@ export enum Server {
export const Servers: Server[] = enum_values(Server);
export enum Episode {
I = 1,
II = 2,
IV = 4,
}
export const Episodes: Episode[] = enum_values(Episode);
export function check_episode(episode: Episode): void {
if (Episode[episode] == undefined) {
throw new Error(`Invalid episode ${episode}.`);
}
}
export enum SectionId {
Viridia,
Greenill,
@ -69,7 +55,7 @@ export class Section {
if (!Number.isInteger(id) || id < -1)
throw new Error(`Expected id to be an integer greater than or equal to -1, got ${id}.`);
if (!position) throw new Error("position is required.");
if (typeof y_axis_rotation !== "number") throw new Error("y_axis_rotation is required.");
if (!Number.isFinite(y_axis_rotation)) throw new Error("y_axis_rotation is required.");
this.id = id;
this.position = position;
@ -79,7 +65,7 @@ export class Section {
}
}
export class Quest {
export class ObservableQuest {
@observable private _id!: number;
get id(): number {
@ -145,9 +131,12 @@ export class Quest {
readonly episode: Episode;
@observable readonly area_variants: AreaVariant[];
@observable readonly objects: QuestObject[];
@observable readonly npcs: QuestNpc[];
/**
* One variant per area.
*/
@observable readonly area_variants: ObservableAreaVariant[];
@observable readonly objects: ObservableQuestObject[];
@observable readonly npcs: ObservableQuestNpc[];
/**
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
*/
@ -162,17 +151,17 @@ export class Quest {
short_description: string,
long_description: string,
episode: Episode,
area_variants: AreaVariant[],
objects: QuestObject[],
npcs: QuestNpc[],
area_variants: ObservableAreaVariant[],
objects: ObservableQuestObject[],
npcs: ObservableQuestNpc[],
dat_unknowns: DatUnknown[],
object_code: Segment[],
shop_items: number[]
shop_items: number[],
) {
check_episode(episode);
if (!area_variants) throw new Error("area_variants is required.");
if (!objects || !(objects instanceof Array)) throw new Error("objs is required.");
if (!npcs || !(npcs instanceof Array)) throw new Error("npcs is required.");
if (!Array.isArray(objects)) throw new Error("objs is required.");
if (!Array.isArray(npcs)) throw new Error("npcs is required.");
if (!dat_unknowns) throw new Error("dat_unknowns is required.");
if (!object_code) throw new Error("object_code is required.");
if (!shop_items) throw new Error("shop_items is required.");
@ -192,21 +181,15 @@ export class Quest {
}
}
export interface EntityType {
readonly id: number;
readonly code: string;
readonly name: string;
}
/**
* Abstract class from which QuestNpc and QuestObject derive.
* Abstract class from which ObservableQuestNpc and ObservableQuestObject derive.
*/
export abstract class QuestEntity<Type extends EntityType = EntityType> {
export abstract class ObservableQuestEntity<Type extends EntityType = EntityType> {
readonly type: Type;
@observable area_id: number;
private _section_id: number;
private readonly _section_id: number;
@computed get section_id(): number {
return this.section ? this.section.id : this._section_id;
@ -260,15 +243,15 @@ export abstract class QuestEntity<Type extends EntityType = EntityType> {
}
}
constructor(
protected constructor(
type: Type,
area_id: number,
section_id: number,
position: Vec3,
rotation: Vec3,
scale: Vec3
scale: Vec3,
) {
if (!type) throw new Error("type is required.");
if (type == undefined) throw new Error("type is required.");
if (!Number.isInteger(area_id) || area_id < 0)
throw new Error(`Expected area_id to be a non-negative integer, got ${area_id}.`);
if (!Number.isInteger(section_id) || section_id < 0)
@ -292,7 +275,7 @@ export abstract class QuestEntity<Type extends EntityType = EntityType> {
}
}
export class QuestObject extends QuestEntity<ObjectType> {
export class ObservableQuestObject extends ObservableQuestEntity<ObjectType> {
@observable type: ObjectType;
/**
* Data of which the purpose hasn't been discovered yet.
@ -306,7 +289,7 @@ export class QuestObject extends QuestEntity<ObjectType> {
position: Vec3,
rotation: Vec3,
scale: Vec3,
unknown: number[][]
unknown: number[][],
) {
super(type, area_id, section_id, position, rotation, scale);
@ -315,7 +298,7 @@ export class QuestObject extends QuestEntity<ObjectType> {
}
}
export class QuestNpc extends QuestEntity<NpcType> {
export class ObservableQuestNpc extends ObservableQuestEntity<NpcType> {
@observable type: NpcType;
pso_type_id: number;
pso_skin: number;
@ -333,7 +316,7 @@ export class QuestNpc extends QuestEntity<NpcType> {
position: Vec3,
rotation: Vec3,
scale: Vec3,
unknown: number[][]
unknown: number[][],
) {
super(type, area_id, section_id, position, rotation, scale);
@ -344,13 +327,16 @@ export class QuestNpc extends QuestEntity<NpcType> {
}
}
export class Area {
id: number;
name: string;
order: number;
area_variants: AreaVariant[];
export class ObservableArea {
/**
* Matches the PSO ID.
*/
readonly id: number;
readonly name: string;
readonly order: number;
readonly area_variants: ObservableAreaVariant[];
constructor(id: number, name: string, order: number, area_variants: AreaVariant[]) {
constructor(id: number, name: string, order: number, area_variants: ObservableAreaVariant[]) {
if (!Number.isInteger(id) || id < 0)
throw new Error(`Expected id to be a non-negative integer, got ${id}.`);
if (!name) throw new Error("name is required.");
@ -363,12 +349,17 @@ export class Area {
}
}
export class AreaVariant {
@observable.shallow sections: Section[] = [];
export class ObservableAreaVariant {
readonly id: number;
readonly area: ObservableArea;
@observable.shallow readonly sections: IObservableArray<Section> = observable.array();
constructor(public id: number, public area: Area) {
constructor(id: number, area: ObservableArea) {
if (!Number.isInteger(id) || id < 0)
throw new Error(`Expected id to be a non-negative integer, got ${id}.`);
this.id = id;
this.area = area;
}
}
@ -387,7 +378,7 @@ export class EnemyDrop implements ItemDrop {
readonly npc_type: NpcType,
readonly item_type: ItemType,
readonly anything_rate: number,
readonly rare_rate: number
readonly rare_rate: number,
) {
this.rate = anything_rate * rare_rate;
}
@ -429,10 +420,10 @@ export class HuntMethod {
export class SimpleQuest {
constructor(
public readonly id: number,
public readonly name: string,
public readonly episode: Episode,
public readonly enemy_counts: Map<NpcType, number>
readonly id: number,
readonly name: string,
readonly episode: Episode,
readonly enemy_counts: Map<NpcType, number>,
) {
if (!id) throw new Error("id is required.");
if (!name) throw new Error("name is required.");
@ -442,13 +433,13 @@ export class SimpleQuest {
export class PlayerModel {
constructor(
public readonly name: string,
public readonly head_style_count: number,
public readonly hair_styles_count: number,
public readonly hair_styles_with_accessory: Set<number>
readonly name: string,
readonly head_style_count: number,
readonly hair_styles_count: number,
readonly hair_styles_with_accessory: Set<number>,
) {}
}
export class PlayerAnimation {
constructor(public readonly id: number, public readonly name: string) {}
constructor(readonly id: number, readonly name: string) {}
}

View File

@ -1,5 +1,5 @@
import { Object3D } from "three";
import { Endianness } from "../data_formats";
import { Endianness } from "../data_formats/Endianness";
import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor";
import { parse_area_collision_geometry } from "../data_formats/parsing/area_collision_geometry";
import { parse_area_geometry } from "../data_formats/parsing/area_geometry";
@ -20,46 +20,46 @@ const collision_geometry_cache = new LoadingCache<string, Promise<Object3D>>();
export async function load_area_sections(
episode: number,
area_id: number,
area_variant: number
area_variant: number,
): Promise<Section[]> {
return render_geometry_cache.get_or_set(`${episode}-${area_id}-${area_variant}`, () =>
load_area_sections_and_render_geometry(episode, area_id, area_variant)
load_area_sections_and_render_geometry(episode, area_id, area_variant),
).sections;
}
export async function load_area_render_geometry(
episode: number,
area_id: number,
area_variant: number
area_variant: number,
): Promise<Object3D> {
return render_geometry_cache.get_or_set(`${episode}-${area_id}-${area_variant}`, () =>
load_area_sections_and_render_geometry(episode, area_id, area_variant)
load_area_sections_and_render_geometry(episode, area_id, area_variant),
).geometry;
}
export async function load_area_collision_geometry(
episode: number,
area_id: number,
area_variant: number
area_variant: number,
): Promise<Object3D> {
return collision_geometry_cache.get_or_set(`${episode}-${area_id}-${area_variant}`, () =>
get_area_asset(episode, area_id, area_variant, "collision").then(buffer =>
area_collision_geometry_to_object_3d(
parse_area_collision_geometry(new ArrayBufferCursor(buffer, Endianness.Little))
)
)
parse_area_collision_geometry(new ArrayBufferCursor(buffer, Endianness.Little)),
),
),
);
}
function load_area_sections_and_render_geometry(
episode: number,
area_id: number,
area_variant: number
area_variant: number,
): { geometry: Promise<Object3D>; sections: Promise<Section[]> } {
const promise = get_area_asset(episode, area_id, area_variant, "render").then(buffer =>
area_geometry_to_sections_and_object_3d(
parse_area_geometry(new ArrayBufferCursor(buffer, Endianness.Little))
)
parse_area_geometry(new ArrayBufferCursor(buffer, Endianness.Little)),
),
);
return {
@ -127,7 +127,7 @@ async function get_area_asset(
episode: number,
area_id: number,
area_variant: number,
type: "render" | "collision"
type: "render" | "collision",
): Promise<ArrayBuffer> {
const base_url = area_version_to_base_url(episode, area_id, area_variant);
const suffix = type === "render" ? "n.rel" : "c.rel";
@ -153,7 +153,7 @@ function area_version_to_base_url(episode: number, area_id: number, area_variant
return `/maps/map_${base_name}${variant}`;
} else {
throw new Error(
`Unknown variant ${area_variant} of area ${area_id} in episode ${episode}.`
`Unknown variant ${area_variant} of area ${area_id} in episode ${episode}.`,
);
}
} else {

View File

@ -1,14 +1,15 @@
import { Texture, CylinderBufferGeometry, BufferGeometry } from "three";
import { ObjectType, NpcType } from "../domain";
import Logger from "js-logger";
import { LoadingCache } from "./LoadingCache";
import { Endianness } from "../data_formats";
import { Endianness } from "../data_formats/Endianness";
import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor";
import { ninja_object_to_buffer_geometry } from "../rendering/conversion/ninja_geometry";
import { parse_nj, parse_xj } from "../data_formats/parsing/ninja";
import { parse_xvm } from "../data_formats/parsing/ninja/texture";
import { xvm_to_textures } from "../rendering/conversion/ninja_textures";
import { load_array_buffer } from "./load_array_buffer";
import { object_data, ObjectType } from "../data_formats/parsing/quest/object_types";
import { NpcType } from "../data_formats/parsing/quest/npc_types";
const logger = Logger.get("loading/entities");
@ -16,13 +17,13 @@ const DEFAULT_ENTITY = new CylinderBufferGeometry(3, 3, 20);
DEFAULT_ENTITY.translate(0, 10, 0);
const DEFAULT_ENTITY_PROMISE: Promise<BufferGeometry> = new Promise(resolve =>
resolve(DEFAULT_ENTITY)
resolve(DEFAULT_ENTITY),
);
const DEFAULT_ENTITY_TEX: Texture[] = [];
const DEFAULT_ENTITY_TEX_PROMISE: Promise<Texture[]> = new Promise(resolve =>
resolve(DEFAULT_ENTITY_TEX)
resolve(DEFAULT_ENTITY_TEX),
);
const npc_cache = new LoadingCache<NpcType, Promise<BufferGeometry>>();
@ -69,11 +70,11 @@ export async function load_npc_geometry(npc_type: NpcType): Promise<BufferGeomet
if (nj_objects.length) {
return ninja_object_to_buffer_geometry(nj_objects[0]);
} else {
logger.warn(`Couldn't parse ${url} for ${npc_type.code}.`);
logger.warn(`Couldn't parse ${url} for ${NpcType[npc_type]}.`);
return DEFAULT_ENTITY;
}
} catch (e) {
logger.warn(`Couldn't load geometry file for ${npc_type.code}.`, e);
logger.warn(`Couldn't load geometry file for ${NpcType[npc_type]}.`, e);
return DEFAULT_ENTITY;
}
});
@ -87,7 +88,7 @@ export async function load_npc_tex(npc_type: NpcType): Promise<Texture[]> {
const xvm = parse_xvm(cursor);
return xvm_to_textures(xvm);
} catch (e) {
logger.warn(`Couldn't load texture file for ${npc_type.code}.`, e);
logger.warn(`Couldn't load texture file for ${NpcType[npc_type]}.`, e);
return DEFAULT_ENTITY_TEX;
}
});
@ -103,11 +104,11 @@ export async function load_object_geometry(object_type: ObjectType): Promise<Buf
if (nj_objects.length) {
return ninja_object_to_buffer_geometry(nj_objects[0]);
} else {
logger.warn(`Couldn't parse ${url} for ${object_type.name}.`);
logger.warn(`Couldn't parse ${url} for ${ObjectType[object_type]}.`);
return DEFAULT_ENTITY;
}
} catch (e) {
logger.warn(`Couldn't load geometry file for ${object_type.name}.`, e);
logger.warn(`Couldn't load geometry file for ${ObjectType[object_type]}.`, e);
return DEFAULT_ENTITY;
}
});
@ -121,7 +122,7 @@ export async function load_object_tex(object_type: ObjectType): Promise<Texture[
const xvm = parse_xvm(cursor);
return xvm_to_textures(xvm);
} catch (e) {
logger.warn(`Couldn't load texture file for ${object_type.name}.`, e);
logger.warn(`Couldn't load texture file for ${ObjectType[object_type]}.`, e);
return DEFAULT_ENTITY_TEX;
}
});
@ -129,7 +130,7 @@ export async function load_object_tex(object_type: ObjectType): Promise<Texture[
export async function load_npc_data(
npc_type: NpcType,
type: AssetType
type: AssetType,
): Promise<{ url: string; data: ArrayBuffer }> {
const url = npc_type_to_url(npc_type, type);
const data = await load_array_buffer(url);
@ -138,7 +139,7 @@ export async function load_npc_data(
export async function load_object_data(
object_type: ObjectType,
type: AssetType
type: AssetType,
): Promise<{ url: string; data: ArrayBuffer }> {
const url = object_type_to_url(object_type, type);
const data = await load_array_buffer(url);
@ -154,7 +155,7 @@ function npc_type_to_url(npc_type: NpcType, type: AssetType): string {
switch (npc_type) {
// The dubswitch model is in XJ format.
case NpcType.Dubswitch:
return `/npcs/${npc_type.code}.${type === AssetType.Geometry ? "xj" : "xvm"}`;
return `/npcs/${NpcType[npc_type]}.${type === AssetType.Geometry ? "xj" : "xvm"}`;
// Episode II VR Temple
@ -203,7 +204,7 @@ function npc_type_to_url(npc_type: NpcType, type: AssetType): string {
return npc_type_to_url(NpcType.ChaosSorcerer, type);
default:
return `/npcs/${npc_type.code}.${type === AssetType.Geometry ? "nj" : "xvm"}`;
return `/npcs/${NpcType[npc_type]}.${type === AssetType.Geometry ? "nj" : "xvm"}`;
}
}
@ -226,12 +227,12 @@ function object_type_to_url(object_type: ObjectType, type: AssetType): string {
case ObjectType.FallingRock:
case ObjectType.DesertFixedTypeBoxBreakableCrystals:
case ObjectType.BeeHive:
return `/objects/${object_type.pso_id}.nj`;
return `/objects/${object_data(object_type).pso_id}.nj`;
default:
return `/objects/${object_type.pso_id}.xj`;
return `/objects/${object_data(object_type).pso_id}.xj`;
}
} else {
return `/objects/${object_type.pso_id}.xvm`;
return `/objects/${object_data(object_type).pso_id}.xvm`;
}
}

View File

@ -1,16 +1,16 @@
import { autorun } from "mobx";
import { Intersection, Mesh, MeshLambertMaterial, Plane, Raycaster, Vector2, Vector3 } from "three";
import { Vec3 } from "../data_formats/vector";
import { QuestEntity, QuestNpc, Section } from "../domain";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { AreaUserData } from "./conversion/areas";
import { ColorType, EntityUserData, NPC_COLORS, OBJECT_COLORS } from "./conversion/entities";
import { QuestRenderer } from "./QuestRenderer";
import { ObservableQuestEntity, ObservableQuestNpc, Section } from "../domain";
const DOWN_VECTOR = new Vector3(0, -1, 0);
type Highlighted = {
entity: QuestEntity;
entity: ObservableQuestEntity;
mesh: Mesh;
};
@ -22,7 +22,7 @@ type Pick = {
};
type PickResult = Pick & {
entity: QuestEntity;
entity: ObservableQuestEntity;
mesh: Mesh;
};
@ -203,7 +203,7 @@ export class QuestEntityControls {
private translate_vertically(
selection: Highlighted,
pick: Pick,
pointer_position: Vector2
pointer_position: Vector2,
): void {
// We intersect with a plane that's oriented toward the camera and that's coplanar with the point where the entity was grabbed.
this.raycaster.setFromCamera(pointer_position, this.renderer.camera);
@ -212,7 +212,7 @@ export class QuestEntityControls {
const negative_world_dir = this.renderer.camera.getWorldDirection(new Vector3()).negate();
const plane = new Plane().setFromNormalAndCoplanarPoint(
new Vector3(negative_world_dir.x, 0, negative_world_dir.z).normalize(),
selection.mesh.position.sub(pick.grab_offset)
selection.mesh.position.sub(pick.grab_offset),
);
const intersection_point = new Vector3();
@ -225,7 +225,7 @@ export class QuestEntityControls {
selection.entity.position = new Vec3(
selection.entity.position.x,
y,
selection.entity.position.z
selection.entity.position.z,
);
}
}
@ -233,7 +233,7 @@ export class QuestEntityControls {
private translate_horizontally(
selection: Highlighted,
pick: Pick,
pointer_position: Vector2
pointer_position: Vector2,
): void {
// Cast ray adjusted for dragging entities.
const { intersection, section } = this.pick_terrain(pointer_position, pick);
@ -243,9 +243,9 @@ export class QuestEntityControls {
new Vec3(
intersection.point.x,
intersection.point.y + pick.drag_y,
intersection.point.z
intersection.point.z,
),
section
section,
);
} else {
// If the cursor is not over any terrain, we translate the entity accross the horizontal plane in which the entity's origin lies.
@ -254,7 +254,7 @@ export class QuestEntityControls {
// ray.origin.add(data.dragAdjust);
const plane = new Plane(
new Vector3(0, 1, 0),
-selection.entity.position.y + pick.grab_offset.y
-selection.entity.position.y + pick.grab_offset.y,
);
const intersection_point = new Vector3();
@ -262,7 +262,7 @@ export class QuestEntityControls {
selection.entity.position = new Vec3(
intersection_point.x + pick.grab_offset.x,
selection.entity.position.y,
intersection_point.z + pick.grab_offset.z
intersection_point.z + pick.grab_offset.z,
);
}
}
@ -274,7 +274,7 @@ export class QuestEntityControls {
quest_editor_store.push_entity_move_action(
entity,
this.pick.initial_position,
entity.position
entity.position,
);
}
@ -288,7 +288,7 @@ export class QuestEntityControls {
// Find the nearest object and NPC under the pointer.
this.raycaster.setFromCamera(pointer_position, this.renderer.camera);
const [intersection] = this.raycaster.intersectObjects(
this.renderer.entity_models.children
this.renderer.entity_models.children,
);
if (!intersection) {
@ -307,7 +307,7 @@ export class QuestEntityControls {
this.raycaster.set(intersection.object.position, DOWN_VECTOR);
const [collision_geom_intersection] = this.raycaster.intersectObjects(
this.renderer.collision_geometry.children,
true
true,
);
if (collision_geom_intersection) {
@ -330,7 +330,7 @@ export class QuestEntityControls {
*/
private pick_terrain(
pointer_pos: Vector2,
data: Pick
data: Pick,
): {
intersection?: Intersection;
section?: Section;
@ -339,7 +339,7 @@ export class QuestEntityControls {
this.raycaster.ray.origin.add(data.drag_adjust);
const intersections = this.raycaster.intersectObjects(
this.renderer.collision_geometry.children,
true
true,
);
// Don't allow entities to be placed on very steep terrain.
@ -359,7 +359,7 @@ export class QuestEntityControls {
}
function set_color({ entity, mesh }: Highlighted, type: ColorType): void {
const color = entity instanceof QuestNpc ? NPC_COLORS[type] : OBJECT_COLORS[type];
const color = entity instanceof ObservableQuestNpc ? NPC_COLORS[type] : OBJECT_COLORS[type];
if (mesh) {
if (Array.isArray(mesh.material)) {

View File

@ -1,7 +1,6 @@
import Logger from "js-logger";
import { autorun, IReactionDisposer } from "mobx";
import { Mesh, Object3D, Vector3, Raycaster, Intersection } from "three";
import { Area, Quest, QuestEntity } from "../domain";
import { load_area_collision_geometry, load_area_render_geometry } from "../loading/areas";
import {
load_npc_geometry,
@ -12,6 +11,7 @@ import {
import { create_npc_mesh, create_object_mesh } from "./conversion/entities";
import { QuestRenderer } from "./QuestRenderer";
import { AreaUserData } from "./conversion/areas";
import { ObservableArea, ObservableQuest, ObservableQuestEntity } from "../domain";
const logger = Logger.get("rendering/QuestModelManager");
@ -20,13 +20,13 @@ const CAMERA_LOOKAT = new Vector3(0, 0, 0);
const DUMMY_OBJECT = new Object3D();
export class QuestModelManager {
private quest?: Quest;
private area?: Area;
private quest?: ObservableQuest;
private area?: ObservableArea;
private entity_reaction_disposers: IReactionDisposer[] = [];
constructor(private renderer: QuestRenderer) {}
async load_models(quest?: Quest, area?: Area): Promise<void> {
async load_models(quest?: ObservableQuest, area?: ObservableArea): Promise<void> {
if (this.quest === quest && this.area === area) {
return;
}
@ -47,13 +47,13 @@ export class QuestModelManager {
const collision_geometry = await load_area_collision_geometry(
episode,
area_id,
variant_id
variant_id,
);
const render_geometry = await load_area_render_geometry(
episode,
area_id,
variant_id
variant_id,
);
this.add_sections_to_collision_geometry(collision_geometry, render_geometry);
@ -106,7 +106,7 @@ export class QuestModelManager {
private add_sections_to_collision_geometry(
collision_geom: Object3D,
render_geom: Object3D
render_geom: Object3D,
): void {
const raycaster = new Raycaster();
const origin = new Vector3();
@ -145,7 +145,7 @@ export class QuestModelManager {
}
}
private update_entity_geometry(entity: QuestEntity, model: Mesh): void {
private update_entity_geometry(entity: ObservableQuestEntity, model: Mesh): void {
this.renderer.add_entity_model(model);
this.entity_reaction_disposers.push(
@ -155,7 +155,7 @@ export class QuestModelManager {
const rot = entity.rotation;
model.rotation.set(rot.x, rot.y, rot.z);
this.renderer.schedule_render();
})
}),
);
}

View File

@ -1,11 +1,11 @@
import { autorun } from "mobx";
import { Mesh, Object3D, PerspectiveCamera, Group } from "three";
import { QuestEntity } from "../domain";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { QuestEntityControls } from "./QuestEntityControls";
import { QuestModelManager } from "./QuestModelManager";
import { Renderer } from "./Renderer";
import { EntityUserData } from "./conversion/entities";
import { ObservableQuestEntity } from "../domain";
let renderer: QuestRenderer | undefined;
@ -58,7 +58,7 @@ export class QuestRenderer extends Renderer<PerspectiveCamera> {
return this._entity_models;
}
private entity_to_mesh = new Map<QuestEntity, Mesh>();
private entity_to_mesh = new Map<ObservableQuestEntity, Mesh>();
private entity_controls: QuestEntityControls;
constructor() {
@ -69,7 +69,7 @@ export class QuestRenderer extends Renderer<PerspectiveCamera> {
autorun(() => {
model_manager.load_models(
quest_editor_store.current_quest,
quest_editor_store.current_area
quest_editor_store.current_area,
);
});
@ -103,7 +103,7 @@ export class QuestRenderer extends Renderer<PerspectiveCamera> {
}
}
get_entity_mesh(entity: QuestEntity): Mesh | undefined {
get_entity_mesh(entity: ObservableQuestEntity): Mesh | undefined {
return this.entity_to_mesh.get(entity);
}
}

View File

@ -1,6 +1,8 @@
import { BufferGeometry, DoubleSide, Mesh, MeshLambertMaterial, Texture } from "three";
import { QuestEntity, QuestNpc, QuestObject } from "../../domain";
import { create_mesh } from "./create_mesh";
import { ObservableQuestEntity, ObservableQuestNpc, ObservableQuestObject } from "../../domain";
import { ObjectType } from "../../data_formats/parsing/quest/object_types";
import { NpcType } from "../../data_formats/parsing/quest/npc_types";
export enum ColorType {
Normal,
@ -19,31 +21,37 @@ NPC_COLORS[ColorType.Hovered] = 0xff3f5f;
NPC_COLORS[ColorType.Selected] = 0xff0054;
export type EntityUserData = {
entity: QuestEntity;
entity: ObservableQuestEntity;
};
export function create_object_mesh(
object: QuestObject,
object: ObservableQuestObject,
geometry: BufferGeometry,
textures: Texture[]
textures: Texture[],
): Mesh {
return create(object, geometry, textures, OBJECT_COLORS[ColorType.Normal], object.type.name);
return create(
object,
geometry,
textures,
OBJECT_COLORS[ColorType.Normal],
ObjectType[object.type],
);
}
export function create_npc_mesh(
npc: QuestNpc,
npc: ObservableQuestNpc,
geometry: BufferGeometry,
textures: Texture[]
textures: Texture[],
): Mesh {
return create(npc, geometry, textures, NPC_COLORS[ColorType.Normal], npc.type.code);
return create(npc, geometry, textures, NPC_COLORS[ColorType.Normal], NpcType[npc.type]);
}
function create(
entity: QuestEntity,
entity: ObservableQuestEntity,
geometry: BufferGeometry,
textures: Texture[],
color: number,
name: string
name: string,
): Mesh {
const default_material = new MeshLambertMaterial({
color,
@ -59,10 +67,10 @@ function create(
map: tex,
side: DoubleSide,
alphaTest: 0.5,
})
}),
)
: default_material,
default_material
default_material,
);
mesh.name = name;

View File

@ -1,5 +1,5 @@
import { readFileSync } from "fs";
import { Endianness } from "../data_formats";
import { Endianness } from "../data_formats/Endianness";
import { prs_decompress } from "../data_formats/compression/prs/decompress";
import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor";
import { BufferCursor } from "../data_formats/cursor/BufferCursor";

View File

@ -1,90 +1,48 @@
import { Area, AreaVariant, Section, Episode } from "../domain";
import { ObservableArea, ObservableAreaVariant, Section } from "../domain";
import { load_area_sections } from "../loading/areas";
function area(id: number, name: string, order: number, variants: number): Area {
const area = new Area(id, name, order, []);
const varis = Array(variants)
.fill(null)
.map((_, i) => new AreaVariant(i, area));
area.area_variants.splice(0, 0, ...varis);
return area;
}
import { Episode, EPISODES } from "../data_formats/parsing/quest/Episode";
import { get_areas_for_episode } from "../data_formats/parsing/quest/areas";
class AreaStore {
private areas: Area[][] = [];
private readonly areas: ObservableArea[][] = [];
constructor() {
// The IDs match the PSO IDs for areas.
let order = 0;
this.areas[Episode.I] = [
area(0, "Pioneer II", order++, 1),
area(1, "Forest 1", order++, 1),
area(2, "Forest 2", order++, 1),
area(11, "Under the Dome", order++, 1),
area(3, "Cave 1", order++, 6),
area(4, "Cave 2", order++, 5),
area(5, "Cave 3", order++, 6),
area(12, "Underground Channel", order++, 1),
area(6, "Mine 1", order++, 6),
area(7, "Mine 2", order++, 6),
area(13, "Monitor Room", order++, 1),
area(8, "Ruins 1", order++, 5),
area(9, "Ruins 2", order++, 5),
area(10, "Ruins 3", order++, 5),
area(14, "Dark Falz", order++, 1),
area(15, "BA Ruins", order++, 3),
area(16, "BA Spaceship", order++, 3),
area(17, "Lobby", order++, 15),
];
order = 0;
this.areas[Episode.II] = [
area(0, "Lab", order++, 1),
area(1, "VR Temple Alpha", order++, 3),
area(2, "VR Temple Beta", order++, 3),
area(14, "VR Temple Final", order++, 1),
area(3, "VR Spaceship Alpha", order++, 3),
area(4, "VR Spaceship Beta", order++, 3),
area(15, "VR Spaceship Final", order++, 1),
area(5, "Central Control Area", order++, 1),
area(6, "Jungle Area East", order++, 1),
area(7, "Jungle Area North", order++, 1),
area(8, "Mountain Area", order++, 3),
area(9, "Seaside Area", order++, 1),
area(12, "Cliffs of Gal Da Val", order++, 1),
area(10, "Seabed Upper Levels", order++, 3),
area(11, "Seabed Lower Levels", order++, 3),
area(13, "Test Subject Disposal Area", order++, 1),
area(16, "Seaside Area at Night", order++, 1),
area(17, "Control Tower", order++, 5),
];
order = 0;
this.areas[Episode.IV] = [
area(0, "Pioneer II (Ep. IV)", order++, 1),
area(1, "Crater Route 1", order++, 1),
area(2, "Crater Route 2", order++, 1),
area(3, "Crater Route 3", order++, 1),
area(4, "Crater Route 4", order++, 1),
area(5, "Crater Interior", order++, 1),
area(6, "Subterranean Desert 1", order++, 3),
area(7, "Subterranean Desert 2", order++, 3),
area(8, "Subterranean Desert 3", order++, 3),
area(9, "Meteor Impact Site", order++, 1),
];
for (const episode of EPISODES) {
this.areas[episode] = get_areas_for_episode(episode).map(area => {
const observable_area = new ObservableArea(area.id, area.name, area.order, []);
for (const variant of area.area_variants) {
observable_area.area_variants.push(
new ObservableAreaVariant(variant.id, observable_area),
);
}
get_area = (episode: Episode, area_id: number): Area => {
return observable_area;
});
}
}
get_areas_for_episode = (episode: Episode): ObservableArea[] => {
return this.areas[episode];
};
get_area = (episode: Episode, area_id: number): ObservableArea => {
const area = this.areas[episode].find(a => a.id === area_id);
if (!area) throw new Error(`Area id ${area_id} for episode ${episode} is invalid.`);
return area;
};
get_variant = (episode: Episode, area_id: number, variant_id: number): AreaVariant => {
get_variant = (
episode: Episode,
area_id: number,
variant_id: number,
): ObservableAreaVariant => {
const area = this.get_area(episode, area_id);
const area_variant = area.area_variants[variant_id];
if (!area_variant)
throw new Error(
`Area variant id ${variant_id} for area ${area_id} of episode ${episode} is invalid.`
`Area variant id ${variant_id} for area ${area_id} of episode ${episode} is invalid.`,
);
return area_variant;
@ -93,7 +51,7 @@ class AreaStore {
get_area_sections = (
episode: Episode,
area_id: number,
variant_id: number
variant_id: number,
): Promise<Section[]> => {
return load_area_sections(episode, area_id, variant_id);
};

View File

@ -1,23 +1,24 @@
import Logger from "js-logger";
import { autorun, IReactionDisposer, observable } from "mobx";
import { HuntMethod, NpcType, Server, SimpleQuest } from "../domain";
import { HuntMethod, Server, SimpleQuest } from "../domain";
import { QuestDto } from "../dto";
import { Loadable } from "../Loadable";
import { hunt_method_persister } from "../persistence/HuntMethodPersister";
import { ServerMap } from "./ServerMap";
import { NpcType } from "../data_formats/parsing/quest/npc_types";
const logger = Logger.get("stores/HuntMethodStore");
class HuntMethodStore {
@observable methods: ServerMap<Loadable<HuntMethod[]>> = new ServerMap(
server => new Loadable([], () => this.load_hunt_methods(server))
server => new Loadable([], () => this.load_hunt_methods(server)),
);
private storage_disposer?: IReactionDisposer;
private async load_hunt_methods(server: Server): Promise<HuntMethod[]> {
const response = await fetch(
`${process.env.PUBLIC_URL}/quests.${Server[server].toLowerCase()}.json`
`${process.env.PUBLIC_URL}/quests.${Server[server].toLowerCase()}.json`,
);
const quests = (await response.json()) as QuestDto[];
const methods = new Array<HuntMethod>();
@ -27,7 +28,7 @@ class HuntMethodStore {
const enemy_counts = new Map<NpcType, number>();
for (const [code, count] of Object.entries(quest.enemyCounts)) {
const npc_type = NpcType.by_code(code);
const npc_type = (NpcType as any)[code];
if (!npc_type) {
logger.error(`No NpcType found for code ${code}.`);
@ -59,8 +60,8 @@ class HuntMethodStore {
`q${quest.id}`,
quest.name,
new SimpleQuest(quest.id, quest.name, quest.episode, enemy_counts),
/^\d-\d.*/.test(quest.name) ? 0.75 : total_count > 400 ? 0.75 : 0.5
)
/^\d-\d.*/.test(quest.name) ? 0.75 : total_count > 400 ? 0.75 : 0.5,
),
);
}

View File

@ -3,11 +3,9 @@ import { autorun, computed, IObservableArray, observable } from "mobx";
import {
Difficulties,
Difficulty,
Episode,
HuntMethod,
ItemType,
KONDRIEU_PROB,
NpcType,
RARE_ENEMY_PROB,
SectionId,
SectionIds,
@ -17,6 +15,8 @@ import { application_store } from "./ApplicationStore";
import { hunt_method_store } from "./HuntMethodStore";
import { item_drop_stores } from "./ItemDropStore";
import { item_type_stores } from "./ItemTypeStore";
import { Episode } from "../data_formats/parsing/quest/Episode";
import { npc_data, NpcType } from "../data_formats/parsing/quest/npc_types";
export class WantedItem {
@observable readonly item_type: ItemType;
@ -55,7 +55,7 @@ export class OptimalMethod {
method_episode: Episode,
method_time: number,
runs: number,
item_counts: Map<ItemType, number>
item_counts: Map<ItemType, number>,
) {
this.difficulty = difficulty;
this.section_ids = section_ids;
@ -79,7 +79,7 @@ class HuntOptimizerStore {
@computed get huntable_item_types(): ItemType[] {
const item_drop_store = item_drop_stores.current.value;
return item_type_stores.current.value.item_types.filter(
i => item_drop_store.enemy_drops.get_drops_for_item_type(i.id).length
i => item_drop_store.enemy_drops.get_drops_for_item_type(i.id).length,
);
}
@ -100,7 +100,7 @@ class HuntOptimizerStore {
// Initialize this set before awaiting data, so user changes don't affect this optimization
// run from this point on.
const wanted_items = new Set(
this.wanted_items.filter(w => w.amount > 0).map(w => w.item_type)
this.wanted_items.filter(w => w.amount > 0).map(w => w.item_type),
);
const methods = await hunt_method_store.methods.current.promise;
@ -136,11 +136,12 @@ class HuntOptimizerStore {
// Counts include rare enemies, so they are fractional.
const counts = new Map<NpcType, number>();
for (const [enemy, count] of method.enemy_counts.entries()) {
const old_count = counts.get(enemy) || 0;
for (const [enemy_type, count] of method.enemy_counts.entries()) {
const old_count = counts.get(enemy_type) || 0;
const enemy = npc_data(enemy_type);
if (enemy.rare_type == null) {
counts.set(enemy, old_count + count);
counts.set(enemy_type, old_count + count);
} else {
let rate, rare_rate;
@ -152,10 +153,10 @@ class HuntOptimizerStore {
rare_rate = RARE_ENEMY_PROB;
}
counts.set(enemy, old_count + count * rate);
counts.set(enemy_type, old_count + count * rate);
counts.set(
enemy.rare_type,
(counts.get(enemy.rare_type) || 0) + count * rare_rate
(counts.get(enemy.rare_type) || 0) + count * rare_rate,
);
}
}
@ -216,7 +217,7 @@ class HuntOptimizerStore {
difficulty,
section_id,
method,
split_pan_arms
split_pan_arms,
);
variables[name] = variable;
variable_details.set(name, {
@ -312,8 +313,8 @@ class HuntOptimizerStore {
method.episode,
method.time,
runs,
items
)
items,
),
);
}
}
@ -325,7 +326,7 @@ class HuntOptimizerStore {
difficulty: Difficulty,
section_id: SectionId,
method: HuntMethod,
split_pan_arms: boolean
split_pan_arms: boolean,
): string {
let name = `${difficulty}\t${section_id}\t${method.id}`;
if (split_pan_arms) name += "\tspa";
@ -334,13 +335,13 @@ class HuntOptimizerStore {
private initialize_persistence = async () => {
this.wanted_items.replace(
await hunt_optimizer_persister.load_wanted_items(application_store.current_server)
await hunt_optimizer_persister.load_wanted_items(application_store.current_server),
);
autorun(() => {
hunt_optimizer_persister.persist_wanted_items(
application_store.current_server,
this.wanted_items
this.wanted_items,
);
});
};

View File

@ -1,54 +1,47 @@
import { observable } from "mobx";
import {
Difficulties,
Difficulty,
EnemyDrop,
NpcType,
SectionId,
SectionIds,
Server,
} from "../domain";
import { NpcTypes } from "../domain/NpcType";
import { Difficulties, Difficulty, EnemyDrop, SectionId, SectionIds, Server } from "../domain";
import { EnemyDropDto } from "../dto";
import { Loadable } from "../Loadable";
import { item_type_stores } from "./ItemTypeStore";
import { ServerMap } from "./ServerMap";
import Logger from "js-logger";
import { NpcType } from "../data_formats/parsing/quest/npc_types";
const logger = Logger.get("stores/ItemDropStore");
export class EnemyDropTable {
// Mapping of difficulties to section IDs to NpcTypes to EnemyDrops.
private table: EnemyDrop[] = new Array(
Difficulties.length * SectionIds.length * NpcTypes.length
);
private table: EnemyDrop[][][] = [];
// Mapping of ItemType ids to EnemyDrops.
private item_type_to_drops: EnemyDrop[][] = [];
constructor() {
for (let i = 0; i < Difficulties.length; i++) {
const diff_array: EnemyDrop[][] = [];
this.table.push(diff_array);
for (let j = 0; j < SectionIds.length; j++) {
diff_array.push([]);
}
}
}
get_drop(
difficulty: Difficulty,
section_id: SectionId,
npc_type: NpcType
npc_type: NpcType,
): EnemyDrop | undefined {
return this.table[
difficulty * SectionIds.length * NpcTypes.length +
section_id * NpcTypes.length +
npc_type.id
];
return this.table[difficulty][section_id][npc_type];
}
set_drop(
difficulty: Difficulty,
section_id: SectionId,
npc_type: NpcType,
drop: EnemyDrop
drop: EnemyDrop,
): void {
this.table[
difficulty * SectionIds.length * NpcTypes.length +
section_id * NpcTypes.length +
npc_type.id
] = drop;
this.table[difficulty][section_id][npc_type] = drop;
let drops = this.item_type_to_drops[drop.item_type.id];
@ -77,18 +70,18 @@ export const item_drop_stores: ServerMap<Loadable<ItemDropStore>> = new ServerMa
async function load(store: ItemDropStore, server: Server): Promise<ItemDropStore> {
const item_type_store = await item_type_stores.current.promise;
const response = await fetch(
`${process.env.PUBLIC_URL}/enemyDrops.${Server[server].toLowerCase()}.json`
`${process.env.PUBLIC_URL}/enemyDrops.${Server[server].toLowerCase()}.json`,
);
const data: EnemyDropDto[] = await response.json();
const drops = new EnemyDropTable();
for (const drop_dto of data) {
const npc_type = NpcType.by_code(drop_dto.enemy);
const npc_type = (NpcType as any)[drop_dto.enemy];
if (!npc_type) {
logger.warn(
`Couldn't determine NpcType of episode ${drop_dto.episode} ${drop_dto.enemy}.`
`Couldn't determine NpcType of episode ${drop_dto.episode} ${drop_dto.enemy}.`,
);
continue;
}
@ -118,8 +111,8 @@ async function load(store: ItemDropStore, server: Server): Promise<ItemDropStore
npc_type,
item_type,
drop_dto.dropRate,
drop_dto.rareRate
)
drop_dto.rareRate,
),
);
}

View File

@ -11,7 +11,7 @@ import {
SkinnedMesh,
Texture,
} from "three";
import { Endianness } from "../data_formats";
import { Endianness } from "../data_formats/Endianness";
import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor";
import { NjModel, NjObject, parse_nj, parse_xj } from "../data_formats/parsing/ninja";
import { NjMotion, parse_njm } from "../data_formats/parsing/ninja/motion";

View File

@ -1,15 +1,24 @@
import Logger from "js-logger";
import { action, flow, observable } from "mobx";
import { Endianness } from "../data_formats";
import { Endianness } from "../data_formats/Endianness";
import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor";
import { parse_quest, write_quest_qst } from "../data_formats/parsing/quest";
import { Vec3 } from "../data_formats/vector";
import { Area, Episode, Quest, QuestEntity, Section } from "../domain";
import {
ObservableArea,
ObservableQuest,
ObservableQuestEntity,
Section,
ObservableQuestObject,
ObservableQuestNpc,
} from "../domain";
import { read_file } from "../read_file";
import { SimpleUndo, UndoStack, undo_manager } from "../undo";
import { application_store } from "./ApplicationStore";
import { area_store } from "./AreaStore";
import { create_new_quest } from "./quest_creation";
import { Episode } from "../data_formats/parsing/quest/Episode";
import { entity_data } from "../data_formats/parsing/quest/entities";
const logger = Logger.get("stores/QuestEditorStore");
@ -20,23 +29,23 @@ class QuestEditorStore {
readonly script_undo = new SimpleUndo("Text edits", () => {}, () => {});
@observable current_quest_filename?: string;
@observable current_quest?: Quest;
@observable current_area?: Area;
@observable current_quest?: ObservableQuest;
@observable current_area?: ObservableArea;
@observable selected_entity?: QuestEntity;
@observable selected_entity?: ObservableQuestEntity;
@observable save_dialog_filename?: string;
@observable save_dialog_open: boolean = false;
constructor() {
application_store.on_global_keyup("quest_editor", "Ctrl-Z", () => {
// Let Monaco handle its own keybindings.
// Let Monaco handle its own key bindings.
if (undo_manager.current !== this.script_undo) {
undo_manager.undo();
}
});
application_store.on_global_keyup("quest_editor", "Ctrl-Shift-Z", () => {
// Let Monaco handle its own keybindings.
// Let Monaco handle its own key bindings.
if (undo_manager.current !== this.script_undo) {
undo_manager.redo();
}
@ -50,7 +59,7 @@ class QuestEditorStore {
};
@action
set_selected_entity = (entity?: QuestEntity) => {
set_selected_entity = (entity?: ObservableQuestEntity) => {
if (entity) {
this.set_current_area_id(entity.area_id);
}
@ -65,10 +74,7 @@ class QuestEditorStore {
if (area_id == null) {
this.current_area = undefined;
} else if (this.current_quest) {
const area_variant = this.current_quest.area_variants.find(
variant => variant.area.id === area_id
);
this.current_area = area_variant && area_variant.area;
this.current_area = area_store.get_area(this.current_quest.episode, area_id);
}
};
@ -82,7 +88,50 @@ class QuestEditorStore {
try {
const buffer = yield read_file(file);
const quest = parse_quest(new ArrayBufferCursor(buffer, Endianness.Little));
this.set_quest(quest, filename);
this.set_quest(
quest &&
new ObservableQuest(
quest.id,
quest.language,
quest.name,
quest.short_description,
quest.long_description,
quest.episode,
quest.area_variants.map(variant =>
area_store.get_variant(quest.episode, variant.area.id, variant.id),
),
quest.objects.map(
obj =>
new ObservableQuestObject(
obj.type,
obj.area_id,
obj.section_id,
obj.position,
obj.rotation,
obj.scale,
obj.unknown,
),
),
quest.npcs.map(
npc =>
new ObservableQuestNpc(
npc.type,
npc.pso_type_id,
npc.pso_skin,
npc.area_id,
npc.section_id,
npc.position,
npc.rotation,
npc.scale,
npc.unknown,
),
),
quest.dat_unknowns,
quest.object_code,
quest.shop_items,
),
filename,
);
} catch (e) {
logger.error("Couldn't read file.", e);
}
@ -110,8 +159,44 @@ class QuestEditorStore {
};
save_current_quest_to_file = (file_name: string) => {
if (this.current_quest) {
const buffer = write_quest_qst(this.current_quest, file_name);
const quest = this.current_quest;
if (quest) {
const buffer = write_quest_qst(
{
id: quest.id,
language: quest.language,
name: quest.name,
short_description: quest.short_description,
long_description: quest.long_description,
episode: quest.episode,
objects: quest.objects.map(obj => ({
type: obj.type,
area_id: obj.area_id,
section_id: obj.section_id,
position: obj.section_position,
rotation: obj.rotation,
scale: obj.scale,
unknown: obj.unknown,
})),
npcs: quest.npcs.map(npc => ({
type: npc.type,
area_id: npc.area_id,
section_id: npc.section_id,
position: npc.section_position,
rotation: npc.rotation,
scale: npc.scale,
unknown: npc.unknown,
pso_type_id: npc.pso_type_id,
pso_skin: npc.pso_skin,
})),
dat_unknowns: quest.dat_unknowns,
object_code: quest.object_code,
shop_items: quest.shop_items,
area_variants: quest.area_variants,
},
file_name,
);
if (!file_name.endsWith(".qst")) {
file_name += ".qst";
@ -130,9 +215,13 @@ class QuestEditorStore {
};
@action
push_entity_move_action = (entity: QuestEntity, initial_position: Vec3, new_position: Vec3) => {
push_entity_move_action = (
entity: ObservableQuestEntity,
initial_position: Vec3,
new_position: Vec3,
) => {
this.undo.push_action(
`Move ${entity.type.name}`,
`Move ${entity_data(entity.type).name}`,
() => {
entity.position = initial_position;
quest_editor_store.set_selected_entity(entity);
@ -140,15 +229,15 @@ class QuestEditorStore {
() => {
entity.position = new_position;
quest_editor_store.set_selected_entity(entity);
}
},
);
};
@action
private set_quest = flow(function* set_quest(
this: QuestEditorStore,
quest?: Quest,
filename?: string
quest?: ObservableQuest,
filename?: string,
) {
this.current_quest_filename = filename;
@ -158,8 +247,8 @@ class QuestEditorStore {
this.selected_entity = undefined;
this.current_quest = quest;
if (quest && quest.area_variants.length) {
this.current_area = quest.area_variants[0].area;
if (quest) {
this.current_area = area_store.get_area(quest.episode, 0);
} else {
this.current_area = undefined;
}
@ -170,13 +259,13 @@ class QuestEditorStore {
const sections = yield area_store.get_area_sections(
quest.episode,
variant.area.id,
variant.id
variant.id,
);
variant.sections = sections;
variant.sections.replace(sections);
for (const object of quest.objects.filter(o => o.area_id === variant.area.id)) {
try {
this.set_section_on_visible_quest_entity(object, sections);
this.set_section_on_quest_entity(object, sections);
} catch (e) {
logger.error(e);
}
@ -184,7 +273,7 @@ class QuestEditorStore {
for (const npc of quest.npcs.filter(npc => npc.area_id === variant.area.id)) {
try {
this.set_section_on_visible_quest_entity(npc, sections);
this.set_section_on_quest_entity(npc, sections);
} catch (e) {
logger.error(e);
}
@ -196,7 +285,7 @@ class QuestEditorStore {
}
});
private set_section_on_visible_quest_entity = (entity: QuestEntity, sections: Section[]) => {
private set_section_on_quest_entity = (entity: ObservableQuestEntity, sections: Section[]) => {
let { x, y, z } = entity.position;
const section = sections.find(s => s.id === entity.section_id);

View File

@ -2,7 +2,7 @@ import { observable } from "mobx";
import { Xvm, parse_xvm } from "../data_formats/parsing/ninja/texture";
import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor";
import { read_file } from "../read_file";
import { Endianness } from "../data_formats";
import { Endianness } from "../data_formats/Endianness";
import Logger from "js-logger";
const logger = Logger.get("stores/TextureViewerStore");

View File

@ -1,13 +1,17 @@
import { Vec3 } from "../data_formats/vector";
import { Episode, NpcType, ObjectType, Quest, QuestNpc, QuestObject } from "../domain";
import { ObservableQuest, ObservableQuestNpc, ObservableQuestObject } from "../domain";
import { area_store } from "./AreaStore";
import { SegmentType, Instruction } from "../scripting/instructions";
import { Opcode } from "../scripting/opcodes";
import { Episode } from "../data_formats/parsing/quest/Episode";
import { ObjectType } from "../data_formats/parsing/quest/object_types";
import { NpcType } from "../data_formats/parsing/quest/npc_types";
export function create_new_quest(episode: Episode): Quest {
export function create_new_quest(episode: Episode): ObservableQuest {
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 Quest(
return new ObservableQuest(
0,
0,
"Untitled",
@ -72,13 +76,13 @@ export function create_new_quest(episode: Episode): Quest {
],
},
],
[]
[],
);
}
function create_default_objects(): QuestObject[] {
function create_default_objects(): ObservableQuestObject[] {
return [
new QuestObject(
new ObservableQuestObject(
ObjectType.MenuActivation,
0,
10,
@ -89,9 +93,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 0, 0, 0, 0, 0, 64, 0, 0],
[0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 75, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.MenuActivation,
0,
10,
@ -102,9 +106,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 1, 0, 0, 0, 1, 64, 0, 0],
[0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 76, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.MenuActivation,
0,
10,
@ -115,9 +119,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 2, 0, 0, 0, 2, 64, 0, 0],
[0, 0],
[2, 0, 0, 0, 0, 0, 1, 0, 10, 0, 0, 0, 192, 76, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.MenuActivation,
0,
10,
@ -128,9 +132,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 3, 0, 0, 0, 3, 64, 0, 0],
[0, 0],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 77, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.PlayerSet,
0,
10,
@ -141,9 +145,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 4, 0, 0, 0, 4, 64, 0, 0],
[0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.PlayerSet,
0,
10,
@ -154,9 +158,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 5, 0, 0, 0, 5, 64, 0, 0],
[0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 160, 78, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.PlayerSet,
0,
10,
@ -167,9 +171,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 6, 0, 0, 0, 6, 64, 0, 0],
[0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 79, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.PlayerSet,
0,
10,
@ -180,9 +184,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 7, 0, 0, 0, 7, 64, 0, 0],
[0, 0],
[0, 0, 1, 0, 10, 0, 0, 0, 0, 0, 0, 0, 224, 79, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.MainRagolTeleporter,
0,
10,
@ -193,9 +197,9 @@ function create_default_objects(): QuestObject[] {
[0, 0, 87, 7, 0, 0, 88, 71, 0, 0],
[0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 208, 128, 250, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.PrincipalWarp,
0,
10,
@ -206,9 +210,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 9, 0, 0, 0, 9, 64, 0, 0],
[0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 176, 81, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.MenuActivation,
0,
10,
@ -219,9 +223,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 10, 0, 0, 0, 10, 64, 0, 0],
[1, 0],
[4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 82, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.MenuActivation,
0,
10,
@ -232,9 +236,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 11, 0, 0, 0, 11, 64, 0, 0],
[0, 0],
[5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 83, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.PrincipalWarp,
0,
10,
@ -245,9 +249,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 12, 0, 0, 0, 12, 64, 0, 0],
[0, 0],
[0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 84, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.TelepipeLocation,
0,
10,
@ -258,9 +262,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 13, 0, 0, 0, 13, 64, 0, 0],
[0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 85, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.TelepipeLocation,
0,
10,
@ -271,9 +275,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 14, 0, 0, 0, 14, 64, 0, 0],
[0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 86, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.TelepipeLocation,
0,
10,
@ -284,9 +288,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 15, 0, 0, 0, 15, 64, 0, 0],
[0, 0],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 208, 86, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.TelepipeLocation,
0,
10,
@ -297,9 +301,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 16, 0, 0, 0, 16, 64, 0, 0],
[0, 0],
[2, 0, 0, 0, 0, 0, 1, 0, 10, 0, 0, 0, 144, 87, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.MedicalCenterDoor,
0,
10,
@ -310,9 +314,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 17, 0, 0, 0, 17, 64, 0, 0],
[0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 88, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.ShopDoor,
0,
10,
@ -323,9 +327,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 18, 0, 0, 0, 18, 64, 0, 0],
[0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 89, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.MenuActivation,
0,
10,
@ -336,9 +340,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 19, 0, 0, 0, 19, 64, 0, 0],
[0, 0],
[6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 90, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.HuntersGuildDoor,
0,
10,
@ -349,9 +353,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 20, 0, 0, 0, 20, 64, 0, 0],
[0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 90, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.TeleporterDoor,
0,
10,
@ -362,9 +366,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 21, 0, 0, 0, 21, 64, 0, 0],
[0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 91, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.PlayerSet,
0,
10,
@ -375,9 +379,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 22, 0, 0, 0, 22, 64, 0, 0],
[0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 92, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.PlayerSet,
0,
10,
@ -388,9 +392,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 23, 0, 0, 0, 23, 64, 0, 0],
[0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 144, 93, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.PlayerSet,
0,
10,
@ -401,9 +405,9 @@ function create_default_objects(): QuestObject[] {
[2, 0, 24, 0, 0, 0, 24, 64, 0, 0],
[0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 94, 251, 140],
]
],
),
new QuestObject(
new ObservableQuestObject(
ObjectType.PlayerSet,
0,
10,
@ -414,14 +418,14 @@ function create_default_objects(): QuestObject[] {
[2, 0, 25, 0, 0, 0, 25, 64, 0, 0],
[0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 208, 94, 251, 140],
]
],
),
];
}
function create_default_npcs(): QuestNpc[] {
function create_default_npcs(): ObservableQuestNpc[] {
return [
new QuestNpc(
new ObservableQuestNpc(
NpcType.GuildLady,
29,
0,
@ -435,9 +439,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[18, 192, 124, 68, 0, 128, 84, 68],
[128, 238, 223, 176],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.FemaleFat,
4,
1,
@ -451,9 +455,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[18, 0, 126, 68, 6, 0, 200, 66],
[128, 238, 232, 48],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.MaleDwarf,
10,
1,
@ -467,9 +471,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[18, 192, 125, 68, 6, 0, 180, 66],
[128, 238, 236, 176],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.RedSoldier,
26,
0,
@ -483,9 +487,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[18, 0, 127, 68, 6, 0, 2, 67],
[128, 238, 241, 48],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.BlueSoldier,
25,
0,
@ -499,9 +503,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[18, 192, 126, 68, 11, 0, 240, 66],
[128, 238, 245, 176],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.FemaleMacho,
5,
1,
@ -515,9 +519,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[18, 128, 125, 68, 9, 0, 160, 66],
[128, 238, 250, 48],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.Scientist,
30,
1,
@ -531,9 +535,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[18, 64, 125, 68, 8, 0, 140, 66],
[128, 238, 254, 176],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.MaleOld,
13,
1,
@ -547,9 +551,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[18, 0, 125, 68, 15, 0, 112, 66],
[128, 239, 3, 48],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.GuildLady,
29,
0,
@ -563,9 +567,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[18, 128, 124, 68, 0, 0, 82, 68],
[128, 239, 100, 192],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.Tekker,
28,
0,
@ -579,9 +583,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[0, 64, 124, 68, 0, 128, 79, 68],
[128, 239, 16, 176],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.MaleMacho,
12,
0,
@ -595,9 +599,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[0, 128, 123, 68, 0, 0, 72, 68],
[128, 239, 21, 48],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.FemaleMacho,
5,
0,
@ -611,9 +615,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[0, 0, 124, 68, 0, 0, 77, 68],
[128, 239, 25, 176],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.MaleFat,
11,
0,
@ -627,9 +631,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[18, 192, 123, 68, 0, 128, 74, 68],
[128, 239, 30, 48],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.FemaleTall,
7,
1,
@ -643,9 +647,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[18, 64, 127, 68, 4, 0, 12, 67],
[128, 239, 34, 176],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.Nurse,
31,
0,
@ -659,9 +663,9 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[0, 64, 126, 68, 0, 0, 87, 68],
[128, 239, 39, 48],
]
],
),
new QuestNpc(
new ObservableQuestNpc(
NpcType.Nurse,
31,
1,
@ -675,7 +679,7 @@ function create_default_npcs(): QuestNpc[] {
[0, 0, 0, 0, 0, 0],
[18, 128, 126, 68, 7, 0, 220, 66],
[128, 239, 43, 176],
]
],
),
];
}

View File

@ -1,13 +1,14 @@
import { TimePicker } from "antd";
import { observer } from "mobx-react";
import moment, { Moment } from "moment";
import React, { ReactNode, Component } from "react";
import React, { Component, ReactNode } from "react";
import { AutoSizer, Index, SortDirection } from "react-virtualized";
import { Episode, HuntMethod } from "../../domain";
import { EnemyNpcTypes, NpcType } from "../../domain/NpcType";
import { hunt_method_store } from "../../stores/HuntMethodStore";
import { BigTable, Column, ColumnSort } from "../BigTable";
import styles from "./MethodsComponent.css";
import { HuntMethod } from "../../domain";
import { Episode } from "../../data_formats/parsing/quest/Episode";
import { ENEMY_NPC_TYPES, npc_data, NpcType } from "../../data_formats/parsing/quest/npc_types";
@observer
export class MethodsComponent extends Component {
@ -39,13 +40,13 @@ export class MethodsComponent extends Component {
];
// One column per enemy type.
for (const enemy of EnemyNpcTypes) {
for (const enemy_type of ENEMY_NPC_TYPES) {
columns.push({
key: enemy.code,
name: enemy.name,
key: NpcType[enemy_type],
name: npc_data(enemy_type).name,
width: 75,
cell_renderer: method => {
const count = method.enemy_counts.get(enemy);
const count = method.enemy_counts.get(enemy_type);
return count == null ? "" : count.toString();
},
class_name: "number",
@ -97,7 +98,7 @@ export class MethodsComponent extends Component {
} else if (column.key === "time") {
cmp = a.time - b.time;
} else if (column.key) {
const type = NpcType.by_code(column.key);
const type = (NpcType as any)[column.key];
if (type) {
cmp = (a.enemy_counts.get(type) || 0) - (b.enemy_counts.get(type) || 0);

View File

@ -2,12 +2,13 @@ import { computed } from "mobx";
import { observer } from "mobx-react";
import React, { Component, ReactNode } from "react";
import { AutoSizer, Index } from "react-virtualized";
import { Difficulty, Episode, SectionId } from "../../domain";
import { Difficulty, SectionId } from "../../domain";
import { hunt_optimizer_store, OptimalMethod } from "../../stores/HuntOptimizerStore";
import { BigTable, Column } from "../BigTable";
import { SectionIdIcon } from "../SectionIdIcon";
import { hours_to_string } from "../time";
import styles from "./OptimizationResultComponent.css";
import { Episode } from "../../data_formats/parsing/quest/Episode";
@observer
export class OptimizationResultComponent extends Component {

View File

@ -3,10 +3,11 @@ import { autorun, IReactionDisposer } from "mobx";
import { observer } from "mobx-react";
import React, { Component, PureComponent, ReactNode } from "react";
import { Vec3 } from "../../data_formats/vector";
import { QuestEntity, QuestNpc } from "../../domain";
import { quest_editor_store } from "../../stores/QuestEditorStore";
import { DisabledTextComponent } from "../DisabledTextComponent";
import styles from "./EntityInfoComponent.css";
import { ObservableQuestEntity, ObservableQuestNpc } from "../../domain";
import { entity_data, entity_type_to_string } from "../../data_formats/parsing/quest/entities";
@observer
export class EntityInfoComponent extends Component {
@ -21,8 +22,8 @@ export class EntityInfoComponent extends Component {
<table className={styles.table}>
<tbody>
<tr>
<th>{entity instanceof QuestNpc ? "NPC" : "Object"}:</th>
<td>{entity.type.name}</td>
<th>{entity instanceof ObservableQuestNpc ? "NPC" : "Object"}:</th>
<td>{entity_data(entity.type).name}</td>
</tr>
<tr>
<th>Section:</th>
@ -56,7 +57,7 @@ export class EntityInfoComponent extends Component {
}
type CoordProps = {
entity: QuestEntity;
entity: ObservableQuestEntity;
position_type: "position" | "section_position";
coord: "x" | "y" | "z";
};
@ -117,9 +118,11 @@ class CoordInput extends Component<CoordProps, { value: number; initial_position
});
},
{
name: `${this.props.entity.type.code}.${this.props.position_type}.${this.props.coord} changed`,
name: `${entity_type_to_string(this.props.entity.type)}.${
this.props.position_type
}.${this.props.coord} changed`,
delay: 50,
}
},
);
}
@ -132,7 +135,7 @@ class CoordInput extends Component<CoordProps, { value: number; initial_position
quest_editor_store.push_entity_move_action(
this.props.entity,
this.state.initial_position,
this.props.entity.position
this.props.entity.position,
);
}
};

View File

@ -1,10 +1,11 @@
import { Input, InputNumber } from "antd";
import { observer } from "mobx-react";
import React, { ChangeEvent, Component, ReactNode } from "react";
import { Episode, NpcType } from "../../domain";
import { quest_editor_store } from "../../stores/QuestEditorStore";
import { DisabledTextComponent } from "../DisabledTextComponent";
import styles from "./QuestInfoComponent.css";
import { Episode } from "../../data_formats/parsing/quest/Episode";
import { npc_data, NpcType } from "../../data_formats/parsing/quest/npc_types";
@observer
export class QuestInfoComponent extends Component {
@ -24,14 +25,14 @@ export class QuestInfoComponent extends Component {
const extra_canadines = (npc_counts.get(NpcType.Canane) || 0) * 8;
// Sort by type ID.
const sorted_npc_counts = [...npc_counts].sort((a, b) => a[0].id - b[0].id);
// Sort by canonical order.
const sorted_npc_counts = [...npc_counts].sort((a, b) => a[0] - b[0]);
const npc_count_rows = sorted_npc_counts.map(([npc_type, count]) => {
const extra = npc_type === NpcType.Canadine ? extra_canadines : 0;
return (
<tr key={npc_type.id}>
<td>{npc_type.name}:</td>
<tr key={npc_type}>
<td>{npc_data(npc_type).name}:</td>
<td>{count + extra}</td>
</tr>
);

View File

@ -1,20 +1,19 @@
import { Button, Dropdown, Form, Icon, Input, Menu, Modal, Select, Upload } from "antd";
import { ClickParam } from "antd/lib/menu";
import { UploadChangeParam, UploadFile } from "antd/lib/upload/interface";
import { computed } from "mobx";
import { observer } from "mobx-react";
import React, { ChangeEvent, Component, ReactNode } from "react";
import { Episode } from "../../domain";
import { area_store } from "../../stores/AreaStore";
import { quest_editor_store } from "../../stores/QuestEditorStore";
import { undo_manager } from "../../undo";
import styles from "./Toolbar.css";
import { Episode } from "../../data_formats/parsing/quest/Episode";
@observer
export class Toolbar extends Component {
render(): ReactNode {
const quest = quest_editor_store.current_quest;
const areas = quest ? Array.from(quest.area_variants).map(a => a.area) : [];
const area = quest_editor_store.current_area;
const area_id = area && area.id;
return (
<div className={styles.main}>
@ -67,18 +66,7 @@ export class Toolbar extends Component {
>
Redo
</Button>
<Select
onChange={quest_editor_store.set_current_area_id}
value={area_id}
style={{ width: 200 }}
disabled={!quest}
>
{areas.map(area => (
<Select.Option key={area.id} value={area.id}>
{area.name}
</Select.Option>
))}
</Select>
<AreaComponent />
<SaveQuestComponent />
</div>
);
@ -103,6 +91,51 @@ export class Toolbar extends Component {
}
}
@observer
class AreaComponent extends Component {
@computed private get entities_per_area(): Map<number, number> {
const quest = quest_editor_store.current_quest;
const map = new Map<number, number>();
if (quest) {
for (const npc of quest.npcs) {
map.set(npc.area_id, (map.get(npc.area_id) || 0) + 1);
}
for (const obj of quest.objects) {
map.set(obj.area_id, (map.get(obj.area_id) || 0) + 1);
}
}
return map;
}
render(): ReactNode {
const quest = quest_editor_store.current_quest;
const areas = quest ? area_store.get_areas_for_episode(quest.episode) : [];
const area = quest_editor_store.current_area;
return (
<Select
onChange={quest_editor_store.set_current_area_id}
value={area && area.id}
style={{ width: 200 }}
disabled={!quest}
>
{areas.map(area => {
const entity_count = quest && this.entities_per_area.get(area.id);
return (
<Select.Option key={area.id} value={area.id}>
{area.name}
{entity_count && ` (${entity_count})`}
</Select.Option>
);
})}
</Select>
);
}
}
@observer
class SaveQuestComponent extends Component {
render(): ReactNode {
@ -137,7 +170,7 @@ class SaveQuestComponent extends Component {
private ok(): void {
quest_editor_store.save_current_quest_to_file(
quest_editor_store.save_dialog_filename || "untitled"
quest_editor_store.save_dialog_filename || "untitled",
);
}