Added Loadable and PerServer. Refactored code to make data loading and per-server data simpler.

This commit is contained in:
Daan Vanden Bosch 2019-06-03 21:41:18 +02:00
parent 6cfcb6fac0
commit 734ed1016d
46 changed files with 511 additions and 321 deletions

View File

@ -10,6 +10,8 @@
@component-background: @body-background;
@text-color: hsl(200, 10%, 90%);
@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%);
@ -28,8 +30,13 @@
// Disabled states
@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-bg: darken(@body-background, 10%);
@input-bg: darken(@body-background, 5%);
// Buttons
@btn-default-bg: lighten(@body-background, 10%);
@ -39,4 +46,7 @@
// Table
@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;

View File

@ -11,6 +11,7 @@
"@types/text-encoding": "^0.0.35",
"antd": "^3.19.1",
"craco-antd": "^1.11.0",
"javascript-lp-solver": "^0.4.5",
"lodash": "^4.17.11",
"mobx": "^5.9.4",
"mobx-react": "^5.4.4",

133
src/Loadable.ts Normal file
View 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;
}
}
}

View File

@ -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));
}
);

View File

@ -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));
}
);

View File

@ -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);
}
);

View File

@ -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);
}
};

View File

@ -1,5 +1,5 @@
import { ArrayBufferCursor } from '../../ArrayBufferCursor';
import { compress, decompress } from '.';
import { compress, decompress } from '../prs';
function testWithBytes(bytes: number[], expectedCompressedSize: number) {
const cursor = new ArrayBufferCursor(new Uint8Array(bytes).buffer, true);

View File

@ -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
)
);
});
}

View File

@ -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));
}

View File

@ -2,12 +2,31 @@ import { Object3D } from 'three';
import { computed, observable } from 'mobx';
import { NpcType } from './NpcType';
import { ObjectType } from './ObjectType';
import { DatObject, DatNpc, DatUnknown } from '../data/parsing/dat';
import { ArrayBufferCursor } from '../data/ArrayBufferCursor';
import { DatObject, DatNpc, DatUnknown } from '../bin-data/parsing/dat';
import { ArrayBufferCursor } from '../bin-data/ArrayBufferCursor';
export { NpcType } from './NpcType';
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 {
x: number;
y: number;

1
src/javascript-lp-solver.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'javascript-lp-solver';

View File

@ -1,32 +1,10 @@
import * as THREE from 'three';
import {
Color,
HemisphereLight,
MOUSE,
Object3D,
PerspectiveCamera,
Plane,
Raycaster,
Scene,
Vector2,
Vector3,
WebGLRenderer,
Intersection,
Mesh,
MeshLambertMaterial
} from 'three';
import { Color, HemisphereLight, Intersection, Mesh, MeshLambertMaterial, MOUSE, Object3D, PerspectiveCamera, Plane, Raycaster, Scene, Vector2, Vector3, WebGLRenderer } from 'three';
import OrbitControlsCreator from 'three-orbit-controls';
import { Vec3, Area, Quest, QuestEntity, QuestObject, QuestNpc, Section } from '../domain';
import { getAreaCollisionGeometry, getAreaRenderGeometry } from '../data/loading/areas';
import {
OBJECT_COLOR,
OBJECT_HOVER_COLOR,
OBJECT_SELECTED_COLOR,
NPC_COLOR,
NPC_HOVER_COLOR,
NPC_SELECTED_COLOR
} from './entities';
import { setSelectedEntity } from '../actions/quest-editor/questEditor';
import { getAreaCollisionGeometry, getAreaRenderGeometry } from '../bin-data/loading/areas';
import { Area, Quest, QuestEntity, QuestNpc, QuestObject, Section, Vec3 } from '../domain';
import { questEditorStore } from '../stores/QuestEditorStore';
import { NPC_COLOR, NPC_HOVER_COLOR, NPC_SELECTED_COLOR, OBJECT_COLOR, OBJECT_HOVER_COLOR, OBJECT_SELECTED_COLOR } from './entities';
const OrbitControls = OrbitControlsCreator(THREE);
@ -273,7 +251,7 @@ export class Renderer {
: oldSelectedData !== data;
if (selectionChanged) {
setSelectedEntity(data && data.entity);
questEditorStore.setSelectedEntity(data && data.entity);
}
}

View File

@ -1,5 +1,5 @@
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 { createNpcMesh, createObjectMesh, NPC_COLOR, OBJECT_COLOR } from './entities';

View 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();

View File

@ -1,8 +1,46 @@
import { observable, IObservableArray } from "mobx";
import { HuntMethod } from "../domain";
import { observable } from "mobx";
import { HuntMethod, NpcType, Server, SimpleNpc, SimpleQuest } from "../domain";
import { Loadable } from "../Loadable";
import { PerServer } from "./PerServer";
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();

View 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
}

View File

@ -1,8 +1,21 @@
import { observable, IObservableArray } from "mobx";
import { Item } from "../domain";
import { sortedUniq } from "lodash";
import { observable } from "mobx";
import { Item, Server } from "../domain";
import { Loadable } from "../Loadable";
import { PerServer } from "./PerServer";
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
View 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);
}
}

View File

