Browse Source

feat: Added tasks and character DB

develop
Dale 2 months ago
parent
commit
ccc0b25721
Signed by: Deiru GPG Key ID: AA250C0277B927E1
10 changed files with 391 additions and 30 deletions
  1. +2
    -0
      package.json
  2. +55
    -0
      src/components/Characters/index.tsx
  3. +52
    -12
      src/components/Menu/index.tsx
  4. +9
    -3
      src/components/Menu/styles.ts
  5. +91
    -0
      src/components/Tasks/index.tsx
  6. +29
    -7
      src/game.tsx
  7. +76
    -7
      src/lib/store.ts
  8. +29
    -0
      src/lib/tasks.ts
  9. +24
    -1
      yarn.lock
  10. +24
    -0
      yarn.nix

+ 2
- 0
package.json View File

@@ -10,6 +10,7 @@
},
"dependencies": {
"@emotion/core": "^10.0.35",
"@emotion/styled": "^10.0.27",
"classnames": "^2.2.6",
"discoteque": "^1.1.7",
"emotion": "^10.0.27",
@@ -19,6 +20,7 @@
"react-redux": "^7.2.1",
"react-toastify": "^6.0.8",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"typescript": "^4.0.3",
"webpack": "^4.44.2",
"webpack-dev-server": "^3.11.0"


+ 55
- 0
src/components/Characters/index.tsx View File

@@ -0,0 +1,55 @@
import React, { FC } from 'react';
import styled from '@emotion/styled';
import { IActor } from 'discoteque/lib/engine/types';
import { colors } from 'discoteque/lib/assets/style-vars';

const CharacterListContainer = styled.div`
height: 100%;
width: 100%;
overflow-x: auto;
`;

const CharacterListItem = styled.div`
border-bottom: 1px solid black;
padding: 15px;
&:last-of-type {
border-bottom: none;
}
& > img {
width: 100px;
margin-right: 15px;
float: left;
}
& > h3 {
margin: 0 0 15px 0;
}
& > p {
margin: 0;
}
`;

type Props = {
actorDescriptions: Record<string, string>;
actors: Record<string, IActor>;
knownActors: string[];
}

const Characters: FC<Props> = ({ actors, knownActors, actorDescriptions }) => {
return (
<CharacterListContainer>
{knownActors.map(actorId => {
const actor = actors[actorId];
const desc = actorDescriptions[actorId];
return (
<CharacterListItem>
{actor.image && (<img src={actor.image} />)}
<h3>{actor.name}</h3>
{desc && <p>{desc}</p>}
</CharacterListItem>
);
})}
</CharacterListContainer>
)
};

export default Characters;

+ 52
- 12
src/components/Menu/index.tsx View File

@@ -1,5 +1,5 @@
import React, { FC, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';

import SkillTree from 'discoteque/lib/components/SkillTree';

@@ -8,28 +8,68 @@ type Tabs = 'tasks' | 'characters' | 'skills';
import cn from 'classnames';
import * as styles from './styles';
import { setShowCustom } from 'discoteque/lib/engine/lib/store';
import TaskList from '../Tasks';
import taskInfo from '@/lib/tasks';
import styled from '@emotion/styled';
import Characters from '../Characters';
import { NodeMap, IActor } from 'discoteque/lib/engine/types';
import { stateSelector, setMenuTab, menuTabSelector } from '@/lib/store';

const TabMenu: FC = () => {
const TopMenuContainer = styled.div`
display: flex;
flex-direction: row;
border-bottom: 2px solid black;
height: 40px;
`

const actorDescriptions = {
char_helper: 'A very friendly fellow! Lover of helping others and very absurd guestions',
}

type TabMenuProps = {
nodeMap: NodeMap<any, any, any>;
}

const TabMenu: FC<TabMenuProps> = ({ nodeMap }) => {
const dispatch = useDispatch();
const [openTab, setOpenTab] = useState<Tabs>('tasks');

const openTasks = () => setOpenTab('tasks');
const openSkills = () => setOpenTab('skills');
const openChars = () => setOpenTab('characters');
const state = useSelector(stateSelector);
const openTab = useSelector(menuTabSelector);

const setTab = (tabName: string) => () => dispatch(setMenuTab(tabName));
const openTasks = setTab('tasks');
const openSkills = setTab('skills');
const openChars = setTab('characters');

const closeMenu = () => dispatch(setShowCustom(false));

const actors = {
'char_helper': nodeMap['char_helper'] as IActor,
}

return (
<div className={styles.menuContainer}>
<ul className={styles.menuTabList}>
<li className={styles.menuClose} onClick={closeMenu}>X</li>
<li className={cn({ [styles.active]: openTab === 'tasks' })} onClick={openTasks}>Tasks</li>
<li className={cn({ [styles.active]: openTab === 'skills' })} onClick={openSkills}>Skills</li>
<li className={cn({ [styles.active]: openTab === 'characters' })} onClick={openChars}>Characters</li>
</ul>
<TopMenuContainer>
<a className={styles.menuClose} onClick={closeMenu}>X</a>
<ul className={styles.menuTabList}>
<li className={cn({ [styles.active]: openTab === 'skills' })} onClick={openSkills}><a>Skills</a></li>
<li className={cn({ [styles.active]: openTab === 'tasks' })} onClick={openTasks}><a>Tasks</a></li>
<li className={cn({ [styles.active]: openTab === 'characters' })} onClick={openChars}><a>Characters</a></li>
</ul>
</TopMenuContainer>
{openTab === 'skills' && (
<SkillTree />
)}
{openTab === 'tasks' && (
<TaskList taskList={state.db.tasks} taskInfo={taskInfo} />
)}
{openTab === 'characters' && (
<Characters
actors={actors}
knownActors={state.db.characters}
actorDescriptions={actorDescriptions}
/>
)}
</div>
);
}


+ 9
- 3
src/components/Menu/styles.ts View File

@@ -15,11 +15,12 @@ export const menuTabList = css`
list-style: none;
padding: 0;
margin: 0;
overflow-y: auto;
display: flex;
flex-direction: row;
justify-content: space-betwee;
box-sizing: border-box;
border-bottom: 2px dashed ${colors.MainBgColorDark};
width: 100%;
& > li {
width: 100%;
text-align: center;
@@ -35,14 +36,19 @@ export const menuTabList = css`
`;

export const menuClose = css`
flex-basis: 40px;
max-width: 40px !important;
padding: 0px !important;
height: 40px !important;
min-height: 40px !important;
line-height: 40px;
font-size: 40px;
box-sizing: border-box;
border-right: 1px solid ${colors.MainBgColorDark};
box-sizing: content-box;
border-right: 2px solid ${colors.MainBgColor};
text-align: center;
cursor: pointer;
color: ${colors.MainBgColor};
background-color: ${colors.MainBgColorDark};
`;

export const active = "active";

+ 91
- 0
src/components/Tasks/index.tsx View File

@@ -0,0 +1,91 @@
import React, { FC } from "react";
import styled from '@emotion/styled';
import { css } from "emotion";

export type Task = {
id: string;
stages: number[];
stagesComplete: number[];
}

export type TaskInfo<KT extends string = string> = Record<KT, {
name: string;
description?: string;
stages: string[];
}>;


type Props = {
taskList: Task[],
taskInfo: TaskInfo,
}

const TaskListContainer = styled.div`
overflow-x: auto;
height: 100%;
`;

const Task = styled.div`
padding: 15px;
& > i {
margin: 0;
margin-bottom: 15px;
display: inline-block;
}
& > h3 {
margin: 0;
}
`;

const TaskStages = styled.ul`
list-style: none;
margin: 0;
padding: 0;
& > li::before {
content: '—';
margin-right: 5px;
}
`;

const taskStageComplete = css`
margin-bottom: 5px;
text-decoration: line-through;
`;

const TaskStage = styled.li`
margin-bottom: 5px;
&::before {
content: '';
}
`

const TaskList: FC<Props> = ({ taskList, taskInfo }) => {
return (
<TaskListContainer>
{taskList.map(task => (
<Task key={task.id}>
<h3>{taskInfo[task.id].name}</h3>
{taskInfo[task.id].description && (
<i>{taskInfo[task.id].description}</i>
)}
<TaskStages>
{task.stages.map(stage => (
<TaskStage
key={stage}
className={
(task.stagesComplete || []).includes(stage)
? taskStageComplete
: undefined
}
>
{taskInfo[task.id].stages[stage]}
</TaskStage>
))}
</TaskStages>
</Task>
))}
</TaskListContainer>
);
}

export default TaskList;

+ 29
- 7
src/game.tsx View File

@@ -1,6 +1,6 @@
import React from 'react';

import { IGameState, setState, reducer } from '@/lib/store';
import { IGameState, setState, reducer, addKnownCharacter, setTask } from '@/lib/store';
import { INode, IActor, EngineConfig, ILineOption, ILocation, RendererFN, ToolbarOptionsFN } from 'discoteque/lib/engine/types';
import { setShowCustom } from 'discoteque/lib/engine/lib/store';

@@ -15,8 +15,12 @@ const nodes: INode<IGameState>[] = [
kind: 'node',
next: 'pre_choice',
lines: [
{ actorId: 'char_helper', text: "Hi!" },
(_, _g, dispatch) => dispatch(lockSkills()) && null,
{ actorId: 'char_helper', text: "Hi!" } ,
(_, _g, dispatch) => {
dispatch(lockSkills());
dispatch(addKnownCharacter('char_helper')(dispatch));
return { actorId: 'char_helper', text: 'I\'m Helper! I help people play this demo. (Not really...)' };
},
{ actorId: 'char_helper', text: "Lets's start by advancing some lines!" },
{ actorId: 'char_helper', text: "This should be easy enough for you!" },
{ actorId: 'char_helper', text: "See?" },
@@ -29,6 +33,8 @@ const nodes: INode<IGameState>[] = [
{ actorId: 'char_helper', text: "You could also try going outside." },
({ skillPoints }, _, dispatch) => {
awardSkill(dispatch, skillPoints);
dispatch(setTask("choice", { stages: [0] })(dispatch));
dispatch(setTask("outside", { stages: [0] })(dispatch));
return { actorId: 'char_helper', text: "By the way, here's a skill point just for you!" }
},
{ actorId: 'char_helper', text: "You can spend it using \"Skills\" menu in \"Options\", in the upper left corner! " },
@@ -256,6 +262,12 @@ const nodes: INode<IGameState>[] = [
// { actorId: 'char_helper', text: '' },
const actors: IActor<IGameState>[] = [
{ 'id': 'char_helper', kind: 'actor', name: 'Helper', image: require('@/assets/images/user.png').default, lines: [
(_, { haveTalkedToHelper }, dispatch) => {
if (!haveTalkedToHelper) {
dispatch(setState({ haveTalkedToHelper: true }));
dispatch(setTask('outside', { stagesComplete: [0, 1, 2] })(dispatch));
}
},
{ actorId: 'char_helper', text: 'Back again, huh?' },
{ actorId: 'char_helper', text: 'Then I guess you\'re ready to pick right from left now.', },
], next: 'dialogue_helper' },
@@ -264,8 +276,11 @@ const actors: IActor<IGameState>[] = [

const locations: ILocation<IGameState>[] = [
{ 'id': 'outside', kind: 'location', name: 'Great Outdoors', lines: [
(_, _gameState, dispatch) => {
dispatch(setState({ beenOutside: true }));
(_, { beenOutside }, dispatch) => {
if (!beenOutside) {
dispatch(setState({ beenOutside: true }));
dispatch(setTask("outside", { stages: [0, 1], stagesComplete: [0] })(dispatch));
}
return { text: 'You are standing in an open field, west of house.' };
},
{ text: 'The sea of green extends into all directions, as far as your eye can see.' },
@@ -275,6 +290,12 @@ const locations: ILocation<IGameState>[] = [
] },
] },
{ 'id': 'inside', kind: 'location', name: 'Inside House', lines: [
(_, { beenInside }, dispatch) => {
if (!beenInside) {
dispatch(setState({ beenInside: true }));
dispatch(setTask('outside', { stages: [0, 1, 2], stagesComplete: [0, 1] })(dispatch));
}
},
{ text: 'You are standing inside a small wooden house. The setup feels familiar... ' },
{ text: 'You almost expect there to be a trapdoor to a great underground empire.' },
{ text: 'You can see Helper here as well. They are standing in the corner, gesturing you to come and talk to them.' },
@@ -308,9 +329,10 @@ const locations: ILocation<IGameState>[] = [
interface IGameConfig extends EngineConfig<IGameState, undefined, GameSkills> {
}

const renderFn: RendererFN<IGameState, undefined, GameSkills> = () => {
const renderFn: RendererFN<IGameState, undefined, GameSkills> = (engine) => {
const nodeMap = engine.config.nodes.reduce((acc, node) => ({ ...acc, [node.id]: node }), {});
return (
<Menu />
<Menu nodeMap={nodeMap} />
);
};



+ 76
- 7
src/lib/store.ts View File

@@ -1,36 +1,78 @@
import * as R from 'ramda';
import { createStore, Action } from 'redux';
import { createStore, Action, Dispatch } from 'redux';
import { ILine } from 'discoteque/lib/engine/types';
import { Task } from '@/components/Tasks';
import { toast } from 'discoteque/lib/engine/lib/utils';
import { setShowCustom } from 'discoteque/lib/engine/lib/store';

export interface IGameState {
visitedLeft: number;
askedAboutChoice: boolean;
visitedRight: number;
beenOutside: boolean;
lookedAtTrapdoor: boolean,
talkedAboutTrapdoor: boolean,
readMailbox: boolean,
beenInside: boolean;
haveTalkedToHelper: boolean;
lookedAtTrapdoor: boolean;
talkedAboutTrapdoor: boolean;
readMailbox: boolean;
ui: { menuTab: string }
db: {
characters: string[],
tasks: Task[],
}
backlog: ILine<IGameState>[];
}

export interface IGameStateAction extends Action {
type: 'PATCH-GAME' | 'INIT-GAME' | 'RESET-GAME';
type: 'INIT' | 'PATCH-GAME' | 'INIT-GAME' | 'RESET-GAME' | 'SET-TASK' | 'ADD-KNOWN-CHARACTER' | 'SET-MENU-TAB';
state?: IGameState;
tab?: string;
task?: {
id: string;
data: Partial<Task>;
}
character?: string;
}


export const setState = (state: object): IGameStateAction => ({ type: 'PATCH-GAME', state: (state as IGameState) });
export const initState = (state: IGameState): IGameStateAction => ({ type: 'INIT-GAME', state });
export const resetState = (): IGameStateAction => ({ type: 'RESET-GAME' })
export const setMenuTab = (tab: string): IGameStateAction => ({ type: 'SET-MENU-TAB', tab });
export const setTask = (id: string, data: Partial<Task>) => (dispatch: Dispatch): IGameStateAction => {
toast.info("Task list updated", { onClick: () => {
dispatch(setMenuTab('tasks'));
dispatch(setShowCustom(true));
} });
return {
type: 'SET-TASK', task: { id, data },
};
};
export const addKnownCharacter = (character: string) => (dispatch: Dispatch): IGameStateAction => {
toast.info("Character database updated", { onClick: () => {
dispatch(setMenuTab('characters'));
dispatch(setShowCustom(true));
} });
return {
type: 'ADD-KNOWN-CHARACTER', character,
};
};

const defaultState: IGameState = {
visitedLeft: 0,
visitedRight: 0,
askedAboutChoice: false,
beenOutside: false,
beenInside: false,
haveTalkedToHelper: false,
lookedAtTrapdoor: false,
talkedAboutTrapdoor: false,
readMailbox: false,
ui: { menuTab: 'skills' },
db: {
characters: [],
tasks: [],
},
backlog: [],
};

@@ -40,13 +82,40 @@ export const reducer = (state: IGameState = defaultState, action: IGameStateActi
return ({ ...state, ...action.state });
case 'INIT-GAME':
return action.state || state;
case 'INIT':
return (action as any).data.gameState || state;
case 'RESET-GAME':
return defaultState;
default:
return state;
case 'SET-TASK':
if (action.task) {
const newTasks = [
R.mergeDeepRight<Partial<Task>, Partial<Task>>(
state.db.tasks.find(task => task.id === action.task?.id) || { id: action.task.id },
action.task.data,
),
...state.db.tasks.filter(task => task.id !== action.task?.id),
] as Task[];
const statePatch: Partial<IGameState> = { db: { ...state.db, tasks: newTasks } };
return R.mergeDeepRight<IGameState, Partial<IGameState>>(
state,
statePatch,
) as IGameState;
}
case 'ADD-KNOWN-CHARACTER':
return R.mergeDeepRight(state, {
db: { characters: [ ...state.db.characters, action.character ].filter(R.identity) }
}) as IGameState;
case 'SET-MENU-TAB':
if (action.tab) {
return R.mergeDeepRight(state, {
ui: { menuTab: action.tab }
});
}
}
return state;
}

export const stateSelector: (state: object) => IGameState = R.propOr(defaultState, 'game');
export const menuTabSelector: (state: object) => string = R.pathOr(defaultState.ui.menuTab, ['game', 'ui', 'menuTab']);

export default createStore(reducer);

+ 29
- 0
src/lib/tasks.ts View File

@@ -0,0 +1,29 @@
import { TaskInfo } from "@/components/Tasks";

const tasks: TaskInfo = {
trapdoor: {
name: "Find the trap door",
description: "\"You're here to find a famous trap door leading to adventure and treasure!\"",
stages: [
"Find the trap door",
]
},
choice: {
name: "Make a choice",
description: "\"Helper wants me to pick right form left. I should do that probably... Or maybe hold off for a moment.\"",
stages: [
"Make the choice",
],
},
outside: {
name: "Go outside",
description: "\"Helper mentioned Outside. It's a good idea to check it out.\"",
stages: [
"Go outside",
"Check the house",
"Talk to helper"
],
}
}

export default tasks;

+ 24
- 1
yarn.lock View File

@@ -150,7 +150,7 @@
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==

"@emotion/is-prop-valid@^0.8.8":
"@emotion/is-prop-valid@0.8.8", "@emotion/is-prop-valid@^0.8.8":
version "0.8.8"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
@@ -178,6 +178,24 @@
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5"
integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==

"@emotion/styled-base@^10.0.27":
version "10.0.31"
resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.31.tgz#940957ee0aa15c6974adc7d494ff19765a2f742a"
integrity sha512-wTOE1NcXmqMWlyrtwdkqg87Mu6Rj1MaukEoEmEkHirO5IoHDJ8LgCQL4MjJODgxWxXibGR3opGp1p7YvkNEdXQ==
dependencies:
"@babel/runtime" "^7.5.5"
"@emotion/is-prop-valid" "0.8.8"
"@emotion/serialize" "^0.11.15"
"@emotion/utils" "0.11.3"

"@emotion/styled@^10.0.27":
version "10.0.27"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-10.0.27.tgz#12cb67e91f7ad7431e1875b1d83a94b814133eaf"
integrity sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q==
dependencies:
"@emotion/styled-base" "^10.0.27"
babel-plugin-emotion "^10.0.27"

"@emotion/stylis@0.8.5", "@emotion/stylis@^0.8.4":
version "0.8.5"
resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04"
@@ -4073,6 +4091,11 @@ readdirp@~3.4.0:
dependencies:
picomatch "^2.2.1"

redux-thunk@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==

redux@^4.0.0, redux@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"


+ 24
- 0
yarn.nix View File

@@ -177,6 +177,22 @@
sha1 = "894374bea39ec30f489bbfc3438192b9774d32e5";
};
}
{
name = "_emotion_styled_base___styled_base_10.0.31.tgz";
path = fetchurl {
name = "_emotion_styled_base___styled_base_10.0.31.tgz";
url = "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.31.tgz";
sha1 = "940957ee0aa15c6974adc7d494ff19765a2f742a";
};
}
{
name = "_emotion_styled___styled_10.0.27.tgz";
path = fetchurl {
name = "_emotion_styled___styled_10.0.27.tgz";
url = "https://registry.yarnpkg.com/@emotion/styled/-/styled-10.0.27.tgz";
sha1 = "12cb67e91f7ad7431e1875b1d83a94b814133eaf";
};
}
{
name = "_emotion_stylis___stylis_0.8.5.tgz";
path = fetchurl {
@@ -4505,6 +4521,14 @@
sha1 = "9fdccdf9e9155805449221ac645e8303ab5b9ada";
};
}
{
name = "redux_thunk___redux_thunk_2.3.0.tgz";
path = fetchurl {
name = "redux_thunk___redux_thunk_2.3.0.tgz";
url = "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz";
sha1 = "51c2c19a185ed5187aaa9a2d08b666d0d6467622";
};
}
{
name = "redux___redux_4.0.5.tgz";
path = fetchurl {


Loading…
Cancel
Save