Skip to content

Commit

Permalink
feat(characters): Ability to change the player character during the g…
Browse files Browse the repository at this point in the history
…ame (#115)
  • Loading branch information
liana-p committed Jul 5, 2023
1 parent 670e75b commit 91205ce
Show file tree
Hide file tree
Showing 15 changed files with 220 additions and 10 deletions.
4 changes: 4 additions & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ const config = {
text: 'Branching dialogue and choices',
link: '/features/branching-dialogue',
},
{
text: 'Changing the player character',
link: '/features/changing-player-character',
},
{ text: 'Game Settings', link: '/features/game-settings' },
{ text: 'Gamepad support', link: '/features/gamepad' },
{
Expand Down
9 changes: 9 additions & 0 deletions docs/commands/all-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,15 @@ See [Dynamic sprites and text documentation](../features/dynamic-sprites-text-ob
| delete_sprite | `delete_sprite $mySprite` | Deletes a sprite (stored in a variable) |
| empty_sprites | `empty_sprites [optional layer number]` | Deletes all sprites, on specified layer or all layers if no layer is passed |

## Characters

It is possible to change which character is used by the player (which is used when making choices). See the [changing player character feature docs](../features/changing-player-character.md) for more info.

| Command | Example | Description |
| ----------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------- |
| change_player_character | `change_player_character player_2` | Will use `player_2` as the character for the player's words in choices etc. |
| change_game_character | `change_game_character game_2` | Changes the default character used to represent the game (by default is a character with no name) |

## Others

| command | example | description |
Expand Down
95 changes: 95 additions & 0 deletions docs/features/changing-player-character.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
title: Changing the player character
description: Here is how to change the player character during gameplay in narrat
---

# Changing the player and game characters

## Changing player or game character in the config

In the characters config, values can be changed to change the default player and game characters.

**Note that those two characters must always exist.**

In `characters.yaml`:

```yaml
config:
imagesPath: img/characters/
playerCharacter: player # will use the character with key "player" in the list of chrracters whenever the player speaks
gameCharacter: game # Same for the game character
```
### The player character
The player character appears when making choices, as it's the character that says the line that you chose.
### The game character
The game character is the "default" / narrator character. When writing text without a talk command, it will appear as this character. Some generic system messages might also appear with this character. By default the `game` character has an empty name, which makes it appear as a line of text with no character. But you can change it to be an actual character.

## Changing the player and game characters during gameplay

The commands `change_player_character` and `change_game_character` are available to change which character is the active player, or the one representing the game (when a line of script has text in it with no talk command). They can be used during gameplay to allow dynamic change of the player character.

Here's an example script demoing those features:

```yaml
characters:
game:
name: ''
color: white
player:
style:
color: orange
sprites:
idle: player.webp
name: You
player2:
style:
color: green
sprites:
idle: player.webp
name: Player 2
game2:
name: 'The Narrator'
style:
color: red
sprites:
idle: helper_cat.webp
helper:
sprites:
idle: helper_cat.webp
style:
color: green
name: Helper Cat
```

```narrat
main:
jump test_change_player
test_change_player:
talk helper idle "Let's change who the player character is."
choice:
"change character?"
"Default player character":
change_player_character player
"Second player character":
change_player_character player2
jump test_change_player_2
test_change_player_2:
talk helper idle "Ok, we've changed player character. The new name should appear when making choices now."
choice:
talk helper idle "Did you like changing the player character?"
"Yes":
talk helper idle "I'm glad you liked it"
"No":
talk helper idle "I'm sorry you didn't like it. We can change again if you want."
change_game_character game2
"Demo game character change"
change_game_character game
"Demo back to default game character"
jump test_change_player
```
14 changes: 14 additions & 0 deletions packages/narrat/examples/games/default/data/characters.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
config:
imagesPath: img/characters/
playerCharacter: player
gameCharacter: game
characters:
game:
name: ''
Expand All @@ -11,6 +13,18 @@ characters:
sprites:
idle: player.webp
name: You
player2:
style:
color: green
sprites:
idle: player.webp
name: Player 2
game2:
name: 'The Narrator'
style:
color: red
sprites:
idle: helper_cat.webp
cat:
sprites:
idle: cat_idle.webp
Expand Down
4 changes: 4 additions & 0 deletions packages/narrat/src/config/characters-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ export type CharacterConfig = Static<typeof CharacterConfigSchema>;
export const CharactersFilesConfigSchema = Type.Object({
config: Type.Object({
imagesPath: Type.String(),
playerCharacter: Type.Optional(Type.String()),
gameCharacter: Type.Optional(Type.String()),
}),
characters: Type.Record(Type.String(), CharacterConfigSchema),
});
export type CharactersFilesConfig = Static<typeof CharactersFilesConfigSchema>;
export const defaultCharactersConfig: CharactersFilesConfig = {
config: {
imagesPath: '',
playerCharacter: 'player',
gameCharacter: 'game',
},
characters: {},
};
30 changes: 27 additions & 3 deletions packages/narrat/src/examples/default/scripts/default.narrat
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,34 @@ main:
// if (== $global.counter 2):
// unlock_achievement win_game
// talk player idle "Global counter is %{$global.counter}"
// jump test_change_player
// jump test_hmr
set_screen @empty 2
jump quest_demo

test_change_player:
talk helper idle "Let's change who the player character is."
choice:
"change character?"
"Default player character":
change_player_character player
"Second player character":
change_player_character player2
jump test_change_player_2

test_change_player_2:
talk helper idle "Ok, we've changed player character. The new name should appear when making choices now."
choice:
talk helper idle "Did you like changing the player character?"
"Yes":
talk helper idle "I'm glad you liked it"
"No":
talk helper idle "I'm sorry you didn't like it. We can change again if you want."
change_game_character game2
"Hello game"
change_game_character game
"Hello 2"
jump test_change_player

test_hmr:
talk helper idle "Hello hmr 1"
jump test_hmr_2
Expand All @@ -29,9 +53,9 @@ test_custom_settings:
"Test, %{$data.name}"

test_label_reload:
run set_config_overrides
// run set_config_overrides
"Game reloaded"
talk player idle "Welcome back"
talk helper idle "Welcome back"

set_config_overrides:
set config.characters.characters.player.name $data.playerName
Expand Down
23 changes: 23 additions & 0 deletions packages/narrat/src/stores/config-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { Config, defaultConfig } from '@/config/config-output';
import { defineStore } from 'pinia';
import deepmerge from 'deepmerge';

export type ConfigStoreSave = {
playerCharacter: string;
gameCharacter: string;
};

export const useConfig = defineStore('config', {
state: () => {
const config: Config = defaultConfig;
Expand All @@ -16,5 +21,23 @@ export const useConfig = defineStore('config', {
extendConfig(config: Partial<Config>) {
this.config = deepmerge(this.config, config);
},
generateSaveData(): ConfigStoreSave {
return {
playerCharacter: this.playerCharacter,
gameCharacter: this.gameCharacter,
};
},
loadSaveData(saveData: ConfigStoreSave) {
this.config.characters.config.playerCharacter = saveData.playerCharacter;
this.config.characters.config.gameCharacter = saveData.gameCharacter;
},
},
getters: {
playerCharacter(): string {
return this.config.characters.config.playerCharacter ?? 'player';
},
gameCharacter(): string {
return this.config.characters.config.gameCharacter ?? 'game';
},
},
});
3 changes: 3 additions & 0 deletions packages/narrat/src/stores/main-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { useMenu } from './menu-store';
import { useScreenObjects } from './screen-objects-store';
import { useAchievements } from './achievements-store';
import { useSettings } from './settings-store';
import { useConfig } from './config-store';

export function defaultAppOptions(): AppOptions {
return {
Expand Down Expand Up @@ -446,6 +447,7 @@ export const useMain = defineStore('main', {
metadata,
settings: useSettings().generateSaveData(),
screenObjects: useScreenObjects().generateSaveData(),
config: useConfig().generateSaveData(),
};
vm.plugins.forEach((plugin) => {
if (plugin.save) {
Expand Down Expand Up @@ -486,6 +488,7 @@ export const useMain = defineStore('main', {
const inventoryStore = useInventory();
this.loadGlobalData();
useScreenObjects().loadSaveData(save.screenObjects);
useConfig().loadSaveData(save.config);
screensStore.loadSaveData(save.screen);
skillsStore.loadSaveData(save.skills);
dialogStore.loadSaveData(save.dialog);
Expand Down
3 changes: 2 additions & 1 deletion packages/narrat/src/stores/vm-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { isScreenObject, useScreenObjects } from './screen-objects-store';
import { GlobalGameSave } from '@/types/game-save';
import { getSaveFile } from '@/utils/save-helpers';
import { NarratScript } from '@/types/app-types';
import { useConfig } from './config-store';

export type AddFrameOptions = Omit<SetFrameOptions, 'label'> & {
label?: string;
Expand Down Expand Up @@ -392,7 +393,7 @@ export const useVM = defineStore('vm', {
// const dialogStore = useDialogStore();
if (getConfig().debugging.showScriptFinishedMessage) {
// dialogStore.addDialog({
// speaker: 'game',
// speaker: useConfig().gameCharacter,
// text: '[DEBUG] Game Script is finished. This is the end of the game flow. This message only appears in debug mode.',
// });
}
Expand Down
3 changes: 2 additions & 1 deletion packages/narrat/src/vm/commands/choice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { MachineBlock, useVM } from '@/stores/vm-store';
import { useSkills } from '@/stores/skills';
import { commandRuntimeError } from './command-helpers';
import { getSkillCheckConfig, skillCheckConfigExists } from '@/config';
import { useConfig } from '@/stores/config-store';

export const runChoice: CommandRunner<
ChoiceOptions,
Expand Down Expand Up @@ -190,7 +191,7 @@ const onChoicePlayerAnswered = async (
if (playerText) {
// If the choice involves printing a player dialog, show it
const dialog: AddDialogParams = {
speaker: 'player',
speaker: useConfig().playerCharacter,
text: playerText,
interactive: false,
};
Expand Down
8 changes: 7 additions & 1 deletion packages/narrat/src/vm/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ import {
sliceCommand,
spliceCommand,
} from './array-commands';
import { loadDataPlugin } from './util-commands';
import {
changeGameCharacterPlugin,
changePlayerCharacterPlugin,
loadDataPlugin,
} from './util-commands';
import {
nowPlugin,
sessionPlaytimePlugin,
Expand Down Expand Up @@ -281,6 +285,8 @@ export function registerBaseCommands(vm: VM) {

// Util commands
vm.addCommand(loadDataPlugin);
vm.addCommand(changePlayerCharacterPlugin);
vm.addCommand(changeGameCharacterPlugin);

// Time commands
vm.addCommand(nowPlugin);
Expand Down
5 changes: 3 additions & 2 deletions packages/narrat/src/vm/commands/text-field.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { AddDialogParams, useDialogStore } from '@/stores/dialog-store';
import { commandRuntimeError } from './command-helpers';
import { CommandPlugin } from './command-plugin';
import { useConfig } from '@/stores/config-store';

export const textFieldPlugin = CommandPlugin.FromOptions<{ prompt: string }>({
keyword: 'text_field',
argTypes: [{ name: 'prompt', type: 'string' }],
returnAfterPlayerAnswer: true,
runner: async (cmd) => {
const dialog: AddDialogParams = {
speaker: 'game',
speaker: useConfig().gameCharacter,
text: cmd.options.prompt,
textField: true,
interactive: true,
Expand All @@ -31,7 +32,7 @@ export const textFieldPromptPlugin = CommandPlugin.FromOptions<{
argTypes: [{ name: 'prompt', type: 'string' }],
runner: async (cmd) => {
const dialog: AddDialogParams = {
speaker: 'game',
speaker: useConfig().gameCharacter,
text: cmd.options.prompt,
textField: true,
interactive: true,
Expand Down
3 changes: 2 additions & 1 deletion packages/narrat/src/vm/commands/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
generateParser,
} from './command-plugin';
import { textCommand } from '../vm-helpers';
import { useConfig } from '@/stores/config-store';

export interface TalkArgs {
speaker: string;
Expand Down Expand Up @@ -86,7 +87,7 @@ export const textCommandPlugin = CommandPlugin.FromOptions<
argTypes: [],
runner: async (cmd, choices) => {
await textCommand({
speaker: 'game',
speaker: useConfig().gameCharacter,
cssClass: 'text-command',
text: cmd.staticOptions.text,
choices,
Expand Down
Loading

0 comments on commit 91205ce

Please sign in to comment.