@ -1,12 +1,150 @@
import { observable } from 'mobx';
import { observable, action } from 'mobx';
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 {
@observable currentModel?: Object3D;
@observable currentQuest?: Quest;
@observable currentArea?: Area;
@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();

View File

@ -13,8 +13,7 @@
}
.ApplicationComponent-heading {
font-size: 22px;
margin: 10px 10px 0 10px;
margin: 5px 10px 0 10px;
}
.ApplicationComponent-beta {

View File

@ -1,20 +1,11 @@
import React from "react";
import './HuntOptimizerComponent.css';
import { WantedItemsComponent } from "./WantedItemsComponent";
import { loadItems } from "../../actions/items";
import { loadHuntMethods } from "../../actions/huntMethods";
export class HuntOptimizerComponent extends React.Component {
componentDidMount() {
loadItems('ephinea');
loadHuntMethods('ephinea');
}
render() {
return (
<section className="ho-HuntOptimizerComponent">
<WantedItemsComponent />
</section>
);
}
export function HuntOptimizerComponent() {
return (
<section className="ho-HuntOptimizerComponent">
<WantedItemsComponent />
</section>
);
}

View File

@ -1,42 +1,41 @@
import { Button, InputNumber, Select, Table } from "antd";
import { observable } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import { Item } from "../../domain";
import { huntOptimizerStore, WantedItem } from "../../stores/HuntOptimizerStore";
import { itemStore } from "../../stores/ItemStore";
import './WantedItemsComponent.css';
@observer
export class WantedItemsComponent extends React.Component {
@observable
private wantedItems: Array<WantedItem> = [];
render() {
// Make sure render is called on updates.
this.wantedItems.slice(0, 0);
huntOptimizerStore.wantedItems.slice(0, 0);
return (
<section className="ho-WantedItemsComponent">
<h2>Wanted Items</h2>
<Select
value={undefined}
showSearch
placeholder="Add an item"
optionFilterProp="children"
style={{ width: 200 }}
filterOption
onChange={this.addWanted}
>
{itemStore.items.map(item => (
<Select.Option key={item.name}>
{item.name}
</Select.Option>
))}
</Select>
<div>
<Select
value={undefined}
showSearch
placeholder="Add an item"
optionFilterProp="children"
style={{ width: 200 }}
filterOption
onChange={this.addWanted}
>
{itemStore.items.current.value.map(item => (
<Select.Option key={item.name}>
{item.name}
</Select.Option>
))}
</Select>
<Button onClick={huntOptimizerStore.optimize}>Optimize</Button>
</div>
<Table
className="ho-WantedItemsComponent-table"
size="small"
dataSource={this.wantedItems}
dataSource={huntOptimizerStore.wantedItems}
rowKey={wanted => wanted.item.name}
pagination={false}
>
@ -59,30 +58,20 @@ export class WantedItemsComponent extends React.Component {
}
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) {
const item = itemStore.items.find(i => i.name === itemName)!;
this.wantedItems.push(new WantedItem(item, 1));
const item = itemStore.items.current.value.find(i => i.name === itemName)!;
huntOptimizerStore.wantedItems.push(new WantedItem(item, 1));
}
};
}
private removeWanted = (wanted: WantedItem) => () => {
const i = this.wantedItems.findIndex(w => w === wanted);
const i = huntOptimizerStore.wantedItems.findIndex(w => w === wanted);
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;
}
}

View File

@ -3,8 +3,6 @@ import { UploadChangeParam } from "antd/lib/upload";
import { UploadFile } from "antd/lib/upload/interface";
import { observer } from "mobx-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 { EntityInfoComponent } from "./EntityInfoComponent";
import './QuestEditorComponent.css';
@ -66,7 +64,7 @@ export class QuestEditorComponent extends React.Component<{}, {
}
private saveDialogAffirmed = () => {
saveCurrentQuestToFile(this.state.saveDialogFilename);
questEditorStore.saveCurrentQuestToFile(this.state.saveDialogFilename);
this.setState({ saveDialogOpen: false });
}
@ -98,8 +96,8 @@ class Toolbar extends React.Component<{ onSaveAsClicked: (filename?: string) =>
</Upload>
{areas && (
<Select
onChange={setCurrentAreaId}
defaultValue={areaId}
onChange={questEditorStore.setCurrentAreaId}
value={areaId}
style={{ width: 200 }}
>
{areas.map(area =>
@ -120,7 +118,7 @@ class Toolbar extends React.Component<{ onSaveAsClicked: (filename?: string) =>
private setFilename = (info: UploadChangeParam<UploadFile>) => {
if (info.file.originFileObj) {
this.setState({ filename: info.file.name });
loadFile(info.file.originFileObj);
questEditorStore.loadFile(info.file.originFileObj);
}
}

View File

@ -17,7 +17,7 @@
.qe-QuestInfoComponent pre {
padding: 8px;
border: solid 1px lightgray;
border: solid 1px hsl(200, 10%, 30%);
margin: 4px 0;
}

View File

@ -5521,6 +5521,11 @@ istanbul-reports@^2.1.1:
dependencies:
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:
version "24.8.0"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.8.0.tgz#7e7eb21cf687587a85e50f3d249d1327e15b157b"