mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-07 08:48:28 +08:00
Refactored code so that new tools can now be added easily.
This commit is contained in:
parent
eacf826fc8
commit
f31570d5f5
@ -1,14 +1,14 @@
|
||||
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 { createNpcMesh, createObjectMesh } from '../rendering/entities';
|
||||
import { createModelMesh } from '../rendering/models';
|
||||
import { setModel, setQuest } from './appState';
|
||||
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 { createNpcMesh, createObjectMesh } from '../../rendering/entities';
|
||||
import { createModelMesh } from '../../rendering/models';
|
||||
import { setModel, setQuest } from './questEditor';
|
||||
|
||||
export function loadFile(file: File) {
|
||||
const reader = new FileReader();
|
@ -1,6 +1,6 @@
|
||||
import { writeQuestQst } from '../data/parsing/quest';
|
||||
import { VisibleQuestEntity, Quest } from '../domain';
|
||||
import { appStateStore } from '../stores/AppStateStore';
|
||||
import { writeQuestQst } from '../../data/parsing/quest';
|
||||
import { VisibleQuestEntity, Quest } from '../../domain';
|
||||
import { questEditorStore } from '../../stores/QuestEditorStore';
|
||||
import { action } from 'mobx';
|
||||
import { Object3D } from 'three';
|
||||
|
||||
@ -9,7 +9,7 @@ import { Object3D } from 'three';
|
||||
*/
|
||||
export const setModel = action('setModel', (model?: Object3D) => {
|
||||
resetModelAndQuestState();
|
||||
appStateStore.currentModel = model;
|
||||
questEditorStore.currentModel = model;
|
||||
});
|
||||
|
||||
/**
|
||||
@ -17,39 +17,39 @@ export const setModel = action('setModel', (model?: Object3D) => {
|
||||
*/
|
||||
export const setQuest = action('setQuest', (quest?: Quest) => {
|
||||
resetModelAndQuestState();
|
||||
appStateStore.currentQuest = quest;
|
||||
questEditorStore.currentQuest = quest;
|
||||
|
||||
if (quest && quest.areaVariants.length) {
|
||||
appStateStore.currentArea = quest.areaVariants[0].area;
|
||||
questEditorStore.currentArea = quest.areaVariants[0].area;
|
||||
}
|
||||
});
|
||||
|
||||
function resetModelAndQuestState() {
|
||||
appStateStore.currentQuest = undefined;
|
||||
appStateStore.currentArea = undefined;
|
||||
appStateStore.selectedEntity = undefined;
|
||||
appStateStore.currentModel = undefined;
|
||||
questEditorStore.currentQuest = undefined;
|
||||
questEditorStore.currentArea = undefined;
|
||||
questEditorStore.selectedEntity = undefined;
|
||||
questEditorStore.currentModel = undefined;
|
||||
}
|
||||
|
||||
export const setSelectedEntity = action('setSelectedEntity', (entity?: VisibleQuestEntity) => {
|
||||
appStateStore.selectedEntity = entity;
|
||||
questEditorStore.selectedEntity = entity;
|
||||
});
|
||||
|
||||
export const setCurrentAreaId = action('setCurrentAreaId', (areaId?: number) => {
|
||||
appStateStore.selectedEntity = undefined;
|
||||
questEditorStore.selectedEntity = undefined;
|
||||
|
||||
if (areaId == null) {
|
||||
appStateStore.currentArea = undefined;
|
||||
} else if (appStateStore.currentQuest) {
|
||||
const areaVariant = appStateStore.currentQuest.areaVariants.find(
|
||||
questEditorStore.currentArea = undefined;
|
||||
} else if (questEditorStore.currentQuest) {
|
||||
const areaVariant = questEditorStore.currentQuest.areaVariants.find(
|
||||
variant => variant.area.id === areaId);
|
||||
appStateStore.currentArea = areaVariant && areaVariant.area;
|
||||
questEditorStore.currentArea = areaVariant && areaVariant.area;
|
||||
}
|
||||
});
|
||||
|
||||
export const saveCurrentQuestToFile = (fileName: string) => {
|
||||
if (appStateStore.currentQuest) {
|
||||
const cursor = writeQuestQst(appStateStore.currentQuest, fileName);
|
||||
if (questEditorStore.currentQuest) {
|
||||
const cursor = writeQuestQst(questEditorStore.currentQuest, fileName);
|
||||
|
||||
if (!fileName.endsWith('.qst')) {
|
||||
fileName += '.qst';
|
@ -1,5 +1,5 @@
|
||||
import { action } from "mobx";
|
||||
import { VisibleQuestEntity, Vec3, Section } from "../domain";
|
||||
import { VisibleQuestEntity, Vec3, Section } from "../../domain";
|
||||
|
||||
export const setPositionOnVisibleQuestEntity = action('setPositionOnVisibleQuestEntity',
|
||||
(entity: VisibleQuestEntity, position: Vec3, section?: Section) => {
|
@ -26,8 +26,8 @@ import {
|
||||
NPC_HOVER_COLOR,
|
||||
NPC_SELECTED_COLOR
|
||||
} from './entities';
|
||||
import { setSelectedEntity } from '../actions/appState';
|
||||
import { setPositionOnVisibleQuestEntity as setPositionAndSectionOnVisibleQuestEntity } from '../actions/visibleQuestEntities';
|
||||
import { setSelectedEntity } from '../actions/quest-editor/questEditor';
|
||||
import { setPositionOnVisibleQuestEntity as setPositionAndSectionOnVisibleQuestEntity } from '../actions/quest-editor/visibleQuestEntities';
|
||||
|
||||
const OrbitControls = OrbitControlsCreator(THREE);
|
||||
|
||||
@ -41,7 +41,7 @@ interface PickEntityResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders one quest area at a time.
|
||||
* Renders a quest area or an NJ/XJ model.
|
||||
*/
|
||||
export class Renderer {
|
||||
private renderer = new WebGLRenderer({ antialias: true });
|
||||
|
@ -2,11 +2,11 @@ import { observable } from 'mobx';
|
||||
import { Object3D } from 'three';
|
||||
import { Area, Quest, VisibleQuestEntity } from '../domain';
|
||||
|
||||
class AppStateStore {
|
||||
class QuestEditorStore {
|
||||
@observable currentModel?: Object3D;
|
||||
@observable currentQuest?: Quest;
|
||||
@observable currentArea?: Area;
|
||||
@observable selectedEntity?: VisibleQuestEntity;
|
||||
}
|
||||
|
||||
export const appStateStore = new AppStateStore();
|
||||
export const questEditorStore = new QuestEditorStore();
|
@ -8,8 +8,8 @@
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.ApplicationComponent-heading {
|
||||
font-size: 22px !important;
|
||||
div.ApplicationComponent .ApplicationComponent-heading {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.ApplicationComponent-beta {
|
||||
@ -18,16 +18,13 @@
|
||||
margin-left: 2;
|
||||
}
|
||||
|
||||
.ApplicationComponent-button-bar > * {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.ApplicationComponent-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ApplicationComponent-main div:nth-child(2) {
|
||||
.ApplicationComponent-main>* {
|
||||
flex: 1;
|
||||
}
|
@ -1,32 +1,22 @@
|
||||
import { Button, Dialog, Intent, Classes, Navbar, NavbarGroup, NavbarHeading, FileInput, HTMLSelect, FormGroup, InputGroup } from '@blueprintjs/core';
|
||||
import { Classes, Navbar, NavbarGroup, NavbarHeading, Button } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { saveCurrentQuestToFile, setCurrentAreaId } from '../actions/appState';
|
||||
import { loadFile } from '../actions/loadFile';
|
||||
import { appStateStore } from '../stores/AppStateStore';
|
||||
import React from 'react';
|
||||
import './ApplicationComponent.css';
|
||||
import { RendererComponent } from './RendererComponent';
|
||||
import { EntityInfoComponent } from './EntityInfoComponent';
|
||||
import { QuestInfoComponent } from './QuestInfoComponent';
|
||||
import { QuestEditorComponent } from './quest-editor/QuestEditorComponent';
|
||||
import { observable, action } from 'mobx';
|
||||
|
||||
@observer
|
||||
export class ApplicationComponent extends React.Component<{}, {
|
||||
filename?: string,
|
||||
saveDialogOpen: boolean,
|
||||
saveDialogFilename: string
|
||||
}> {
|
||||
state = {
|
||||
filename: undefined,
|
||||
saveDialogOpen: false,
|
||||
saveDialogFilename: 'Untitled',
|
||||
};
|
||||
export class ApplicationComponent extends React.Component {
|
||||
@observable private tool = 'quest-editor';
|
||||
|
||||
render() {
|
||||
const quest = appStateStore.currentQuest;
|
||||
const model = appStateStore.currentModel;
|
||||
const areas = quest && Array.from(quest.areaVariants).map(a => a.area);
|
||||
const area = appStateStore.currentArea;
|
||||
const areaId = area && String(area.id);
|
||||
let toolComponent;
|
||||
|
||||
switch (this.tool) {
|
||||
case 'quest-editor':
|
||||
toolComponent = <QuestEditorComponent />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`ApplicationComponent ${Classes.DARK}`}>
|
||||
@ -34,121 +24,22 @@ export class ApplicationComponent extends React.Component<{}, {
|
||||
<NavbarGroup className="ApplicationComponent-button-bar">
|
||||
<NavbarHeading className="ApplicationComponent-heading">
|
||||
Phantasmal World
|
||||
<sup className="ApplicationComponent-beta">BETA</sup>
|
||||
</NavbarHeading>
|
||||
<FileInput
|
||||
text={this.state.filename || 'Choose file...'}
|
||||
inputProps={{
|
||||
type: 'file',
|
||||
accept: '.nj, .qst, .xj',
|
||||
onChange: this.onFileChange
|
||||
}}
|
||||
<Button
|
||||
text="Quest Editor (Beta)"
|
||||
minimal={true}
|
||||
onClick={() => this.setTool('quest-editor')}
|
||||
/>
|
||||
{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="ApplicationComponent-main">
|
||||
<QuestInfoComponent
|
||||
quest={quest} />
|
||||
<RendererComponent
|
||||
quest={quest}
|
||||
area={area}
|
||||
model={model} />
|
||||
<EntityInfoComponent entity={appStateStore.selectedEntity} />
|
||||
{toolComponent}
|
||||
</div>
|
||||
<Dialog
|
||||
title="Save as..."
|
||||
icon="floppy-disk"
|
||||
className={Classes.DARK}
|
||||
style={{ width: 360 }}
|
||||
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}
|
||||
/>
|
||||
</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;
|
||||
|
||||
this.setState({
|
||||
saveDialogOpen: true,
|
||||
saveDialogFilename: name
|
||||
});
|
||||
}
|
||||
|
||||
private onSaveDialogNameChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ saveDialogFilename: e.currentTarget.value });
|
||||
}
|
||||
|
||||
private onSaveDialogNameKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.onSaveDialogSaveClick();
|
||||
}
|
||||
}
|
||||
|
||||
private onSaveDialogSaveClick = () => {
|
||||
saveCurrentQuestToFile(this.state.saveDialogFilename);
|
||||
this.setState({ saveDialogOpen: false });
|
||||
}
|
||||
|
||||
private onSaveDialogClose = () => {
|
||||
this.setState({ saveDialogOpen: false });
|
||||
}
|
||||
private setTool = action('setTool', (tool: string) => {
|
||||
this.tool = tool;
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { NumericInput } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import React from 'react';
|
||||
import { QuestNpc, QuestObject, VisibleQuestEntity } from '../domain';
|
||||
import { QuestNpc, QuestObject, VisibleQuestEntity } from '../../domain';
|
||||
import './EntityInfoComponent.css';
|
||||
|
||||
interface Props {
|
18
src/ui/quest-editor/QuestEditorComponent.css
Normal file
18
src/ui/quest-editor/QuestEditorComponent.css
Normal file
@ -0,0 +1,18 @@
|
||||
.QuestEditorComponent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.QuestEditorComponent-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.QuestEditorComponent-button-bar>* {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.QuestEditorComponent-main div:nth-child(2) {
|
||||
flex: 1;
|
||||
}
|
149
src/ui/quest-editor/QuestEditorComponent.tsx
Normal file
149
src/ui/quest-editor/QuestEditorComponent.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { Button, Classes, Dialog, FileInput, FormGroup, HTMLSelect, InputGroup, Intent, Navbar, NavbarGroup } from "@blueprintjs/core";
|
||||
import { observer } from "mobx-react";
|
||||
import React, { ChangeEvent, KeyboardEvent } from "react";
|
||||
import { saveCurrentQuestToFile, setCurrentAreaId } from "../../actions/quest-editor/questEditor";
|
||||
import { loadFile } from "../../actions/quest-editor/loadFile";
|
||||
import { questEditorStore } from "../../stores/QuestEditorStore";
|
||||
import { EntityInfoComponent } from "./EntityInfoComponent";
|
||||
import './QuestEditorComponent.css';
|
||||
import { QuestInfoComponent } from "./QuestInfoComponent";
|
||||
import { RendererComponent } from "./RendererComponent";
|
||||
|
||||
@observer
|
||||
export class QuestEditorComponent extends React.Component<{}, {
|
||||
filename?: string,
|
||||
saveDialogOpen: boolean,
|
||||
saveDialogFilename: string
|
||||
}> {
|
||||
state = {
|
||||
filename: undefined,
|
||||
saveDialogOpen: false,
|
||||
saveDialogFilename: 'Untitled',
|
||||
};
|
||||
|
||||
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">
|
||||
<QuestInfoComponent quest={quest} />
|
||||
<RendererComponent
|
||||
quest={quest}
|
||||
area={area}
|
||||
model={model} />
|
||||
<EntityInfoComponent entity={questEditorStore.selectedEntity} />
|
||||
</div>
|
||||
<Dialog
|
||||
title="Save as..."
|
||||
icon="floppy-disk"
|
||||
className={Classes.DARK}
|
||||
style={{ width: 360 }}
|
||||
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}
|
||||
/>
|
||||
</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;
|
||||
|
||||
this.setState({
|
||||
saveDialogOpen: true,
|
||||
saveDialogFilename: name
|
||||
});
|
||||
}
|
||||
|
||||
private onSaveDialogNameChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ saveDialogFilename: e.currentTarget.value });
|
||||
}
|
||||
|
||||
private onSaveDialogNameKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.onSaveDialogSaveClick();
|
||||
}
|
||||
}
|
||||
|
||||
private onSaveDialogSaveClick = () => {
|
||||
saveCurrentQuestToFile(this.state.saveDialogFilename);
|
||||
this.setState({ saveDialogOpen: false });
|
||||
}
|
||||
|
||||
private onSaveDialogClose = () => {
|
||||
this.setState({ saveDialogOpen: false });
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { NpcType, Quest } from '../domain';
|
||||
import { NpcType, Quest } from '../../domain';
|
||||
import { Pre } from '@blueprintjs/core';
|
||||
import './QuestInfoComponent.css';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Object3D } from 'three';
|
||||
import { Area, Quest } from '../domain';
|
||||
import { Renderer } from '../rendering/Renderer';
|
||||
import { Area, Quest } from '../../domain';
|
||||
import { Renderer } from '../../rendering/Renderer';
|
||||
|
||||
interface Props {
|
||||
quest?: Quest;
|
Loading…
Reference in New Issue
Block a user