Removed blueprintjs and added antd, refactored all code to use antd. Added list of wanted items to hunt optimizer.

This commit is contained in:
Daan Vanden Bosch 2019-05-31 23:20:13 +02:00
parent f324886240
commit 0dc55b5eb7
24 changed files with 1156 additions and 422 deletions

View File

@ -3,12 +3,12 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@blueprintjs/core": "^3.15.1",
"@types/jest": "24.0.13", "@types/jest": "24.0.13",
"@types/lodash": "^4.14.132", "@types/lodash": "^4.14.132",
"@types/react": "16.8.18", "@types/react": "16.8.18",
"@types/react-dom": "16.8.4", "@types/react-dom": "16.8.4",
"@types/text-encoding": "^0.0.35", "@types/text-encoding": "^0.0.35",
"antd": "^3.19.1",
"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",

15
src/actions/items.ts Normal file
View File

@ -0,0 +1,15 @@
import { getItems } from "../data/loading/items";
import { itemStore } from "../stores/ItemStore";
import { action } from "mobx";
import { memoize } from "lodash";
import { Item } from "../domain";
export const loadItems = memoize(
async (server: string) => {
setItems(await getItems(server));
}
);
const setItems = action('setItems', (items: Item[]) => {
itemStore.items.replace(items);
});

View File

