discoteque v2.1.1
Discoteque
Text-based interactive ficiton with RPG elements
Allows you to create simple RPG-esque text stories that follow basic principles. 1. You can read text that is spoken by characters or just presented as is ("narration"). 2. You can navigate branching dialogue. 3. You can distribute and use skills to pass dialogue checks and gain them. 4. State can be used to track progress / remember decisions / set "VN-like" flags. 5. 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
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago