mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-07 08:48:28 +08:00
Added Loadable and PerServer. Refactored code to make data loading and per-server data simpler.
This commit is contained in:
parent
6cfcb6fac0
commit
734ed1016d
@ -10,6 +10,8 @@
|
|||||||
@component-background: @body-background;
|
@component-background: @body-background;
|
||||||
@text-color: hsl(200, 10%, 90%);
|
@text-color: hsl(200, 10%, 90%);
|
||||||
@text-color-secondary: hsl(200, 20%, 80%);
|
@text-color-secondary: hsl(200, 20%, 80%);
|
||||||
|
@text-color-dark: fade(white, 85%);
|
||||||
|
@text-color-secondary-dark: fade(white, 65%);
|
||||||
|
|
||||||
@heading-color: fade(@black, 85%);
|
@heading-color: fade(@black, 85%);
|
||||||
|
|
||||||
@ -28,8 +30,13 @@
|
|||||||
// Disabled states
|
// Disabled states
|
||||||
@disabled-color: fade(#fff, 50%);
|
@disabled-color: fade(#fff, 50%);
|
||||||
|
|
||||||
|
// Animation
|
||||||
|
@animation-duration-slow: 0.1s; // Modal
|
||||||
|
@animation-duration-base: 0.066s;
|
||||||
|
@animation-duration-fast: 0.033s; // Tooltip
|
||||||
|
|
||||||
// Input
|
// Input
|
||||||
@input-bg: darken(@body-background, 10%);
|
@input-bg: darken(@body-background, 5%);
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
@btn-default-bg: lighten(@body-background, 10%);
|
@btn-default-bg: lighten(@body-background, 10%);
|
||||||
@ -39,4 +46,7 @@
|
|||||||
|
|
||||||
// Table
|
// Table
|
||||||
@table-selected-row-bg: @item-active-bg;
|
@table-selected-row-bg: @item-active-bg;
|
||||||
@table-row-hover-bg: @item-hover-bg;
|
@table-row-hover-bg: @item-hover-bg;
|
||||||
|
|
||||||
|
// Menu
|
||||||
|
@menu-dark-bg: @body-background;
|
@ -11,6 +11,7 @@
|
|||||||
"@types/text-encoding": "^0.0.35",
|
"@types/text-encoding": "^0.0.35",
|
||||||
"antd": "^3.19.1",
|
"antd": "^3.19.1",
|
||||||
"craco-antd": "^1.11.0",
|
"craco-antd": "^1.11.0",
|
||||||
|
"javascript-lp-solver": "^0.4.5",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.11",
|
||||||
"mobx": "^5.9.4",
|
"mobx": "^5.9.4",
|
||||||
"mobx-react": "^5.4.4",
|
"mobx-react": "^5.4.4",
|
||||||
|
133
src/Loadable.ts
Normal file
133
src/Loadable.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { observable, computed } from "mobx";
|
||||||
|
import { defer } from "lodash";
|
||||||
|
|
||||||
|
export enum LoadableState {
|
||||||
|
/**
|
||||||
|
* No attempt has been made to load data.
|
||||||
|
*/
|
||||||
|
Uninitialized,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first data load is underway.
|
||||||
|
*/
|
||||||
|
Initializing,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data was loaded at least once. The most recent load was successful.
|
||||||
|
*/
|
||||||
|
Nominal,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data was loaded at least once. The most recent load failed.
|
||||||
|
*/
|
||||||
|
Error,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data was loaded at least once. Another data load is underway.
|
||||||
|
*/
|
||||||
|
Reloading,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a value that can be loaded asynchronously.
|
||||||
|
* [state]{@link Loadable#state} represents the current state of this Loadable's value.
|
||||||
|
*/
|
||||||
|
export class Loadable<T> {
|
||||||
|
@observable private _value: T;
|
||||||
|
@observable private _promise: Promise<T> = new Promise(resolve => resolve(this._value));
|
||||||
|
@observable private _state = LoadableState.Uninitialized;
|
||||||
|
private _load?: () => Promise<T>;
|
||||||
|
@observable private _error?: Error;
|
||||||
|
|
||||||
|
constructor(initialValue: T, load?: () => Promise<T>) {
|
||||||
|
this._value = initialValue;
|
||||||
|
this._load = load;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When this Loadable is uninitialized, a load will be triggered.
|
||||||
|
* Will return the initial value until a load has succeeded.
|
||||||
|
*/
|
||||||
|
@computed get value(): T {
|
||||||
|
// Load value on first use and return initial placeholder value.
|
||||||
|
if (this._state === LoadableState.Uninitialized) {
|
||||||
|
// Defer loading value to avoid side effects in computed value.
|
||||||
|
defer(() => this.loadValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method returns valid data as soon as possible.
|
||||||
|
* If the Loadable is uninitialized a data load will be triggered, otherwise the current value will be returned.
|
||||||
|
*/
|
||||||
|
get promise(): Promise<T> {
|
||||||
|
// Load value on first use.
|
||||||
|
if (this._state === LoadableState.Uninitialized) {
|
||||||
|
return this.loadValue();
|
||||||
|
} else {
|
||||||
|
return this._promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get state(): LoadableState {
|
||||||
|
return this._state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true if the initial data load has happened. It may or may not have succeeded.
|
||||||
|
* Check [error]{@link Loadable#error} to know whether an error occurred.
|
||||||
|
*/
|
||||||
|
@computed get isInitialized(): boolean {
|
||||||
|
return this._state !== LoadableState.Uninitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true if a data load is underway. This may be the initializing load or a later load.
|
||||||
|
*/
|
||||||
|
@computed get isLoading(): boolean {
|
||||||
|
switch (this._state) {
|
||||||
|
case LoadableState.Initializing:
|
||||||
|
case LoadableState.Reloading:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns an {@link Error} if an error occurred during the most recent data load.
|
||||||
|
*/
|
||||||
|
@computed get error(): Error | undefined {
|
||||||
|
return this._error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the data. Initializes the Loadable if it is uninitialized.
|
||||||
|
*/
|
||||||
|
load(): Promise<T> {
|
||||||
|
return this.loadValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadValue(): Promise<T> {
|
||||||
|
if (this.isLoading) return this._promise;
|
||||||
|
|
||||||
|
this._state = LoadableState.Initializing;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this._load) {
|
||||||
|
this._promise = this._load();
|
||||||
|
this._value = await this._promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._state = LoadableState.Nominal;
|
||||||
|
this._error = undefined;
|
||||||
|
return this._value;
|
||||||
|
} catch (e) {
|
||||||
|
this._state = LoadableState.Error;
|
||||||
|
this._error = e;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
import { memoize } from 'lodash';
|
|
||||||
import { huntMethodStore } from '../stores/HuntMethodStore';
|
|
||||||
import { getHuntMethods } from '../data/loading/huntMethods';
|
|
||||||
|
|
||||||
export const loadHuntMethods = memoize(
|
|
||||||
async (server: string) => {
|
|
||||||
huntMethodStore.methods.replace(await getHuntMethods(server));
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,9 +0,0 @@
|
|||||||
import { memoize } from "lodash";
|
|
||||||
import { getItems } from "../data/loading/items";
|
|
||||||
import { itemStore } from "../stores/ItemStore";
|
|
||||||
|
|
||||||
export const loadItems = memoize(
|
|
||||||
async (server: string) => {
|
|
||||||
itemStore.items.replace(await getItems(server));
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,87 +0,0 @@
|
|||||||
import { action } from 'mobx';
|
|
||||||
import { ArrayBufferCursor } from '../../data/ArrayBufferCursor';
|
|
||||||
import { getAreaSections } from '../../data/loading/areas';
|
|
||||||
import { getNpcGeometry, getObjectGeometry } from '../../data/loading/entities';
|
|
||||||
import { parseNj, parseXj } from '../../data/parsing/ninja';
|
|
||||||
import { parseQuest } from '../../data/parsing/quest';
|
|
||||||
import { Section, Vec3, QuestEntity } from '../../domain';
|
|
||||||
import { createNpcMesh, createObjectMesh } from '../../rendering/entities';
|
|
||||||
import { createModelMesh } from '../../rendering/models';
|
|
||||||
import { setModel, setQuest } from './questEditor';
|
|
||||||
|
|
||||||
export function loadFile(file: File) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.addEventListener('loadend', () => { loadend(file, reader) });
|
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: notify user of problems.
|
|
||||||
async function loadend(file: File, reader: FileReader) {
|
|
||||||
if (!(reader.result instanceof ArrayBuffer)) {
|
|
||||||
console.error('Couldn\'t read file.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.name.endsWith('.nj')) {
|
|
||||||
setModel(createModelMesh(parseNj(new ArrayBufferCursor(reader.result, true))));
|
|
||||||
} else if (file.name.endsWith('.xj')) {
|
|
||||||
setModel(createModelMesh(parseXj(new ArrayBufferCursor(reader.result, true))));
|
|
||||||
} else {
|
|
||||||
const quest = parseQuest(new ArrayBufferCursor(reader.result, true));
|
|
||||||
setQuest(quest);
|
|
||||||
|
|
||||||
if (quest) {
|
|
||||||
// Load section data.
|
|
||||||
for (const variant of quest.areaVariants) {
|
|
||||||
const sections = await getAreaSections(quest.episode, variant.area.id, variant.id);
|
|
||||||
variant.sections = sections;
|
|
||||||
|
|
||||||
// Generate object geometry.
|
|
||||||
for (const object of quest.objects.filter(o => o.areaId === variant.area.id)) {
|
|
||||||
try {
|
|
||||||
const geometry = await getObjectGeometry(object.type);
|
|
||||||
setSectionOnVisibleQuestEntity(object, sections);
|
|
||||||
object.object3d = createObjectMesh(object, geometry);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate NPC geometry.
|
|
||||||
for (const npc of quest.npcs.filter(npc => npc.areaId === variant.area.id)) {
|
|
||||||
try {
|
|
||||||
const geometry = await getNpcGeometry(npc.type);
|
|
||||||
setSectionOnVisibleQuestEntity(npc, sections);
|
|
||||||
npc.object3d = createNpcMesh(npc, geometry);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('Couldn\'t parse quest file.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setSectionOnVisibleQuestEntity = action('setSectionOnVisibleQuestEntity',
|
|
||||||
(entity: QuestEntity, sections: Section[]) => {
|
|
||||||
let { x, y, z } = entity.position;
|
|
||||||
|
|
||||||
const section = sections.find(s => s.id === entity.sectionId);
|
|
||||||
entity.section = section;
|
|
||||||
|
|
||||||
if (section) {
|
|
||||||
const { x: secX, y: secY, z: secZ } = section.position;
|
|
||||||
const rotX = section.cosYAxisRotation * x + section.sinYAxisRotation * z;
|
|
||||||
const rotZ = -section.sinYAxisRotation * x + section.cosYAxisRotation * z;
|
|
||||||
x = rotX + secX;
|
|
||||||
y += secY;
|
|
||||||
z = rotZ + secZ;
|
|
||||||
} else {
|
|
||||||
console.warn(`Section ${entity.sectionId} not found.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
entity.position = new Vec3(x, y, z);
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,66 +0,0 @@
|
|||||||
import { writeQuestQst } from '../../data/parsing/quest';
|
|
||||||
import { QuestEntity, Quest } from '../../domain';
|
|
||||||
import { questEditorStore } from '../../stores/QuestEditorStore';
|
|
||||||
import { action } from 'mobx';
|
|
||||||
import { Object3D } from 'three';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset application state, then set the current model.
|
|
||||||
*/
|
|
||||||
export const setModel = action('setModel', (model?: Object3D) => {
|
|
||||||
resetModelAndQuestState();
|
|
||||||
questEditorStore.currentModel = model;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset application state, then set current quest and area.
|
|
||||||
*/
|
|
||||||
export const setQuest = action('setQuest', (quest?: Quest) => {
|
|
||||||
resetModelAndQuestState();
|
|
||||||
questEditorStore.currentQuest = quest;
|
|
||||||
|
|
||||||
if (quest && quest.areaVariants.length) {
|
|
||||||
questEditorStore.currentArea = quest.areaVariants[0].area;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function resetModelAndQuestState() {
|
|
||||||
questEditorStore.currentQuest = undefined;
|
|
||||||
questEditorStore.currentArea = undefined;
|
|
||||||
questEditorStore.selectedEntity = undefined;
|
|
||||||
questEditorStore.currentModel = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setSelectedEntity = action('setSelectedEntity', (entity?: QuestEntity) => {
|
|
||||||
questEditorStore.selectedEntity = entity;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setCurrentAreaId = action('setCurrentAreaId', (areaId?: number) => {
|
|
||||||
questEditorStore.selectedEntity = undefined;
|
|
||||||
|
|
||||||
if (areaId == null) {
|
|
||||||
questEditorStore.currentArea = undefined;
|
|
||||||
} else if (questEditorStore.currentQuest) {
|
|
||||||
const areaVariant = questEditorStore.currentQuest.areaVariants.find(
|
|
||||||
variant => variant.area.id === areaId);
|
|
||||||
questEditorStore.currentArea = areaVariant && areaVariant.area;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const saveCurrentQuestToFile = (fileName: string) => {
|
|
||||||
if (questEditorStore.currentQuest) {
|
|
||||||
const cursor = writeQuestQst(questEditorStore.currentQuest, fileName);
|
|
||||||
|
|
||||||
if (!fileName.endsWith('.qst')) {
|
|
||||||
fileName += '.qst';
|
|
||||||
}
|
|
||||||
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = URL.createObjectURL(new Blob([cursor.buffer]));
|
|
||||||
a.download = fileName;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(a.href);
|
|
||||||
document.body.removeChild(a);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,5 +1,5 @@
|
|||||||
import { ArrayBufferCursor } from '../../ArrayBufferCursor';
|
import { ArrayBufferCursor } from '../../ArrayBufferCursor';
|
||||||
import { compress, decompress } from '.';
|
import { compress, decompress } from '../prs';
|
||||||
|
|
||||||
function testWithBytes(bytes: number[], expectedCompressedSize: number) {
|
function testWithBytes(bytes: number[], expectedCompressedSize: number) {
|
||||||
const cursor = new ArrayBufferCursor(new Uint8Array(bytes).buffer, true);
|
const cursor = new ArrayBufferCursor(new Uint8Array(bytes).buffer, true);
|
@ -1,35 +0,0 @@
|
|||||||
import { HuntMethod, NpcType, SimpleNpc, SimpleQuest } from "../../domain";
|
|
||||||
|
|
||||||
export async function getHuntMethods(server: string): Promise<HuntMethod[]> {
|
|
||||||
const response = await fetch(process.env.PUBLIC_URL + `/quests.${server}.tsv`);
|
|
||||||
const data = await response.text();
|
|
||||||
const rows = data.split('\n').map(line => line.split('\t'));
|
|
||||||
|
|
||||||
const npcTypeByIndex = rows[0].slice(2, -2).map((episode, i) => {
|
|
||||||
const enemy = rows[1][i + 2];
|
|
||||||
return NpcType.bySimpleNameAndEpisode(enemy, parseInt(episode, 10))!;
|
|
||||||
});
|
|
||||||
|
|
||||||
return rows.slice(2).map(row => {
|
|
||||||
const questName = row[0];
|
|
||||||
|
|
||||||
const npcs = row.slice(2, -2).flatMap((cell, cellI) => {
|
|
||||||
const amount = parseInt(cell, 10);
|
|
||||||
const type = npcTypeByIndex[cellI];
|
|
||||||
const enemies = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < amount; i++) {
|
|
||||||
enemies.push(new SimpleNpc(type));
|
|
||||||
}
|
|
||||||
|
|
||||||
return enemies;
|
|
||||||
});
|
|
||||||
|
|
||||||
return new HuntMethod(
|
|
||||||
new SimpleQuest(
|
|
||||||
questName,
|
|
||||||
npcs
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import { Item } from "../../domain";
|
|
||||||
import { sortedUniq } from "lodash";
|
|
||||||
|
|
||||||
export async function getItems(server: string): Promise<Item[]> {
|
|
||||||
const response = await fetch(process.env.PUBLIC_URL + `/drops.${server}.tsv`);
|
|
||||||
const data = await response.text();
|
|
||||||
return sortedUniq(
|
|
||||||
data.split('\n').slice(1).map(line => line.split('\t')[4]).sort()
|
|
||||||
).map(name => new Item(name));
|
|
||||||
}
|
|
@ -2,12 +2,31 @@ import { Object3D } from 'three';
|
|||||||
import { computed, observable } from 'mobx';
|
import { computed, observable } from 'mobx';
|
||||||
import { NpcType } from './NpcType';
|
import { NpcType } from './NpcType';
|
||||||
import { ObjectType } from './ObjectType';
|
import { ObjectType } from './ObjectType';
|
||||||
import { DatObject, DatNpc, DatUnknown } from '../data/parsing/dat';
|
import { DatObject, DatNpc, DatUnknown } from '../bin-data/parsing/dat';
|
||||||
import { ArrayBufferCursor } from '../data/ArrayBufferCursor';
|
import { ArrayBufferCursor } from '../bin-data/ArrayBufferCursor';
|
||||||
|
|
||||||
export { NpcType } from './NpcType';
|
export { NpcType } from './NpcType';
|
||||||
export { ObjectType } from './ObjectType';
|
export { ObjectType } from './ObjectType';
|
||||||
|
|
||||||
|
export enum Server {
|
||||||
|
Ephinea = 'Ephinea'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SectionId {
|
||||||
|
Viridia = 'Viridia',
|
||||||
|
Greenill = 'Greenill',
|
||||||
|
Skyly = 'Skyly',
|
||||||
|
Bluefull = 'Bluefull',
|
||||||
|
Purplenum = 'Purplenum',
|
||||||
|
Pinkal = 'Pinkal',
|
||||||
|
Redria = 'Redria',
|
||||||
|
Oran = 'Oran',
|
||||||
|
Yellowboze = 'Yellowboze',
|
||||||
|
Whitill = 'Whitill',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SectionIds = Object.keys(SectionId);
|
||||||
|
|
||||||
export class Vec3 {
|
export class Vec3 {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
1
src/javascript-lp-solver.d.ts
vendored
Normal file
1
src/javascript-lp-solver.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module 'javascript-lp-solver';
|
@ -1,32 +1,10 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import {
|
import { Color, HemisphereLight, Intersection, Mesh, MeshLambertMaterial, MOUSE, Object3D, PerspectiveCamera, Plane, Raycaster, Scene, Vector2, Vector3, WebGLRenderer } from 'three';
|
||||||
Color,
|
|
||||||
HemisphereLight,
|
|
||||||
MOUSE,
|
|
||||||
Object3D,
|
|
||||||
PerspectiveCamera,
|
|
||||||
Plane,
|
|
||||||
Raycaster,
|
|
||||||
Scene,
|
|
||||||
Vector2,
|
|
||||||
Vector3,
|
|
||||||
WebGLRenderer,
|
|
||||||
Intersection,
|
|
||||||
Mesh,
|
|
||||||
MeshLambertMaterial
|
|
||||||
} from 'three';
|
|
||||||
import OrbitControlsCreator from 'three-orbit-controls';
|
import OrbitControlsCreator from 'three-orbit-controls';
|
||||||
import { Vec3, Area, Quest, QuestEntity, QuestObject, QuestNpc, Section } from '../domain';
|
import { getAreaCollisionGeometry, getAreaRenderGeometry } from '../bin-data/loading/areas';
|
||||||
import { getAreaCollisionGeometry, getAreaRenderGeometry } from '../data/loading/areas';
|
import { Area, Quest, QuestEntity, QuestNpc, QuestObject, Section, Vec3 } from '../domain';
|
||||||
import {
|
import { questEditorStore } from '../stores/QuestEditorStore';
|
||||||
OBJECT_COLOR,
|
import { NPC_COLOR, NPC_HOVER_COLOR, NPC_SELECTED_COLOR, OBJECT_COLOR, OBJECT_HOVER_COLOR, OBJECT_SELECTED_COLOR } from './entities';
|
||||||
OBJECT_HOVER_COLOR,
|
|
||||||
OBJECT_SELECTED_COLOR,
|
|
||||||
NPC_COLOR,
|
|
||||||
NPC_HOVER_COLOR,
|
|
||||||
NPC_SELECTED_COLOR
|
|
||||||
} from './entities';
|
|
||||||
import { setSelectedEntity } from '../actions/quest-editor/questEditor';
|
|
||||||
|
|
||||||
const OrbitControls = OrbitControlsCreator(THREE);
|
const OrbitControls = OrbitControlsCreator(THREE);
|
||||||
|
|
||||||
@ -273,7 +251,7 @@ export class Renderer {
|
|||||||
: oldSelectedData !== data;
|
: oldSelectedData !== data;
|
||||||
|
|
||||||
if (selectionChanged) {
|
if (selectionChanged) {
|
||||||
setSelectedEntity(data && data.entity);
|
questEditorStore.setSelectedEntity(data && data.entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CylinderBufferGeometry, MeshLambertMaterial, Object3D, Vector3 } from 'three';
|
import { CylinderBufferGeometry, MeshLambertMaterial, Object3D, Vector3 } from 'three';
|
||||||
import { DatNpc, DatObject } from '../data/parsing/dat';
|
import { DatNpc, DatObject } from '../bin-data/parsing/dat';
|
||||||
import { NpcType, ObjectType, QuestNpc, QuestObject, Vec3 } from '../domain';
|
import { NpcType, ObjectType, QuestNpc, QuestObject, Vec3 } from '../domain';
|
||||||
import { createNpcMesh, createObjectMesh, NPC_COLOR, OBJECT_COLOR } from './entities';
|
import { createNpcMesh, createObjectMesh, NPC_COLOR, OBJECT_COLOR } from './entities';
|
||||||
|
|
||||||
|
8
src/stores/ApplicationStore.ts
Normal file
8
src/stores/ApplicationStore.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { observable } from "mobx";
|
||||||
|
import { Server } from "../domain";
|
||||||
|
|
||||||
|
class ApplicationStore {
|
||||||
|
@observable currentServer: Server = Server.Ephinea;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const applicationStore = new ApplicationStore();
|
@ -1,8 +1,46 @@
|
|||||||
import { observable, IObservableArray } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { HuntMethod } from "../domain";
|
import { HuntMethod, NpcType, Server, SimpleNpc, SimpleQuest } from "../domain";
|
||||||
|
import { Loadable } from "../Loadable";
|
||||||
|
import { PerServer } from "./PerServer";
|
||||||
|
|
||||||
class HuntMethodStore {
|
class HuntMethodStore {
|
||||||
@observable methods: IObservableArray<HuntMethod> = observable.array();
|
@observable methods: PerServer<Loadable<Array<HuntMethod>>> = new PerServer(server =>
|
||||||
|
new Loadable([], () => this.loadHuntMethods(server))
|
||||||
|
);
|
||||||
|
|
||||||
|
private async loadHuntMethods(server: Server): Promise<HuntMethod[]> {
|
||||||
|
const response = await fetch(process.env.PUBLIC_URL + `/quests.${Server[server]}.tsv`);
|
||||||
|
const data = await response.text();
|
||||||
|
const rows = data.split('\n').map(line => line.split('\t'));
|
||||||
|
|
||||||
|
const npcTypeByIndex = rows[0].slice(2, -2).map((episode, i) => {
|
||||||
|
const enemy = rows[1][i + 2];
|
||||||
|
return NpcType.bySimpleNameAndEpisode(enemy, parseInt(episode, 10))!;
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows.slice(2).map(row => {
|
||||||
|
const questName = row[0];
|
||||||
|
|
||||||
|
const npcs = row.slice(2, -2).flatMap((cell, cellI) => {
|
||||||
|
const amount = parseInt(cell, 10);
|
||||||
|
const type = npcTypeByIndex[cellI];
|
||||||
|
const enemies = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < amount; i++) {
|
||||||
|
enemies.push(new SimpleNpc(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
return enemies;
|
||||||
|
});
|
||||||
|
|
||||||
|
return new HuntMethod(
|
||||||
|
new SimpleQuest(
|
||||||
|
questName,
|
||||||
|
npcs
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const huntMethodStore = new HuntMethodStore();
|
export const huntMethodStore = new HuntMethodStore();
|
||||||
|
55
src/stores/HuntOptimizerStore.ts
Normal file
55
src/stores/HuntOptimizerStore.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Solve } from 'javascript-lp-solver';
|
||||||
|
import { observable } from "mobx";
|
||||||
|
import { Item, SectionIds } from "../domain";
|
||||||
|
import { huntMethodStore } from "./HuntMethodStore";
|
||||||
|
|
||||||
|
export class WantedItem {
|
||||||
|
@observable item: Item;
|
||||||
|
@observable amount: number;
|
||||||
|
|
||||||
|
constructor(item: Item, amount: number) {
|
||||||
|
this.item = item;
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HuntOptimizerStore {
|
||||||
|
@observable wantedItems: Array<WantedItem> = [];
|
||||||
|
|
||||||
|
optimize = async () => {
|
||||||
|
if (!this.wantedItems.length) return;
|
||||||
|
|
||||||
|
const constraints: { [itemName: string]: { min: number } } = {};
|
||||||
|
|
||||||
|
for (const item of this.wantedItems) {
|
||||||
|
constraints[item.item.name] = { min: item.amount };
|
||||||
|
}
|
||||||
|
|
||||||
|
const variables: { [methodName: string]: { [itemName: string]: number } } = {};
|
||||||
|
|
||||||
|
for (const method of await huntMethodStore.methods.current.promise) {
|
||||||
|
for (const sectionId of SectionIds) {
|
||||||
|
const variable = {};
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
variables[`${sectionId} ${method.quest.name}`] = variable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Solve({
|
||||||
|
optimize: '',
|
||||||
|
opType: 'min',
|
||||||
|
constraints,
|
||||||
|
variables
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const huntOptimizerStore = new HuntOptimizerStore();
|
||||||
|
|
||||||
|
type MethodWithDropRates = {
|
||||||
|
name: string
|
||||||
|
time: number
|
||||||
|
[itemName: string]: any
|
||||||
|
}
|
@ -1,8 +1,21 @@
|
|||||||
import { observable, IObservableArray } from "mobx";
|
import { sortedUniq } from "lodash";
|
||||||
import { Item } from "../domain";
|
import { observable } from "mobx";
|
||||||
|
import { Item, Server } from "../domain";
|
||||||
|
import { Loadable } from "../Loadable";
|
||||||
|
import { PerServer } from "./PerServer";
|
||||||
|
|
||||||
class ItemStore {
|
class ItemStore {
|
||||||
@observable items: IObservableArray<Item> = observable.array();
|
@observable items: PerServer<Loadable<Array<Item>>> = new PerServer(server =>
|
||||||
|
new Loadable([], () => this.loadItems(server))
|
||||||
|
);
|
||||||
|
|
||||||
|
private async loadItems(server: Server): Promise<Item[]> {
|
||||||
|
const response = await fetch(process.env.PUBLIC_URL + `/drops.${Server[server]}.tsv`);
|
||||||
|
const data = await response.text();
|
||||||
|
return sortedUniq(
|
||||||
|
data.split('\n').slice(1).map(line => line.split('\t')[4]).sort()
|
||||||
|
).map(name => new Item(name));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const itemStore = new ItemStore();
|
export const itemStore = new ItemStore();
|
||||||
|
30
src/stores/PerServer.ts
Normal file
30
src/stores/PerServer.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Server } from "../domain";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import { applicationStore } from "./ApplicationStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a value per server.
|
||||||
|
* E.g. drop tables differ per server, this can be represented by PerServer<DropTable>.
|
||||||
|
*/
|
||||||
|
export class PerServer<T> {
|
||||||
|
private values = new Map<Server, T>();
|
||||||
|
|
||||||
|
constructor(initialValue: T | ((server: Server) => T)) {
|
||||||
|
if (!(initialValue instanceof Function)) {
|
||||||
|
this.values.set(Server.Ephinea, initialValue);
|
||||||
|
} else {
|
||||||
|
this.values.set(Server.Ephinea, initialValue(Server.Ephinea));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(server: Server): T {
|
||||||
|
return this.values.get(server)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the value for the current server as set in {@link applicationStore}.
|
||||||
|
*/
|
||||||
|
@computed get current(): T {
|
||||||
|
return this.get(applicationStore.currentServer);
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,150 @@
|
|||||||
import { observable } from 'mobx';
|
import { observable, action } from 'mobx';
|
||||||
import { Object3D } from 'three';
|
import { Object3D } from 'three';
|
||||||
import { Area, Quest, QuestEntity } from '../domain';
|
import { ArrayBufferCursor } from '../bin-data/ArrayBufferCursor';
|
||||||
|
import { getAreaSections } from '../bin-data/loading/areas';
|
||||||
|
import { getNpcGeometry, getObjectGeometry } from '../bin-data/loading/entities';
|
||||||
|
import { parseNj, parseXj } from '../bin-data/parsing/ninja';
|
||||||
|
import { parseQuest, writeQuestQst } from '../bin-data/parsing/quest';
|
||||||
|
import { Area, Quest, QuestEntity, Section, Vec3 } from '../domain';
|
||||||
|
import { createNpcMesh, createObjectMesh } from '../rendering/entities';
|
||||||
|
import { createModelMesh } from '../rendering/models';
|
||||||
|
|
||||||
class QuestEditorStore {
|
class QuestEditorStore {
|
||||||
@observable currentModel?: Object3D;
|
@observable currentModel?: Object3D;
|
||||||
@observable currentQuest?: Quest;
|
@observable currentQuest?: Quest;
|
||||||
@observable currentArea?: Area;
|
@observable currentArea?: Area;
|
||||||
@observable selectedEntity?: QuestEntity;
|
@observable selectedEntity?: QuestEntity;
|
||||||
|
|
||||||
|
setModel = action('setModel', (model?: Object3D) => {
|
||||||
|
this.resetModelAndQuestState();
|
||||||
|
this.currentModel = model;
|
||||||
|
})
|
||||||
|
|
||||||
|
setQuest = action('setQuest', (quest?: Quest) => {
|
||||||
|
this.resetModelAndQuestState();
|
||||||
|
this.currentQuest = quest;
|
||||||
|
|
||||||
|
if (quest && quest.areaVariants.length) {
|
||||||
|
this.currentArea = quest.areaVariants[0].area;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
private resetModelAndQuestState() {
|
||||||
|
this.currentQuest = undefined;
|
||||||
|
this.currentArea = undefined;
|
||||||
|
this.selectedEntity = undefined;
|
||||||
|
this.currentModel = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedEntity = (entity?: QuestEntity) => {
|
||||||
|
this.selectedEntity = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentAreaId = action('setCurrentAreaId', (areaId?: number) => {
|
||||||
|
this.selectedEntity = undefined;
|
||||||
|
|
||||||
|
if (areaId == null) {
|
||||||
|
this.currentArea = undefined;
|
||||||
|
} else if (this.currentQuest) {
|
||||||
|
const areaVariant = this.currentQuest.areaVariants.find(
|
||||||
|
variant => variant.area.id === areaId
|
||||||
|
);
|
||||||
|
this.currentArea = areaVariant && areaVariant.area;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadFile = (file: File) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('loadend', () => { this.loadend(file, reader) });
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: notify user of problems.
|
||||||
|
private loadend = async (file: File, reader: FileReader) => {
|
||||||
|
if (!(reader.result instanceof ArrayBuffer)) {
|
||||||
|
console.error('Couldn\'t read file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.name.endsWith('.nj')) {
|
||||||
|
this.setModel(createModelMesh(parseNj(new ArrayBufferCursor(reader.result, true))));
|
||||||
|
} else if (file.name.endsWith('.xj')) {
|
||||||
|
this.setModel(createModelMesh(parseXj(new ArrayBufferCursor(reader.result, true))));
|
||||||
|
} else {
|
||||||
|
const quest = parseQuest(new ArrayBufferCursor(reader.result, true));
|
||||||
|
this.setQuest(quest);
|
||||||
|
|
||||||
|
if (quest) {
|
||||||
|
// Load section data.
|
||||||
|
for (const variant of quest.areaVariants) {
|
||||||
|
const sections = await getAreaSections(quest.episode, variant.area.id, variant.id);
|
||||||
|
variant.sections = sections;
|
||||||
|
|
||||||
|
// Generate object geometry.
|
||||||
|
for (const object of quest.objects.filter(o => o.areaId === variant.area.id)) {
|
||||||
|
try {
|
||||||
|
const geometry = await getObjectGeometry(object.type);
|
||||||
|
this.setSectionOnVisibleQuestEntity(object, sections);
|
||||||
|
object.object3d = createObjectMesh(object, geometry);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate NPC geometry.
|
||||||
|
for (const npc of quest.npcs.filter(npc => npc.areaId === variant.area.id)) {
|
||||||
|
try {
|
||||||
|
const geometry = await getNpcGeometry(npc.type);
|
||||||
|
this.setSectionOnVisibleQuestEntity(npc, sections);
|
||||||
|
npc.object3d = createNpcMesh(npc, geometry);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Couldn\'t parse quest file.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setSectionOnVisibleQuestEntity = async (entity: QuestEntity, sections: Section[]) => {
|
||||||
|
let { x, y, z } = entity.position;
|
||||||
|
|
||||||
|
const section = sections.find(s => s.id === entity.sectionId);
|
||||||
|
entity.section = section;
|
||||||
|
|
||||||
|
if (section) {
|
||||||
|
const { x: secX, y: secY, z: secZ } = section.position;
|
||||||
|
const rotX = section.cosYAxisRotation * x + section.sinYAxisRotation * z;
|
||||||
|
const rotZ = -section.sinYAxisRotation * x + section.cosYAxisRotation * z;
|
||||||
|
x = rotX + secX;
|
||||||
|
y += secY;
|
||||||
|
z = rotZ + secZ;
|
||||||
|
} else {
|
||||||
|
console.warn(`Section ${entity.sectionId} not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.position = new Vec3(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCurrentQuestToFile = (fileName: string) => {
|
||||||
|
if (this.currentQuest) {
|
||||||
|
const cursor = writeQuestQst(this.currentQuest, fileName);
|
||||||
|
|
||||||
|
if (!fileName.endsWith('.qst')) {
|
||||||
|
fileName += '.qst';
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(new Blob([cursor.buffer]));
|
||||||
|
a.download = fileName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const questEditorStore = new QuestEditorStore();
|
export const questEditorStore = new QuestEditorStore();
|
||||||
|
@ -13,8 +13,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ApplicationComponent-heading {
|
.ApplicationComponent-heading {
|
||||||
font-size: 22px;
|
margin: 5px 10px 0 10px;
|
||||||
margin: 10px 10px 0 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ApplicationComponent-beta {
|
.ApplicationComponent-beta {
|
||||||
|
@ -1,20 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import './HuntOptimizerComponent.css';
|
import './HuntOptimizerComponent.css';
|
||||||
import { WantedItemsComponent } from "./WantedItemsComponent";
|
import { WantedItemsComponent } from "./WantedItemsComponent";
|
||||||
import { loadItems } from "../../actions/items";
|
|
||||||
import { loadHuntMethods } from "../../actions/huntMethods";
|
|
||||||
|
|
||||||
export class HuntOptimizerComponent extends React.Component {
|
export function HuntOptimizerComponent() {
|
||||||
componentDidMount() {
|
return (
|
||||||
loadItems('ephinea');
|
<section className="ho-HuntOptimizerComponent">
|
||||||
loadHuntMethods('ephinea');
|
<WantedItemsComponent />
|
||||||
}
|
</section>
|
||||||
|
);
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<section className="ho-HuntOptimizerComponent">
|
|
||||||
<WantedItemsComponent />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,41 @@
|
|||||||
import { Button, InputNumber, Select, Table } from "antd";
|
import { Button, InputNumber, Select, Table } from "antd";
|
||||||
import { observable } from "mobx";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Item } from "../../domain";
|
import { huntOptimizerStore, WantedItem } from "../../stores/HuntOptimizerStore";
|
||||||
import { itemStore } from "../../stores/ItemStore";
|
import { itemStore } from "../../stores/ItemStore";
|
||||||
import './WantedItemsComponent.css';
|
import './WantedItemsComponent.css';
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class WantedItemsComponent extends React.Component {
|
export class WantedItemsComponent extends React.Component {
|
||||||
@observable
|
|
||||||
private wantedItems: Array<WantedItem> = [];
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// Make sure render is called on updates.
|
// Make sure render is called on updates.
|
||||||
this.wantedItems.slice(0, 0);
|
huntOptimizerStore.wantedItems.slice(0, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="ho-WantedItemsComponent">
|
<section className="ho-WantedItemsComponent">
|
||||||
<h2>Wanted Items</h2>
|
<h2>Wanted Items</h2>
|
||||||
<Select
|
<div>
|
||||||
value={undefined}
|
<Select
|
||||||
showSearch
|
value={undefined}
|
||||||
placeholder="Add an item"
|
showSearch
|
||||||
optionFilterProp="children"
|
placeholder="Add an item"
|
||||||
style={{ width: 200 }}
|
optionFilterProp="children"
|
||||||
filterOption
|
style={{ width: 200 }}
|
||||||
onChange={this.addWanted}
|
filterOption
|
||||||
>
|
onChange={this.addWanted}
|
||||||
{itemStore.items.map(item => (
|
>
|
||||||
<Select.Option key={item.name}>
|
{itemStore.items.current.value.map(item => (
|
||||||
{item.name}
|
<Select.Option key={item.name}>
|
||||||
</Select.Option>
|
{item.name}
|
||||||
))}
|
</Select.Option>
|
||||||
</Select>
|
))}
|
||||||
|
</Select>
|
||||||
|
<Button onClick={huntOptimizerStore.optimize}>Optimize</Button>
|
||||||
|
</div>
|
||||||
<Table
|
<Table
|
||||||
className="ho-WantedItemsComponent-table"
|
className="ho-WantedItemsComponent-table"
|
||||||
size="small"
|
size="small"
|
||||||
dataSource={this.wantedItems}
|
dataSource={huntOptimizerStore.wantedItems}
|
||||||
rowKey={wanted => wanted.item.name}
|
rowKey={wanted => wanted.item.name}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
>
|
>
|
||||||
@ -59,30 +58,20 @@ export class WantedItemsComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private addWanted = (itemName: string) => {
|
private addWanted = (itemName: string) => {
|
||||||
let added = this.wantedItems.find(w => w.item.name === itemName);
|
let added = huntOptimizerStore.wantedItems.find(w => w.item.name === itemName);
|
||||||
|
|
||||||
if (!added) {
|
if (!added) {
|
||||||
const item = itemStore.items.find(i => i.name === itemName)!;
|
const item = itemStore.items.current.value.find(i => i.name === itemName)!;
|
||||||
this.wantedItems.push(new WantedItem(item, 1));
|
huntOptimizerStore.wantedItems.push(new WantedItem(item, 1));
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
private removeWanted = (wanted: WantedItem) => () => {
|
private removeWanted = (wanted: WantedItem) => () => {
|
||||||
const i = this.wantedItems.findIndex(w => w === wanted);
|
const i = huntOptimizerStore.wantedItems.findIndex(w => w === wanted);
|
||||||
|
|
||||||
if (i !== -1) {
|
if (i !== -1) {
|
||||||
this.wantedItems.splice(i, 1);
|
huntOptimizerStore.wantedItems.splice(i, 1);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class WantedItem {
|
|
||||||
@observable item: Item;
|
|
||||||
@observable amount: number;
|
|
||||||
|
|
||||||
constructor(item: Item, amount: number) {
|
|
||||||
this.item = item;
|
|
||||||
this.amount = amount;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,8 +3,6 @@ import { UploadChangeParam } from "antd/lib/upload";
|
|||||||
import { UploadFile } from "antd/lib/upload/interface";
|
import { UploadFile } from "antd/lib/upload/interface";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React, { ChangeEvent } from "react";
|
import React, { ChangeEvent } from "react";
|
||||||
import { loadFile } from "../../actions/quest-editor/loadFile";
|
|
||||||
import { saveCurrentQuestToFile, setCurrentAreaId } from "../../actions/quest-editor/questEditor";
|
|
||||||
import { questEditorStore } from "../../stores/QuestEditorStore";
|
import { questEditorStore } from "../../stores/QuestEditorStore";
|
||||||
import { EntityInfoComponent } from "./EntityInfoComponent";
|
import { EntityInfoComponent } from "./EntityInfoComponent";
|
||||||
import './QuestEditorComponent.css';
|
import './QuestEditorComponent.css';
|
||||||
@ -66,7 +64,7 @@ export class QuestEditorComponent extends React.Component<{}, {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private saveDialogAffirmed = () => {
|
private saveDialogAffirmed = () => {
|
||||||
saveCurrentQuestToFile(this.state.saveDialogFilename);
|
questEditorStore.saveCurrentQuestToFile(this.state.saveDialogFilename);
|
||||||
this.setState({ saveDialogOpen: false });
|
this.setState({ saveDialogOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,8 +96,8 @@ class Toolbar extends React.Component<{ onSaveAsClicked: (filename?: string) =>
|
|||||||
</Upload>
|
</Upload>
|
||||||
{areas && (
|
{areas && (
|
||||||
<Select
|
<Select
|
||||||
onChange={setCurrentAreaId}
|
onChange={questEditorStore.setCurrentAreaId}
|
||||||
defaultValue={areaId}
|
value={areaId}
|
||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
>
|
>
|
||||||
{areas.map(area =>
|
{areas.map(area =>
|
||||||
@ -120,7 +118,7 @@ class Toolbar extends React.Component<{ onSaveAsClicked: (filename?: string) =>
|
|||||||
private setFilename = (info: UploadChangeParam<UploadFile>) => {
|
private setFilename = (info: UploadChangeParam<UploadFile>) => {
|
||||||
if (info.file.originFileObj) {
|
if (info.file.originFileObj) {
|
||||||
this.setState({ filename: info.file.name });
|
this.setState({ filename: info.file.name });
|
||||||
loadFile(info.file.originFileObj);
|
questEditorStore.loadFile(info.file.originFileObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
.qe-QuestInfoComponent pre {
|
.qe-QuestInfoComponent pre {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: solid 1px lightgray;
|
border: solid 1px hsl(200, 10%, 30%);
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5521,6 +5521,11 @@ istanbul-reports@^2.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
handlebars "^4.1.2"
|
handlebars "^4.1.2"
|
||||||
|
|
||||||
|
javascript-lp-solver@^0.4.5:
|
||||||
|
version "0.4.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/javascript-lp-solver/-/javascript-lp-solver-0.4.5.tgz#597106f586b4843a4ce51c7e6dc8a91f8a929c3f"
|
||||||
|
integrity sha1-WXEG9Ya0hDpM5Rx+bcipH4qSnD8=
|
||||||
|
|
||||||
jest-changed-files@^24.8.0:
|
jest-changed-files@^24.8.0:
|
||||||
version "24.8.0"
|
version "24.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.8.0.tgz#7e7eb21cf687587a85e50f3d249d1327e15b157b"
|
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.8.0.tgz#7e7eb21cf687587a85e50f3d249d1327e15b157b"
|
||||||
|
Loading…
Reference in New Issue
Block a user