@ -1,11 +1,10 @@
import { action } from 'mobx'; import { action } from 'mobx';
import { Object3D } from 'three';
import { ArrayBufferCursor } from '../../data/ArrayBufferCursor'; import { ArrayBufferCursor } from '../../data/ArrayBufferCursor';
import { getAreaSections } from '../../data/loading/areas'; import { getAreaSections } from '../../data/loading/areas';
import { getNpcGeometry, getObjectGeometry } from '../../data/loading/entities'; import { getNpcGeometry, getObjectGeometry } from '../../data/loading/entities';
import { parseNj, parseXj } from '../../data/parsing/ninja'; import { parseNj, parseXj } from '../../data/parsing/ninja';
import { parseQuest } from '../../data/parsing/quest'; import { parseQuest } from '../../data/parsing/quest';
import { AreaVariant, Section, Vec3, VisibleQuestEntity } from '../../domain'; import { Section, Vec3, VisibleQuestEntity } from '../../domain';
import { createNpcMesh, createObjectMesh } from '../../rendering/entities'; import { createNpcMesh, createObjectMesh } from '../../rendering/entities';
import { createModelMesh } from '../../rendering/models'; import { createModelMesh } from '../../rendering/models';
import { setModel, setQuest } from './questEditor'; import { setModel, setQuest } from './questEditor';
@ -35,14 +34,14 @@ async function loadend(file: File, reader: FileReader) {
// Load section data. // Load section data.
for (const variant of quest.areaVariants) { for (const variant of quest.areaVariants) {
const sections = await getAreaSections(quest.episode, variant.area.id, variant.id); const sections = await getAreaSections(quest.episode, variant.area.id, variant.id);
setSectionsOnAreaVariant(variant, sections); variant.sections = sections;
// Generate object geometry. // Generate object geometry.
for (const object of quest.objects.filter(o => o.areaId === variant.area.id)) { for (const object of quest.objects.filter(o => o.areaId === variant.area.id)) {
try { try {
const geometry = await getObjectGeometry(object.type); const geometry = await getObjectGeometry(object.type);
setSectionOnVisibleQuestEntity(object, sections); setSectionOnVisibleQuestEntity(object, sections);
setObject3dOnVisibleQuestEntity(object, createObjectMesh(object, geometry)); object.object3d = createObjectMesh(object, geometry);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@ -53,7 +52,7 @@ async function loadend(file: File, reader: FileReader) {
try { try {
const geometry = await getNpcGeometry(npc.type); const geometry = await getNpcGeometry(npc.type);
setSectionOnVisibleQuestEntity(npc, sections); setSectionOnVisibleQuestEntity(npc, sections);
setObject3dOnVisibleQuestEntity(npc, createNpcMesh(npc, geometry)); npc.object3d = createNpcMesh(npc, geometry);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@ -65,12 +64,6 @@ async function loadend(file: File, reader: FileReader) {
} }
} }
const setSectionsOnAreaVariant = action('setSectionsOnAreaVariant',
(variant: AreaVariant, sections: Section[]) => {
variant.sections = sections;
}
);
const setSectionOnVisibleQuestEntity = action('setSectionOnVisibleQuestEntity', const setSectionOnVisibleQuestEntity = action('setSectionOnVisibleQuestEntity',
(entity: VisibleQuestEntity, sections: Section[]) => { (entity: VisibleQuestEntity, sections: Section[]) => {
let { x, y, z } = entity.position; let { x, y, z } = entity.position;
@ -92,9 +85,3 @@ const setSectionOnVisibleQuestEntity = action('setSectionOnVisibleQuestEntity',
entity.position = new Vec3(x, y, z); entity.position = new Vec3(x, y, z);
} }
); );
const setObject3dOnVisibleQuestEntity = action('setObject3dOnVisibleQuestEntity',
(entity: VisibleQuestEntity, object3d: Object3D) => {
entity.object3d = object3d;
}
);

View File

@ -1,12 +0,0 @@
import { action } from "mobx";
import { VisibleQuestEntity, Vec3, Section } from "../../domain";
export const setPositionOnVisibleQuestEntity = action('setPositionOnVisibleQuestEntity',
(entity: VisibleQuestEntity, position: Vec3, section?: Section) => {
entity.position = position;
if (section) {
entity.section = section;
}
}
);

View File

@ -1,6 +1,6 @@
import { Object3D } from 'three'; import { Object3D } from 'three';
import { Section } from '../../domain'; import { Section } from '../../domain';
import { getAreaRenderData, getAreaCollisionData } from './assets'; import { getAreaRenderData, getAreaCollisionData } from './binaryAssets';
import { parseCRel, parseNRel } from '../parsing/geometry'; import { parseCRel, parseNRel } from '../parsing/geometry';
// //

View File

@ -17,23 +17,15 @@ export function getAreaCollisionData(
} }
export async function getNpcData(npcType: NpcType): Promise<{ url: string, data: ArrayBuffer }> { export async function getNpcData(npcType: NpcType): Promise<{ url: string, data: ArrayBuffer }> {
try { const url = npcTypeToUrl(npcType);
const url = npcTypeToUrl(npcType); const data = await getAsset(url);
const data = await getAsset(url); return ({ url, data });
return ({ url, data });
} catch (e) {
return Promise.reject(e);
}
} }
export async function getObjectData(objectType: ObjectType): Promise<{ url: string, data: ArrayBuffer }> { export async function getObjectData(objectType: ObjectType): Promise<{ url: string, data: ArrayBuffer }> {
try { const url = objectTypeToUrl(objectType);
const url = objectTypeToUrl(objectType); const data = await getAsset(url);
const data = await getAsset(url); return ({ url, data });
return ({ url, data });
} catch (e) {
return Promise.reject(e);
}
} }
/** /**

View File

@ -1,6 +1,6 @@
import { BufferGeometry } from 'three'; import { BufferGeometry } from 'three';
import { NpcType, ObjectType } from '../../domain'; import { NpcType, ObjectType } from '../../domain';
import { getNpcData, getObjectData } from './assets'; import { getNpcData, getObjectData } from './binaryAssets';
import { ArrayBufferCursor } from '../ArrayBufferCursor'; import { ArrayBufferCursor } from '../ArrayBufferCursor';
import { parseNj, parseXj } from '../parsing/ninja'; import { parseNj, parseXj } from '../parsing/ninja';

10
src/data/loading/items.ts Normal file
View File

@ -0,0 +1,10 @@
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

@ -265,3 +265,7 @@ export class AreaVariant {
throw new Error(`Expected id to be a non-negative integer, got ${id}.`); throw new Error(`Expected id to be a non-negative integer, got ${id}.`);
} }
} }
export class Item {
constructor(public name: string) { }
}

View File

@ -1,5 +1,6 @@
@import '~antd/dist/antd.css';
body { body {
background-color: #293742;
margin: 0; margin: 0;
} }

View File

@ -1,15 +1,7 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { ApplicationComponent } from './ui/ApplicationComponent';
import './index.css'; import './index.css';
import "normalize.css"; import { ApplicationComponent } from './ui/ApplicationComponent';
import "@blueprintjs/core/lib/css/blueprint.css";
import "@blueprintjs/icons/lib/css/blueprint-icons.css";
import { configure } from 'mobx';
configure({
enforceActions: 'observed'
});
ReactDOM.render( ReactDOM.render(
<ApplicationComponent />, <ApplicationComponent />,

View File

@ -27,7 +27,6 @@ import {
NPC_SELECTED_COLOR NPC_SELECTED_COLOR
} from './entities'; } from './entities';
import { setSelectedEntity } from '../actions/quest-editor/questEditor'; import { setSelectedEntity } from '../actions/quest-editor/questEditor';
import { setPositionOnVisibleQuestEntity as setPositionAndSectionOnVisibleQuestEntity } from '../actions/quest-editor/visibleQuestEntities';
const OrbitControls = OrbitControlsCreator(THREE); const OrbitControls = OrbitControlsCreator(THREE);
@ -309,11 +308,11 @@ export class Renderer {
const yDelta = y - data.entity.position.y; const yDelta = y - data.entity.position.y;
data.dragY += yDelta; data.dragY += yDelta;
data.dragAdjust.y -= yDelta; data.dragAdjust.y -= yDelta;
setPositionAndSectionOnVisibleQuestEntity(data.entity, new Vec3( data.entity.position = new Vec3(
data.entity.position.x, data.entity.position.x,
y, y,
data.entity.position.z data.entity.position.z
)); );
} }
} else { } else {
// Horizontal movement accross terrain. // Horizontal movement accross terrain.
@ -321,11 +320,12 @@ export class Renderer {
const { intersection, section } = this.pickTerrain(pointerPos, data); const { intersection, section } = this.pickTerrain(pointerPos, data);
if (intersection) { if (intersection) {
setPositionAndSectionOnVisibleQuestEntity(data.entity, new Vec3( data.entity.position = new Vec3(
intersection.point.x, intersection.point.x,
intersection.point.y + data.dragY, intersection.point.y + data.dragY,
intersection.point.z intersection.point.z
), section); );
data.entity.section = section;
} else { } else {
// If the cursor is not over any terrain, we translate the entity accross the horizontal plane in which the entity's origin lies. // If the cursor is not over any terrain, we translate the entity accross the horizontal plane in which the entity's origin lies.
this.raycaster.setFromCamera(pointerPos, this.camera); this.raycaster.setFromCamera(pointerPos, this.camera);
@ -337,11 +337,11 @@ export class Renderer {
const intersectionPoint = new Vector3(); const intersectionPoint = new Vector3();
if (ray.intersectPlane(plane, intersectionPoint)) { if (ray.intersectPlane(plane, intersectionPoint)) {
setPositionAndSectionOnVisibleQuestEntity(data.entity, new Vec3( data.entity.position = new Vec3(
intersectionPoint.x + data.grabOffset.x, intersectionPoint.x + data.grabOffset.x,
data.entity.position.y, data.entity.position.y,
intersectionPoint.z + data.grabOffset.z intersectionPoint.z + data.grabOffset.z
)); );
} }
} }
} }

8
src/stores/ItemStore.ts Normal file
View File

@ -0,0 +1,8 @@
import { observable, IObservableArray } from "mobx";
import { Item } from "../domain";
class ItemStore {
@observable items: IObservableArray<Item> = observable.array();
}
export const itemStore = new ItemStore();

View File

@ -8,14 +8,18 @@
right: 0; right: 0;
} }
div.ApplicationComponent .ApplicationComponent-heading { .ApplicationComponent-navbar {
display: flex;
}
.ApplicationComponent-heading {
font-size: 22px; font-size: 22px;
margin: 10px 10px 0 10px;
} }
.ApplicationComponent-beta { .ApplicationComponent-beta {
color: #f55656; color: #f55656;
font-weight: bold; font-weight: bold;
margin-left: 2;
} }
.ApplicationComponent-main { .ApplicationComponent-main {

View File

@ -1,48 +1,46 @@
import { Classes, Navbar, NavbarGroup, NavbarHeading, Button, Callout, Intent } from '@blueprintjs/core'; import { Alert, Menu } from 'antd';
import { ClickParam } from 'antd/lib/menu';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React from 'react'; import React from 'react';
import { QuestEditorComponent } from './quest-editor/QuestEditorComponent';
import { observable, action } from 'mobx';
import { HuntOptimizerComponent } from './hunt-optimizer/HuntOptimizerComponent';
import './ApplicationComponent.css'; import './ApplicationComponent.css';
import { HuntOptimizerComponent } from './hunt-optimizer/HuntOptimizerComponent';
import { QuestEditorComponent } from './quest-editor/QuestEditorComponent';
@observer @observer
export class ApplicationComponent extends React.Component { export class ApplicationComponent extends React.Component {
@observable private tool = 'quest-editor'; state = { tool: 'huntOptimizer' }
render() { render() {
let toolComponent; let toolComponent;
switch (this.tool) { switch (this.state.tool) {
case 'quest-editor': case 'questEditor':
toolComponent = <QuestEditorComponent />; toolComponent = <QuestEditorComponent />;
break; break;
case 'hunt-optimizer': case 'huntOptimizer':
toolComponent = <HuntOptimizerComponent />; toolComponent = <HuntOptimizerComponent />;
break; break;
} }
return ( return (
<div className={`ApplicationComponent ${Classes.DARK}`}> <div className="ApplicationComponent">
<Navbar> <div className="ApplicationComponent-navbar">
<NavbarGroup className="ApplicationComponent-button-bar"> <h1 className="ApplicationComponent-heading">
<NavbarHeading className="ApplicationComponent-heading"> Phantasmal World
Phantasmal World </h1>
</NavbarHeading> <Menu
<Button onClick={this.menuClicked}
text="Quest Editor (Beta)" selectedKeys={[this.state.tool]}
minimal={true} mode="horizontal"
active={this.tool === 'quest-editor'} >
onClick={() => this.setTool('quest-editor')} <Menu.Item key="questEditor">
/> Quest Editor<sup className="ApplicationComponent-beta">(Beta)</sup>
<Button </Menu.Item>
text="Hunt Optimizer" <Menu.Item key="huntOptimizer">
minimal={true} Hunt Optimizer
active={this.tool === 'hunt-optimizer'} </Menu.Item>
onClick={() => this.setTool('hunt-optimizer')} </Menu>
/> </div>
</NavbarGroup>
</Navbar>
<ErrorBoundary> <ErrorBoundary>
{toolComponent} {toolComponent}
</ErrorBoundary> </ErrorBoundary>
@ -50,9 +48,9 @@ export class ApplicationComponent extends React.Component {
); );
} }
private setTool = action('setTool', (tool: string) => { private menuClicked = (e: ClickParam) => {
this.tool = tool; this.setState({ tool: e.key });
}); };
} }
class ErrorBoundary extends React.Component { class ErrorBoundary extends React.Component {
@ -66,7 +64,7 @@ class ErrorBoundary extends React.Component {
{this.state.hasError ? ( {this.state.hasError ? (
<div className="ApplicationComponent-error"> <div className="ApplicationComponent-error">
<div> <div>
<Callout intent={Intent.DANGER} title="Something went wrong." /> <Alert type="error" message="Something went wrong." />
</div> </div>
</div> </div>
) : this.props.children} ) : this.props.children}

View File

@ -0,0 +1,8 @@
.HuntOptimizerComponent {
display: flex;
align-items: stretch;
}
.HuntOptimizerComponent-wanted-items {
margin: 10px;
}

View File

@ -1,10 +1,84 @@
import { Select, Table, Button } from "antd";
import { observable } from "mobx";
import { observer } from "mobx-react";
import React from "react"; import React from "react";
import { loadItems } from "../../actions/items";
import { Item } from "../../domain";
import { itemStore } from "../../stores/ItemStore";
import './HuntOptimizerComponent.css'; import './HuntOptimizerComponent.css';
export class HuntOptimizerComponent extends React.Component { export function HuntOptimizerComponent() {
return (
<section className="HuntOptimizerComponent">
<WantedItemsComponent />
</section>
);
}
class WantedItem {
@observable item: Item;
@observable amount: number;
constructor(item: Item, amount: number) {
this.item = item;
this.amount = amount;
}
}
@observer
class WantedItemsComponent extends React.Component {
@observable private wantedItems: Array<WantedItem> = [];
componentDidMount() {
loadItems('ephinea');
}
render() { render() {
// Make sure render is called on updates.
this.wantedItems.slice(0, 0);
return ( return (
<div></div> <section className="HuntOptimizerComponent-wanted-items">
<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>
<Table
size="small"
dataSource={this.wantedItems}
rowKey={wanted => wanted.item.name}
pagination={false}
>
<Table.Column title="Amount" dataIndex="amount" />
<Table.Column title="Item" dataIndex="item.name" />
<Table.Column
render={() => (
<Button type="link" icon="delete" />
)}
/>
</Table>
</section>
); );
} }
private addWanted = (itemName: string) => {
let added = this.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));
}
}
} }

View File

@ -9,7 +9,11 @@
border-collapse: collapse; border-collapse: collapse;
} }
.EntityInfoComponent-coord {
width: 100px !important;
}
.EntityInfoComponent-coord input { .EntityInfoComponent-coord input {
text-align: right; text-align: right;
padding-right: 10px !important; padding-right: 24px !important;
} }

View File

@ -1,34 +1,15 @@
import { NumericInput } from '@blueprintjs/core'; import { InputNumber } from 'antd';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React from 'react'; import React from 'react';
import { QuestNpc, QuestObject, VisibleQuestEntity } from '../../domain'; import { QuestNpc, QuestObject, VisibleQuestEntity } from '../../domain';
import './EntityInfoComponent.css'; import './EntityInfoComponent.css';
interface Props { interface Props {
entity?: VisibleQuestEntity entity?: VisibleQuestEntity;
} }
@observer @observer
export class EntityInfoComponent extends React.Component<Props, any> { export class EntityInfoComponent extends React.Component<Props> {
state = {
position: {
x: null,
y: null,
z: null,
},
sectionPosition: {
x: null,
y: null,
z: null,
}
};
componentWillReceiveProps({ entity }: Props) {
if (this.props.entity !== entity) {
this.clearPositionState();
}
}
render() { render() {
const entity = this.props.entity; const entity = this.props.entity;
@ -65,9 +46,9 @@ export class EntityInfoComponent extends React.Component<Props, any> {
<td colSpan={2}> <td colSpan={2}>
<table> <table>
<tbody> <tbody>
{this.coordRow('position', 'x')} <CoordRow entity={entity} positionType="position" coord="x" />
{this.coordRow('position', 'y')} <CoordRow entity={entity} positionType="position" coord="y" />
{this.coordRow('position', 'z')} <CoordRow entity={entity} positionType="position" coord="z" />
</tbody> </tbody>
</table> </table>
</td> </td>
@ -79,9 +60,9 @@ export class EntityInfoComponent extends React.Component<Props, any> {
<td colSpan={2}> <td colSpan={2}>
<table> <table>
<tbody> <tbody>
{this.coordRow('sectionPosition', 'x')} <CoordRow entity={entity} positionType="sectionPosition" coord="x" />
{this.coordRow('sectionPosition', 'y')} <CoordRow entity={entity} positionType="sectionPosition" coord="y" />
{this.coordRow('sectionPosition', 'z')} <CoordRow entity={entity} positionType="sectionPosition" coord="z" />
</tbody> </tbody>
</table> </table>
</td> </td>
@ -94,93 +75,40 @@ export class EntityInfoComponent extends React.Component<Props, any> {
return <div className="EntityInfoComponent-container" />; return <div className="EntityInfoComponent-container" />;
} }
} }
}
private coordRow(posType: string, coord: string) { @observer
if (this.props.entity) { class CoordRow extends React.Component<{
entity: VisibleQuestEntity,
positionType: 'position' | 'sectionPosition',
coord: 'x' | 'y' | 'z'
}> {
render() {
const entity = this.props.entity;
const value = entity[this.props.positionType][this.props.coord];
return (
<tr>
<td>{this.props.coord.toUpperCase()}: </td>
<td>
<InputNumber
value={value}
size="small"
precision={3}
className="EntityInfoComponent-coord"
onChange={this.changed}
/>
</td>
</tr>
);
}
private changed = (value?: number) => {
if (value != null) {
const entity = this.props.entity; const entity = this.props.entity;
const valueStr = (this.state as any)[posType][coord]; const posType = this.props.positionType;
const value = valueStr const pos = entity[posType].clone();
? valueStr pos[this.props.coord] = value;
// Do multiplication, rounding, division and || with zero to avoid numbers close to zero flickering between 0 and -0. entity[posType] = pos;
: (Math.round((entity as any)[posType][coord] * 10000) / 10000 || 0).toFixed(4);
return (
<tr>
<td>{coord.toUpperCase()}: </td>
<td>
<NumericInput
value={value}
className="EntityInfoComponent-coord"
fill={true}
buttonPosition="none"
onValueChange={(this.posChange as any)[posType][coord]}
onBlur={this.coordInputBlurred} />
</td>
</tr>
);
} else {
return null;
} }
} }
private posChange = {
position: {
x: (value: number, valueStr: string) => {
this.posChanged('position', 'x', value, valueStr);
},
y: (value: number, valueStr: string) => {
this.posChanged('position', 'y', value, valueStr);
},
z: (value: number, valueStr: string) => {
this.posChanged('position', 'z', value, valueStr);
}
},
sectionPosition: {
x: (value: number, valueStr: string) => {
this.posChanged('sectionPosition', 'x', value, valueStr);
},
y: (value: number, valueStr: string) => {
this.posChanged('sectionPosition', 'y', value, valueStr);
},
z: (value: number, valueStr: string) => {
this.posChanged('sectionPosition', 'z', value, valueStr);
}
}
};
private posChanged(posType: string, coord: string, value: number, valueStr: string) {
if (!isNaN(value)) {
const entity = this.props.entity as any;
if (entity) {
const v = entity[posType].clone();
v[coord] = value;
entity[posType] = v;
}
}
this.setState({
[posType]: {
[coord]: valueStr
}
});
}
private coordInputBlurred = () => {
this.clearPositionState();
}
private clearPositionState() {
this.setState({
position: {
x: null,
y: null,
z: null,
},
sectionPosition: {
x: null,
y: null,
z: null,
}
});
}
} }

View File

@ -1,18 +1,23 @@
.QuestEditorComponent { .qe-QuestEditorComponent {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.QuestEditorComponent-main { .qe-QuestEditorComponent-toolbar {
display: flex;
padding: 10px 5px;
}
.qe-QuestEditorComponent-toolbar > * {
margin: 0 5px;
}
.qe-QuestEditorComponent-main {
flex: 1; flex: 1;
display: flex; display: flex;
overflow: hidden; overflow: hidden;
} }
.QuestEditorComponent-button-bar>* { .qe-QuestEditorComponent-main div:nth-child(2) {
margin-right: 10px;
}
.QuestEditorComponent-main div:nth-child(2) {
flex: 1; flex: 1;
} }

View File

@ -1,8 +1,10 @@
import { Button, Classes, Dialog, FileInput, FormGroup, HTMLSelect, InputGroup, Intent, Navbar, NavbarGroup } from "@blueprintjs/core"; import { Button, Form, Icon, Input, Modal, Select, Upload } from "antd";
import { UploadChangeParam } from "antd/lib/upload";
import { UploadFile } from "antd/lib/upload/interface";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import React, { ChangeEvent, KeyboardEvent } from "react"; import React, { ChangeEvent } from "react";
import { saveCurrentQuestToFile, setCurrentAreaId } from "../../actions/quest-editor/questEditor";
import { loadFile } from "../../actions/quest-editor/loadFile"; 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';
@ -16,7 +18,6 @@ export class QuestEditorComponent extends React.Component<{}, {
saveDialogFilename: string saveDialogFilename: string
}> { }> {
state = { state = {
filename: undefined,
saveDialogOpen: false, saveDialogOpen: false,
saveDialogFilename: 'Untitled', saveDialogFilename: 'Untitled',
}; };
@ -24,103 +25,33 @@ export class QuestEditorComponent extends React.Component<{}, {
render() { render() {
const quest = questEditorStore.currentQuest; const quest = questEditorStore.currentQuest;
const model = questEditorStore.currentModel; const model = questEditorStore.currentModel;
const areas = quest && Array.from(quest.areaVariants).map(a => a.area);
const area = questEditorStore.currentArea; const area = questEditorStore.currentArea;
const areaId = area && String(area.id);
return ( return (
<div className="QuestEditorComponent"> <div className="qe-QuestEditorComponent">
<Navbar> <Toolbar onSaveAsClicked={this.saveAsClicked} />
<NavbarGroup className="QuestEditorComponent-button-bar"> <div className="qe-QuestEditorComponent-main">
<FileInput
text={this.state.filename || 'Choose file...'}
inputProps={{
type: 'file',
accept: '.nj, .qst, .xj',
onChange: this.onFileChange
}}
/>
{areas ? (
<HTMLSelect
onChange={this.onAreaSelectChange}
defaultValue={areaId}
>
{areas.map(area =>
<option key={area.id} value={area.id}>{area.name}</option>
)}
</HTMLSelect>
) : null}
{quest ? (
<Button
text="Save as..."
icon="floppy-disk"
onClick={this.onSaveAsClick}
/>
) : null}
</NavbarGroup>
</Navbar>
<div className="QuestEditorComponent-main">
<QuestInfoComponent quest={quest} /> <QuestInfoComponent quest={quest} />
<RendererComponent <RendererComponent
quest={quest} quest={quest}
area={area} area={area}
model={model} /> model={model}
/>
<EntityInfoComponent entity={questEditorStore.selectedEntity} /> <EntityInfoComponent entity={questEditorStore.selectedEntity} />
</div> </div>
<Dialog <SaveAsForm
title="Save as..."
icon="floppy-disk"
className={Classes.DARK}
style={{ width: 360 }}
isOpen={this.state.saveDialogOpen} isOpen={this.state.saveDialogOpen}
onClose={this.onSaveDialogClose}> filename={this.state.saveDialogFilename}
<div className={Classes.DIALOG_BODY}> onFilenameChange={this.saveDialogFilenameChanged}
<FormGroup label="Name:" labelFor="file-name-input"> onOk={this.saveDialogAffirmed}
<InputGroup onCancel={this.saveDialogCancelled}
id="file-name-input" />
autoFocus={true}
value={this.state.saveDialogFilename}
maxLength={12}
onChange={this.onSaveDialogNameChange}
onKeyUp={this.onSaveDialogNameKeyUp}
/>
</FormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
text="Save"
style={{ marginLeft: 10 }}
onClick={this.onSaveDialogSaveClick}
intent={Intent.PRIMARY} />
</div>
</div>
</Dialog>
</div> </div>
); );
} }
private onFileChange = (e: ChangeEvent<HTMLInputElement>) => { private saveAsClicked = (filename: string) => {
if (e.currentTarget.files) { const name = filename.endsWith('.qst') ? filename.slice(0, -4) : filename;
const file = e.currentTarget.files[0];
if (file) {
this.setState({
filename: file.name
});
loadFile(file);
}
}
}
private onAreaSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
const areaId = parseInt(e.currentTarget.value, 10);
setCurrentAreaId(areaId);
}
private onSaveAsClick = () => {
let name = this.state.filename || 'Untitled';
name = name.endsWith('.qst') ? name.slice(0, -4) : name;
this.setState({ this.setState({
saveDialogOpen: true, saveDialogOpen: true,
@ -128,22 +59,104 @@ export class QuestEditorComponent extends React.Component<{}, {
}); });
} }
private onSaveDialogNameChange = (e: ChangeEvent<HTMLInputElement>) => { private saveDialogFilenameChanged = (filename: string) => {
this.setState({ saveDialogFilename: e.currentTarget.value }); this.setState({ saveDialogFilename: filename });
} }
private onSaveDialogNameKeyUp = (e: KeyboardEvent<HTMLInputElement>) => { private saveDialogAffirmed = () => {
if (e.key === 'Enter') {
this.onSaveDialogSaveClick();
}
}
private onSaveDialogSaveClick = () => {
saveCurrentQuestToFile(this.state.saveDialogFilename); saveCurrentQuestToFile(this.state.saveDialogFilename);
this.setState({ saveDialogOpen: false }); this.setState({ saveDialogOpen: false });
} }
private onSaveDialogClose = () => { private saveDialogCancelled = () => {
this.setState({ saveDialogOpen: false }); this.setState({ saveDialogOpen: false });
} }
} }
@observer
class Toolbar extends React.Component<{ onSaveAsClicked: (filename: string) => void }> {
state = {
filename: 'Choose file...'
}
render() {
const quest = questEditorStore.currentQuest;
const areas = quest && Array.from(quest.areaVariants).map(a => a.area);
const area = questEditorStore.currentArea;
const areaId = area && area.id;
return (
<div className="qe-QuestEditorComponent-toolbar">
<Upload
accept=".nj, .qst, .xj"
showUploadList={false}
onChange={this.setFilename}
>
<Button icon="file">{this.state.filename}</Button>
</Upload>
{areas && (
<Select
onChange={setCurrentAreaId}
defaultValue={areaId}
style={{ width: 200 }}
>
{areas.map(area =>
<Select.Option key={area.id} value={area.id}>{area.name}</Select.Option>
)}
</Select>
)}
{quest && (
<Button
icon="save"
onClick={this.saveAsClicked}
>Save as...</Button>
)}
</div>
);
}
private setFilename = (info: UploadChangeParam<UploadFile>) => {
if (info.file.originFileObj) {
this.setState({ filename: info.file.name });
loadFile(info.file.originFileObj);
}
}
private saveAsClicked = () => {
this.props.onSaveAsClicked(this.state.filename);
}
}
class SaveAsForm extends React.Component<{
isOpen: boolean,
filename: string,
onFilenameChange: (name: string) => void,
onOk: () => void,
onCancel: () => void
}> {
render() {
return (
<Modal
title={<><Icon type="save" /> Save as...</>}
visible={this.props.isOpen}
onOk={this.props.onOk}
onCancel={this.props.onCancel}
>
<Form layout="vertical">
<Form.Item label="Name">
<Input
autoFocus={true}
maxLength={12}
value={this.props.filename}
onChange={this.nameChanged}
/>
</Form.Item>
</Form>
</Modal>
);
}
private nameChanged = (e: ChangeEvent<HTMLInputElement>) => {
this.props.onFilenameChange(e.currentTarget.value);
}
}

View File

@ -1,20 +1,26 @@
.QuestInfoComponent { .qe-QuestInfoComponent {
width: 280px; width: 280px;
padding: 10px; padding: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.QuestInfoComponent table { .qe-QuestInfoComponent table {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
} }
.QuestInfoComponent table tbody th { .qe-QuestInfoComponent table tbody th {
text-align: right; text-align: right;
padding-right: 5px; padding-right: 5px;
} }
.QuestInfoComponent-npc-counts-container { .qe-QuestInfoComponent pre {
padding: 8px;
border: solid 1px lightgray;
margin: 4px 0;
}
.qe-QuestInfoComponent-npc-counts-container {
overflow: auto; overflow: auto;
} }

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { NpcType, Quest } from '../../domain'; import { NpcType, Quest } from '../../domain';
import { Pre } from '@blueprintjs/core';
import './QuestInfoComponent.css'; import './QuestInfoComponent.css';
export function QuestInfoComponent({ quest }: { quest?: Quest }) { export function QuestInfoComponent({ quest }: { quest?: Quest }) {
@ -29,7 +28,7 @@ export function QuestInfoComponent({ quest }: { quest?: Quest }) {
}); });
return ( return (
<div className="QuestInfoComponent"> <div className="qe-QuestInfoComponent">
<table> <table>
<tbody> <tbody>
<tr> <tr>
@ -40,17 +39,17 @@ export function QuestInfoComponent({ quest }: { quest?: Quest }) {
</tr> </tr>
<tr> <tr>
<td colSpan={2}> <td colSpan={2}>
<Pre>{quest.shortDescription}</Pre> <pre>{quest.shortDescription}</pre>
</td> </td>
</tr> </tr>
<tr> <tr>
<td colSpan={2}> <td colSpan={2}>
<Pre>{quest.longDescription}</Pre> <pre>{quest.longDescription}</pre>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div className="QuestInfoComponent-npc-counts-container"> <div className="qe-QuestInfoComponent-npc-counts-container">
<table > <table >
<thead> <thead>
<tr><th colSpan={2}>NPC Counts</th></tr> <tr><th colSpan={2}>NPC Counts</th></tr>
@ -63,6 +62,6 @@ export function QuestInfoComponent({ quest }: { quest?: Quest }) {
</div> </div>
); );
} else { } else {
return <div className="QuestInfoComponent" />; return <div className="qe-QuestInfoComponent" />;
} }
} }

886
yarn.lock

File diff suppressed because it is too large Load Diff