discoteque
v2.1.1
Published
Interactive Ficiton engine with RPG elements
Downloads
8
Readme
Discoteque
Text-based interactive ficiton with RPG elements
Allows you to create simple RPG-esque text stories that follow basic principles.
- You can read text that is spoken by characters or just presented as is ("narration").
- You can navigate branching dialogue.
- You can distribute and use skills to pass dialogue checks and gain them.
- State can be used to track progress / remember decisions / set "VN-like" flags.
- Time passes with each line
Full Demo (Source Code for Demo)
Usage
With seed
Just clone example seed project, then edit game.ts
, skills.ts
and store.ts
to develop your game.
git clone https://code.gensokyo.social/Gensokyo.social/discoteque-seed.git my-game
cd my-game
yarn
yarn start
With create-react-app as template
(Optional, but provides for stable webpack set up that we're not covering here)
Start with https://create-react-app.dev/docs/adding-typescript/
npx create-react-app my-app --template typescript
Remove all contents from src/
Install following pre-requisits
yarn add discoteque react-toastify redux emotion @emotion/core
yarn add -D @types/react-redux
Follow these steps
Create game store & reducer
Create a store and reducer to process in-game actions and change state
// store.ts
import { createStore, Action } from 'redux';
import { resetGame } from 'discoteque/lib/engine/lib/utils';
export interface IGameState {
myCheck: boolean;
}
const defaultState: IGameState = {
myCheck: false,
};
type ACTION = 'INIT' | 'set-check';
interface IGameAction extends Action {
type: ACTION;
}
interface ISetCheckAction extends IGameAction {
check: boolean;
}
interface InitAction extends IGameAction {
data: { gameState: IGameState };
}
export const setCheck = (check: boolean): ISetCheckAction => ({
type: 'set-check', check,
});
export const reducer = (state: IGameState = defaultState, action: IGameAction): IGameState => {
switch (action.type) {
case 'set-check':
const checkAction = action as ISetCheckAction;
return ({ ...state, myCheck: checkAction.check });
// 'INIT' is a reserved Engine action type, used to initialize both game and engine state from save
// Since game state is defined along with game's script and not included with engine, we have to perform reseting ourselves
// `resetGame` is a supplied utility function that lets us perform necessary reduce without cluttering the rest of reducer
case 'INIT':
const initAction = action as InitAction;
return resetGame(state, initAction);
default:
return state;
}
}
export default createStore(reducer);
Create skills
Create default skills
// skills.ts
import { SkillSet } from "discoteque/lib/engine/types";
export type GameSkills = 'test';
export const skills: SkillSet<GameSkills> = {
test: 5,
};
export const skillDescriptions: Record<GameSkills, string> = {
test: 'This is a test skill',
}
export default skills;
Create script
// game.ts
import { INode, IActor, ILocation } from 'discoteque/lib/engine/types';
import { setState } from 'discoteque/lib/engine/lib/store';
import { IGameState, setCheck } from './store';
import { awardSkill, toast } from 'discoteque/lib/engine/lib/utils';
// Create Nodes
export const nodes: INode<IGameState>[] = [
{ id: 'beginning', kind: 'node', next: 'choice', lines: [
// Lines are basically objects which specify how line should look
{ actorId: 'char_exampler', text: 'Hi! This is an example of Discoteque!' },
{ actorId: 'char_exampler', text: 'Let\'s try picking options' },
] },
{ id: 'choice', kind: 'node', next: 'choice', lines: [
// Make player pick an answer
{ text: 'Your choice?', options: [
// This answer is gated behind a skill check (dice throw)
// name and difficulty are self-explanatory, failTo specifies node to fallback to if check is failed
// failTo is REQUIRED
{ text: 'Let\'s test a skill', value: 'test_success', skill: { name: 'test', difficulty: 15, failTo: 'test_fail' } },
// This one has no costs or prerequisites!
{ text: 'Let me out!', value: 'exit' },
] }
] },
{ id: 'test_success', kind: 'node', next: 'choice', lines: [
// Line can be a funciton which takes a number of parameters and returns a line object
(_, { myCheck }, dispatch) => {
if (!myCheck) {
// We can dispatch redux events to modify game's store!
dispatch(setCheck(true))
return { actorId: 'char_exampler', text: "Yay! You passed a check!" }
} else {
return { text: 'You think you\'ve already passed this check, so no need to do it again' }
}
},
]},
{ id: 'test_fail', kind: 'node', next: 'choice', lines: [
({ skillPoints }, _, dispatch) => {
// This is a helper function from discoteque lib
// It awards one skill point to player and dispatches a toast popup
awardSkill(dispatch, skillPoints);
return { actorId: 'char_exampler', text: 'You\'ve failed... Not a problem! Take a skill point and I\'ll give another if you fail again!' };
}
] },
{ id: 'exit', kind: 'node', next: 'choice', lines: [
(_, _gameState, dispatch) => {
dispatch(setState({ ui: { isOver: true } }));
return { actorId: 'char_exampler' , text: 'Bye-bye!' };
},
] }
];
// Create actors
export const actors: IActor<IGameState>[] = [
{ id: 'char_exampler', kind: 'actor', name: 'Exampler!', lines: [] },
];
// Create locations
export const locations: ILocation<IGameState>[] = [
{ id: 'loc_discoville', kind: 'location', name: 'Discoville!', next: 'beginning', lines: [
{ text: 'It\'s a beautiful day at Discoville today!' },
{ text: 'A friendly feller is approaching you.' }
] },
];
Create game config
// config.ts
import { EngineConfig } from "discoteque/lib/engine/types";
import { IGameState, reducer } from "./store";
import skills, { GameSkills, skillDescriptions } from "./skills";
import { nodes, actors, locations } from "./game";
const config: EngineConfig<IGameState, undefined, GameSkills> = {
// Pass the initial skills
skills: skills,
// Set menu text values
menu: {
title: 'Example Story',
description: 'An Example Discoteque Story',
},
// Define how many points player can spend on start
skillPointsOnStart: 3,
// Define starting node
startNode: 'loc_discoville',
// Define time object
chrono: {
time: 750,
},
// Give skills some desctiptions
skillDescriptoins: skillDescriptions,
// Provide game's reducer
reducer: reducer,
// Supply the actual script (nodes)
nodes: [
...nodes,
...actors,
...locations,
],
};
export default config;
Use config in your app
Replace default project's index.tsx with this
// index.tsx
import makeApp from 'discoteque/lib';
import { injectGlobal } from 'emotion';
import config from './config';
import 'react-toastify/dist/ReactToastify.css';
// Set up default font
import font from "discoteque/src/assets/fonts/AnticSlab-Regular.woff2";
injectGlobal`
@font-face {
font-family: 'Antic Slab Regular';
src: url(${font}) format('woff2');
}
body {
font-family: 'Antic Slab Regular';
}
`;
// Import your config file
// Make an app. Mounting onto DOM is already handled by App.
makeApp(config);
Development
Run build
yarn build
Link
yarn link
Use link in live project
yarn link discoteque