@@ -1,23 +1,22 @@
import React from 'react';
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 ';
import { Node, OptionObject, Actor, Location, EngineConfig } from 'discoteque/lib/engine/types';
import { setUI } from 'discoteque/lib/engine/store/actions ';
import skills, { GameSkills } from './lib/skills';
import Menu from './components/Menu';
import { awardSkill } from 'discoteque/lib/engine/lib/ utils';
import { lockSkills } from 'discoteque/lib/engine/lib/store ';
import { awardSkill } from 'discoteque/lib/engine/util/skil ls';
import { DateTime } from 'luxon ';
const nodes: I Node<IGameState>[] = [
const nodes: Node<IGameState, GameSkills >[] = [
{
id: 'trying_out',
kind: 'node',
next: 'pre_choice',
lines: [
{ actorId: 'char_helper', text: "Hi!" } ,
(_, _g, dispatch) => {
dispatch(lockSkills());
(_, __, dispatch) => {
dispatch(addKnownCharacter('char_helper')(dispatch));
return { actorId: 'char_helper', text: 'I\'m Helper! I help people play this demo. (Not really...)' };
},
@@ -31,8 +30,8 @@ const nodes: INode<IGameState>[] = [
{ actorId: 'char_helper', text: "Failing the check on the left will allow you to go again." },
{ actorId: 'char_helper', text: "Failing the check on the right will instantly end the game." },
{ actorId: 'char_helper', text: "You could also try going outside." },
({ skillPoints }, _, dispatch) => {
awardSkill(dispatch, skillPoints );
(_, _ _, dispatch) => {
awardSkill(dispatch);
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!" }
@@ -58,9 +57,9 @@ const nodes: INode<IGameState>[] = [
next: 'exit',
lines: [
{ text: '[FAIL] You were so overhyped you couldn\'t even say what you wanted to say' },
{ actorId: 'char_helper', text: 'Well, this is the end.' },
(_, _gameState, dispatch) => {
dispatch(setState({ isOver: true }))
dispatch(setUI({ gameOver: true }))
return { actorId: 'char_helper', text: 'Well, this is the end.' };
},
],
},
@@ -68,19 +67,20 @@ const nodes: INode<IGameState>[] = [
id: 'pre_choice',
kind: 'node',
lines: [
(_, gameState) => {
const counterRight = gameState.visitedRight;
const counterLeft = gameState.visitedLeft;
const combinedCounter = counterRight + counterLeft;
const showExit = combinedCounter >= 3;
return { actorId: 'char_helper', text: 'So, left, or right?', options: [
{ text: '"I want to go outside!"', value: 'pre_outside' },
{ text: '"Left!"', value: 'left_choice', skill: { name: 'tired', difficulty: 17, failTo: 'failed_choice_tired' } },
{ text: '"Right!"', value: 'right_choice', skill: { name: 'manic', difficulty: 9, failTo: 'failed_choice_manic' } },
showExit && { 'text': 'Let me out, please', value: 'exit' }
].filter(x => x) as ILineOption[] };
}
]
{ actorId: 'char_helper', text: 'So, left, or right?', },
],
options: (_, gameState) => {
const counterRight = gameState.visitedRight;
const counterLeft = gameState.visitedLeft;
const combinedCounter = counterRight + counterLeft;
const showExit = combinedCounter >= 3;
return [
{ text: '"I want to go outside!"', value: 'pre_outside' },
{ text: '"Left!"', value: 'left_choice', skill: { name: 'tired', difficulty: 17, failTo: 'failed_choice_tired' } },
{ text: '"Right!"', value: 'right_choice', skill: { name: 'manic', difficulty: 9, failTo: 'failed_choice_manic' } },
showExit && { 'text': 'Let me out, please', value: 'exit' }
].filter(x => x) as OptionObject<GameSkills>[]
},
},
{
id: 'pre_outside',
@@ -107,13 +107,13 @@ const nodes: INode<IGameState>[] = [
{ actorId: 'char_helper', text: 'Huh. I think we\'ve been here before, no?' }
) : { actorId: 'char_helper', text: 'Good choice! As i\'ve said, picking left will lead you back.' };
},
({ skillPoints } , gameState, dispatch) => {
(_ , gameState, dispatch) => {
if ((gameState.visitedLeft + 1) === 3) {
awardSkill(dispatch, skillPoints );
awardSkill(dispatch);
}
dispatch(setState({ visitedLeft: gameState.visitedLeft + 1 }));
return { actorId: 'char_helper', text: 'Pick the other choice next time, maybe?' };
},
{ actorId: 'char_helper', text: 'Pick the other choice next time, maybe?' },
]
},
{
@@ -122,9 +122,9 @@ const nodes: INode<IGameState>[] = [
next: 'pre_choice',
lines: [
{ text: 'Without hesistation, you decided to go straight for the right option!' },
{ actorId: 'char_helper', text: 'Well, you passed this check. Try the other one now, and don\'t forget to fail this one.' },
(_, gameState, dispatch) => {
dispatch(setState({ visitedRight: gameState.visitedRight + 1 }));
return { actorId: 'char_helper', text: 'Well, you passed this check. Try the other one now, and don\'t forget to fail this one.' };
}
]
},
@@ -136,6 +136,7 @@ const nodes: INode<IGameState>[] = [
{ actorId: 'char_helper', text: 'Thanks, but this is the end.' },
(_engineState, _gameState, dispatch) => {
dispatch(setState({ isOver: true }))
return { actorId: 'char_helper', text: 'Thanks, but this is the end.' };
},
]
},
@@ -147,10 +148,10 @@ const nodes: INode<IGameState>[] = [
{ text: 'There\'s a letter inside.' },
{ text: 'You open it to take a look...' },
{ text: '"Welcome to Discoteque! An engine for games of adventure, danger, and low cunning!"' },
({ skillPoints } , gameState, dispatch) => {
(_ , gameState, dispatch) => {
if (!gameState.readMailbox) {
dispatch(setState({ readMailbox: true }));
awardSkill(dispatch, skillPoints );
awardSkill(dispatch);
}
return { text: `"No browser should be without one!" ${!gameState.readMailbox ? "[+1 Skill Point]" : ""}`, time: 5 };
}
@@ -190,18 +191,19 @@ const nodes: INode<IGameState>[] = [
{
'id': 'dialogue_helper', kind: 'node',
lines: [
(_, { talkedAboutTrapdoor }) => {
const trapdoorTalkSkill = talkedAboutTrapdoor ? 0 : 10;
return { text: 'What do you want to ask?', options: [
{ text: '"Tell me about that choice."', value: 'about_choice' },
{ text: '"Tell me about the trapdoor"', value: 'about_trapdoor', skill: {
name: 'tired', difficulty: trapdoorTalkSkill, failTo: 'about_trapdoor_fail'
} },
{ text: '"I\'m ready to pick"', value: 'pre_choice' },
{ text: '"Just came by to say hi!"', value: 'inside' },
] };
},
{ text: 'What do you want to ask?' },
],
options: (_, { talkedAboutTrapdoor }) => {
const trapdoorTalkSkill = talkedAboutTrapdoor ? 0 : 10;
return [
{ text: '"Tell me about that choice."', value: 'about_choice' },
{ text: '"Tell me about the trapdoor"', value: 'about_trapdoor', skill: {
name: 'tired', difficulty: trapdoorTalkSkill, failTo: 'about_trapdoor_fail'
} },
{ text: '"I\'m ready to pick"', value: 'pre_choice' },
{ text: '"Just came by to say hi!"', value: 'inside' },
]
}
},
{
'id': 'about_choice', kind: 'node', next: 'dialogue_helper',
@@ -228,11 +230,11 @@ const nodes: INode<IGameState>[] = [
{ actorId: 'char_helper', text: 'What a *grue*some end, right?' },
{ text: 'Helper starts laughing and can\'t help themselves. You don\'t appreciate the joke though.' },
{ actorId: 'char_helper', text: 'Well, enought about gruesome things, let\'s get back to the topic of chosing' },
({ skillPoints } , gameState, dispatch) => {
(_ , gameState, dispatch) => {
const { talkedAboutTrapdoor } = gameState;
if (!talkedAboutTrapdoor) {
dispatch(setState({ talkedAboutTrapdoor: true }));
awardSkill(dispatch, skillPoints );
awardSkill(dispatch);
dispatch(setTask('trapdoor', { stagesComplete: [0, 1] })(dispatch));
return { text: 'You feel like this will help you search for the trap door. [+1 Skill Point]' };
}
@@ -246,13 +248,13 @@ const nodes: INode<IGameState>[] = [
{ text: 'But nothing comes up!' },
{ text: 'It\'s almost as if it\'s there but every time you look at it moves to another place.' },
{ text: 'Or perhaps it is always just right out of the corner of your eye...' },
({ skillPoints } , gameState, dispatch) => {
(_ , gameState, dispatch) => {
const { lookedAtTrapdoor } = gameState;
if (lookedAtTrapdoor) {
return { text: 'Looks like you will have to search more thoroughly.' }
}
dispatch(setState({ lookedAtTrapdoor: true }));
awardSkill(dispatch, skillPoints )
awardSkill(dispatch)
return { text: 'You feel like you\'ll have an easier time looking for the trapdoor properly. [+1 Skill Point]' }
},
],
@@ -260,23 +262,31 @@ 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: [
export const Helper: Actor<IGameState, GameSkills> = {
'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));
dispatch(setTask('trapdoor', { stages: [0, 1, 2], stagesComplete: [0] })(dispatch));
}
return { actorId: 'char_helper', text: 'Back again, huh?' };
},
{ 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' },
],
next: 'dialogue_helper'
};
const actors: Actor<IGameState, GameSkills>[] = [
Helper,
{ 'id': 'char_you', kind: 'actor', name: 'You', lines: [] },
]
const locations: ILocation<IGameState>[] = [
const locations: Location<IGameState, GameSkills >[] = [
{ 'id': 'outside', kind: 'location', name: 'Great Outdoors', lines: [
(_, { beenOutside }, dispatch) => {
if (!beenOutside) {
@@ -286,10 +296,10 @@ const locations: ILocation<IGameState>[] = [
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.' },
{ text: 'What now?', options: [
{ text: 'Look inside mailbox', value: 'mailbox' },
{ text: 'Go inside the house', value: 'inside ' },
] },
{ text: 'What now?', },
], options: [
{ text: 'Look inside mailbox', value: 'mailbox ' },
{ text: 'Go inside the house', value: 'inside' },
] },
{ 'id': 'inside', kind: 'location', name: 'Inside House', lines: [
(_, { beenInside }, dispatch) => {
@@ -298,82 +308,73 @@ const locations: ILocation<IGameState>[] = [
dispatch(setTask('outside', { stages: [0, 1, 2], stagesComplete: [0, 1] })(dispatch));
dispatch(setTask('trapdoor', { stages: [0, 2], stagesComplete: [0] })(dispatch));
}
return { text: 'You are standing inside a small wooden house. The setup feels familiar... ' };
},
{ 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.' },
(_, { lookedAtTrapdoor, talkedAboutTrapdoor }) => {
const searchDifficulties = [
lookedAtTrapdoor && 10,
talkedAboutTrapdoor && 5 ,
].filter(id => id) as Array<number>;
const searchDifficulty = searchDifficulties.reduce(
(acc, val) => acc - val,
25 ,
);
const searchEffects = [
lookedAtTrapdoor && '[+10 from scanning the room]',
talkedAboutTrapdoor && '[+5 from talking about trap door with Helper ]',
].filter(id => id);
return { text: 'What will you do now?', options: [
{ text: 'Go back outside', value: 'outside' },
{ text: 'Talk to Helper', value: 'char_helper ' },
{ text: 'Scan the room for the trapdoor', value: 'look_trapdoo r' },
{
text: `Search thoroughly for the trapdoor $ {searchEffects.join(' ')}`,
value: 'trapdoor_win' ,
skill: { name: 'manic', difficulty: searchDifficulty, failTo: 'trapdoor_end' }
},
] }
},
] },
{ text: 'What will you do now?' }
], options: (_, { lookedAtTrapdoor, talkedAboutTrapdoor }) => {
const searchDifficulties = [
lookedAtTrapdoor && 10 ,
talkedAboutTrapdoor && 5,
].filter(id => id) as Array<number>;
const searchDifficulty = searchDifficulties.reduce(
(acc, val) => acc - val ,
25,
);
const searchEffects = [
lookedAtTrapdoor && '[+10 from scanning the room ]',
talkedAboutTrapdoor && '[+5 from talking about trap door with Helper]',
].filter(id => id);
return [
{ text: 'Go back outside', value: 'outside ' },
{ text: 'Talk to Helper', value: 'char_helpe r' },
{ text: 'Scan the room for the trapdoor', value: 'look_trapdoor' },
{
text: `Search thoroughly for the trapdoor ${searchEffects.join(' ')}` ,
value: 'trapdoor_win',
skill: { name: 'manic', difficulty: searchDifficulty, failTo: 'trapdoor_end' }
},
];
} },
];
interface IGameConfig extends EngineConfig<IGameState, undefined, GameSkills> {
interface IGameConfig extends EngineConfig<IGameState, GameSkills> {
}
const renderFn: RendererFN<IGameState, undefined, GameSkills> = (engine) => {
const nodeMap = engine.config.nodes.reduce((acc, node) => ({ ...acc, [node.id]: node }), {});
const renderFn = () => {
return (
<Menu nodeMap={nodeMap} />
<Menu />
);
};
const optionsFn: ToolbarOptionsFN<IGameState, undefined, GameSkills> = (_engine, _engineConfig, _gameState, dispatch) => {
return [
{ label: 'Journal', onClick: () => dispatch(setShowCustom(true)) },
]
}
export const config: IGameConfig = {
menu: {
title: 'Discoteque Demo',
description: 'Interactive fiction system with light RPG elements',
},
reducer: reducer,
startNode: 'trying_out',
startAt : 'trying_out',
nodes: [
...nodes,
...actors,
...locations,
],
chrono: {
time: 1435,
date: "10 September",
},
onDayCycle: (time) => {
const day = time.date?.substr(0, 2) || "10";
const restOfDate = time.date?.substr(2) || " September";
const dayNum = Number(day) !== NaN ? Number(day) : 10;
return {
time: 0,
date: `${dayNum + 1}${restOfDate}`,
}
date: DateTime.local(),
},
skills,
skillPointsOnStart: 5,
customRenderer: renderFn,
customOptions: optionsFn,
skillDescriptoins: {
'manic': 'Out of this world at the highest pace',
'tired': 'Slow burning and contemplation',
}
skillPoints: 5,
customScreenFn: renderFn,
customDropdownFn: (_, __, dispatch) => {
return [
{ title: 'Journal', onClick: () => dispatch(setUI({ custom: true, game: false })) },
]
},
// skillDescriptoins: {
// 'manic': 'Out of this world at the highest pace',
// 'tired': 'Slow burning and contemplation',
// }
}
export default config;