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",
"private": true,
"dependencies": {
"@blueprintjs/core": "^3.15.1",
"@types/jest": "24.0.13",
"@types/lodash": "^4.14.132",
"@types/react": "16.8.18",
"@types/react-dom": "16.8.4",
"@types/text-encoding": "^0.0.35",
"antd": "^3.19.1",
"lodash": "^4.17.11",
"mobx": "^5.9.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 { Object3D } from 'three';
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 { AreaVariant, Section, Vec3, VisibleQuestEntity } from '../../domain';
import { Section, Vec3, VisibleQuestEntity } from '../../domain';
import { createNpcMesh, createObjectMesh } from '../../rendering/entities';
import { createModelMesh } from '../../rendering/models';
import { setModel, setQuest } from './questEditor';
@ -35,14 +34,14 @@ async function loadend(file: File, reader: FileReader) {
// Load section data.
for (const variant of quest.areaVariants) {
const sections = await getAreaSections(quest.episode, variant.area.id, variant.id);
setSectionsOnAreaVariant(variant, sections);
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);
setObject3dOnVisibleQuestEntity(object, createObjectMesh(object, geometry));
object.object3d = createObjectMesh(object, geometry);
} catch (e) {
console.error(e);
}
@ -53,7 +52,7 @@ async function loadend(file: File, reader: FileReader) {
try {
const geometry = await getNpcGeometry(npc.type);
setSectionOnVisibleQuestEntity(npc, sections);
setObject3dOnVisibleQuestEntity(npc, createNpcMesh(npc, geometry));
npc.object3d = createNpcMesh(npc, geometry);
} catch (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',
(entity: VisibleQuestEntity, sections: Section[]) => {
let { x, y, z } = entity.position;
@ -92,9 +85,3 @@ const setSectionOnVisibleQuestEntity = action('setSectionOnVisibleQuestEntity',
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 { Section } from '../../domain';
import { getAreaRenderData, getAreaCollisionData } from './assets';
import { getAreaRenderData, getAreaCollisionData } from './binaryAssets';
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 }> {
try {
const url = npcTypeToUrl(npcType);
const data = await getAsset(url);
return ({ url, data });
} catch (e) {
return Promise.reject(e);
}
}
export async function getObjectData(objectType: ObjectType): Promise<{ url: string, data: ArrayBuffer }> {
try {
const url = objectTypeToUrl(objectType);
const data = await getAsset(url);
return ({ url, data });
} catch (e) {
return Promise.reject(e);
}
}
/**

View File

@ -1,6 +1,6 @@
import { BufferGeometry } from 'three';
import { NpcType, ObjectType } from '../../domain';
import { getNpcData, getObjectData } from './assets';
import { getNpcData, getObjectData } from './binaryAssets';
import { ArrayBufferCursor } from '../ArrayBufferCursor';
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}.`);
}
}
export class Item {
constructor(public name: string) { }
}

View File

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

View File

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

View File

@ -27,7 +27,6 @@ import {
NPC_SELECTED_COLOR
} from './entities';
import { setSelectedEntity } from '../actions/quest-editor/questEditor';
import { setPositionOnVisibleQuestEntity as setPositionAndSectionOnVisibleQuestEntity } from '../actions/quest-editor/visibleQuestEntities';
const OrbitControls = OrbitControlsCreator(THREE);
@ -309,11 +308,11 @@ export class Renderer {
const yDelta = y - data.entity.position.y;
data.dragY += yDelta;
data.dragAdjust.y -= yDelta;
setPositionAndSectionOnVisibleQuestEntity(data.entity, new Vec3(
data.entity.position = new Vec3(
data.entity.position.x,
y,
data.entity.position.z
));
);
}
} else {
// Horizontal movement accross terrain.
@ -321,11 +320,12 @@ export class Renderer {
const { intersection, section } = this.pickTerrain(pointerPos, data);
if (intersection) {
setPositionAndSectionOnVisibleQuestEntity(data.entity, new Vec3(
data.entity.position = new Vec3(
intersection.point.x,
intersection.point.y + data.dragY,
intersection.point.z
), section);
);
data.entity.section = section;
} else {
// If the cursor is not over any terrain, we translate the entity accross the horizontal plane in which the entity's origin lies.
this.raycaster.setFromCamera(pointerPos, this.camera);
@ -337,11 +337,11 @@ export class Renderer {
const intersectionPoint = new Vector3();
if (ray.intersectPlane(plane, intersectionPoint)) {
setPositionAndSectionOnVisibleQuestEntity(data.entity, new Vec3(
data.entity.position = new Vec3(
intersectionPoint.x + data.grabOffset.x,
data.entity.position.y,
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;
}
div.ApplicationComponent .ApplicationComponent-heading {
.ApplicationComponent-navbar {
display: flex;
}
.ApplicationComponent-heading {
font-size: 22px;
margin: 10px 10px 0 10px;
}
.ApplicationComponent-beta {
color: #f55656;
font-weight: bold;
margin-left: 2;
}
.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 React from 'react';
import { QuestEditorComponent } from './quest-editor/QuestEditorComponent';
import { observable, action } from 'mobx';
import { HuntOptimizerComponent } from './hunt-optimizer/HuntOptimizerComponent';
import './ApplicationComponent.css';
import { HuntOptimizerComponent } from './hunt-optimizer/HuntOptimizerComponent';
import { QuestEditorComponent } from './quest-editor/QuestEditorComponent';
@observer
export class ApplicationComponent extends React.Component {
@observable private tool = 'quest-editor';
state = { tool: 'huntOptimizer' }
render() {
let toolComponent;
switch (this.tool) {
case 'quest-editor':
switch (this.state.tool) {
case 'questEditor':
toolComponent = <QuestEditorComponent />;
break;
case 'hunt-optimizer':
case 'huntOptimizer':
toolComponent = <HuntOptimizerComponent />;
break;
}
return (
<div className={`ApplicationComponent ${Classes.DARK}`}>
<Navbar>
<NavbarGroup className="ApplicationComponent-button-bar">
<NavbarHeading className="ApplicationComponent-heading">
<div className="ApplicationComponent">
<div className="ApplicationComponent-navbar">
<h1 className="ApplicationComponent-heading">
Phantasmal World
</NavbarHeading>
<Button
text="Quest Editor (Beta)"
minimal={true}
active={this.tool === 'quest-editor'}
onClick={() => this.setTool('quest-editor')}
/>
<Button
text="Hunt Optimizer"
minimal={true}
active={this.tool === 'hunt-optimizer'}
onClick={() => this.setTool('hunt-optimizer')}
/>
</NavbarGroup>
</Navbar>
</h1>
<Menu
onClick={this.menuClicked}
selectedKeys={[this.state.tool]}
mode="horizontal"
>
<Menu.Item key="questEditor">
Quest Editor<sup className="ApplicationComponent-beta">(Beta)</sup>
</Menu.Item>
<Menu.Item key="huntOptimizer">
Hunt Optimizer
</Menu.Item>
</Menu>
</div>
<ErrorBoundary>
{toolComponent}
</ErrorBoundary>
@ -50,9 +48,9 @@ export class ApplicationComponent extends React.Component {
);
}
private setTool = action('setTool', (tool: string) => {
this.tool = tool;
});
private menuClicked = (e: ClickParam) => {
this.setState({ tool: e.key });
};
}
class ErrorBoundary extends React.Component {
@ -66,7 +64,7 @@ class ErrorBoundary extends React.Component {
{this.state.hasError ? (
<div className="ApplicationComponent-error">
<div>
<Callout intent={Intent.DANGER} title="Something went wrong." />
<Alert type="error" message="Something went wrong." />
</div>
</div>
) : 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 { loadItems } from "../../actions/items";
import { Item } from "../../domain";
import { itemStore } from "../../stores/ItemStore";
import './HuntOptimizerComponent.css';
export class HuntOptimizerComponent extends React.Component {
render() {
export function HuntOptimizerComponent() {
return (
<div></div>
<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() {
// Make sure render is called on updates.
this.wantedItems.slice(0, 0);
return (
<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;
}
.EntityInfoComponent-coord {
width: 100px !important;
}
.EntityInfoComponent-coord input {
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 React from 'react';
import { QuestNpc, QuestObject, VisibleQuestEntity } from '../../domain';
import './EntityInfoComponent.css';
interface Props {
entity?: VisibleQuestEntity
entity?: VisibleQuestEntity;
}
@observer
export class EntityInfoComponent extends React.Component<Props, any> {
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();
}
}
export class EntityInfoComponent extends React.Component<Props> {
render() {
const entity = this.props.entity;
@ -65,9 +46,9 @@ export class EntityInfoComponent extends React.Component<Props, any> {
<td colSpan={2}>
<table>
<tbody>
{this.coordRow('position', 'x')}
{this.coordRow('position', 'y')}
{this.coordRow('position', 'z')}
<CoordRow entity={entity} positionType="position" coord="x" />
<CoordRow entity={entity} positionType="position" coord="y" />
<CoordRow entity={entity} positionType="position" coord="z" />
</tbody>
</table>
</td>
@ -79,9 +60,9 @@ export class EntityInfoComponent extends React.Component<Props, any> {
<td colSpan={2}>
<table>
<tbody>
{this.coordRow('sectionPosition', 'x')}
{this.coordRow('sectionPosition', 'y')}
{this.coordRow('sectionPosition', 'z')}
<CoordRow entity={entity} positionType="sectionPosition" coord="x" />
<CoordRow entity={entity} positionType="sectionPosition" coord="y" />
<CoordRow entity={entity} positionType="sectionPosition" coord="z" />
</tbody>
</table>
</td>
@ -94,93 +75,40 @@ export class EntityInfoComponent extends React.Component<Props, any> {
return <div className="EntityInfoComponent-container" />;
}
}
}
private coordRow(posType: string, coord: string) {
if (this.props.entity) {
@observer
class CoordRow extends React.Component<{
entity: VisibleQuestEntity,
positionType: 'position' | 'sectionPosition',
coord: 'x' | 'y' | 'z'
}> {
render() {
const entity = this.props.entity;
const valueStr = (this.state as any)[posType][coord];
const value = valueStr
? valueStr
// Do multiplication, rounding, division and || with zero to avoid numbers close to zero flickering between 0 and -0.
: (Math.round((entity as any)[posType][coord] * 10000) / 10000 || 0).toFixed(4);
const value = entity[this.props.positionType][this.props.coord];
return (
<tr>
<td>{coord.toUpperCase()}: </td>
<td>{this.props.coord.toUpperCase()}: </td>
<td>
<NumericInput
<InputNumber
value={value}
size="small"
precision={3}
className="EntityInfoComponent-coord"
fill={true}
buttonPosition="none"
onValueChange={(this.posChange as any)[posType][coord]}
onBlur={this.coordInputBlurred} />
onChange={this.changed}
/>
</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 changed = (value?: number) => {
if (value != null) {
const entity = this.props.entity;
const posType = this.props.positionType;
const pos = entity[posType].clone();
pos[this.props.coord] = value;
entity[posType] = pos;
}
}
};
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;
flex-direction: column;
}
.QuestEditorComponent-main {
.qe-QuestEditorComponent-toolbar {
display: flex;
padding: 10px 5px;
}
.qe-QuestEditorComponent-toolbar > * {
margin: 0 5px;
}
.qe-QuestEditorComponent-main {
flex: 1;
display: flex;
overflow: hidden;
}
.QuestEditorComponent-button-bar>* {
margin-right: 10px;
}
.QuestEditorComponent-main div:nth-child(2) {
.qe-QuestEditorComponent-main div:nth-child(2) {
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 React, { ChangeEvent, KeyboardEvent } from "react";
import { saveCurrentQuestToFile, setCurrentAreaId } from "../../actions/quest-editor/questEditor";
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';
@ -16,7 +18,6 @@ export class QuestEditorComponent extends React.Component<{}, {
saveDialogFilename: string
}> {
state = {
filename: undefined,
saveDialogOpen: false,
saveDialogFilename: 'Untitled',
};
@ -24,103 +25,33 @@ export class QuestEditorComponent extends React.Component<{}, {
render() {
const quest = questEditorStore.currentQuest;
const model = questEditorStore.currentModel;
const areas = quest && Array.from(quest.areaVariants).map(a => a.area);
const area = questEditorStore.currentArea;
const areaId = area && String(area.id);
return (
<div className="QuestEditorComponent">
<Navbar>
<NavbarGroup className="QuestEditorComponent-button-bar">
<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">
<div className="qe-QuestEditorComponent">
<Toolbar onSaveAsClicked={this.saveAsClicked} />
<div className="qe-QuestEditorComponent-main">
<QuestInfoComponent quest={quest} />
<RendererComponent
quest={quest}
area={area}
model={model} />
model={model}
/>
<EntityInfoComponent entity={questEditorStore.selectedEntity} />
</div>
<Dialog
title="Save as..."
icon="floppy-disk"
className={Classes.DARK}
style={{ width: 360 }}
<SaveAsForm
isOpen={this.state.saveDialogOpen}
onClose={this.onSaveDialogClose}>
<div className={Classes.DIALOG_BODY}>
<FormGroup label="Name:" labelFor="file-name-input">
<InputGroup
id="file-name-input"
autoFocus={true}
value={this.state.saveDialogFilename}
maxLength={12}
onChange={this.onSaveDialogNameChange}
onKeyUp={this.onSaveDialogNameKeyUp}
filename={this.state.saveDialogFilename}
onFilenameChange={this.saveDialogFilenameChanged}
onOk={this.saveDialogAffirmed}
onCancel={this.saveDialogCancelled}
/>
</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>
);
}
private onFileChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.currentTarget.files) {
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;
private saveAsClicked = (filename: string) => {
const name = filename.endsWith('.qst') ? filename.slice(0, -4) : filename;
this.setState({
saveDialogOpen: true,
@ -128,22 +59,104 @@ export class QuestEditorComponent extends React.Component<{}, {
});
}
private onSaveDialogNameChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ saveDialogFilename: e.currentTarget.value });
private saveDialogFilenameChanged = (filename: string) => {
this.setState({ saveDialogFilename: filename });
}
private onSaveDialogNameKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
this.onSaveDialogSaveClick();
}
}
private onSaveDialogSaveClick = () => {
private saveDialogAffirmed = () => {
saveCurrentQuestToFile(this.state.saveDialogFilename);
this.setState({ saveDialogOpen: false });
}
private onSaveDialogClose = () => {
private saveDialogCancelled = () => {
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;
padding: 10px;
display: flex;
flex-direction: column;
}
.QuestInfoComponent table {
.qe-QuestInfoComponent table {
border-collapse: collapse;
width: 100%;
}
.QuestInfoComponent table tbody th {
.qe-QuestInfoComponent table tbody th {
text-align: right;
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;
}

View File

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

886
yarn.lock

File diff suppressed because it is too large Load Diff