First commit

This commit is contained in:
David Masad
2021-01-27 15:30:48 -05:00
commit dc8d36b4de
6 changed files with 569 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.vscode

16
Readme.md Normal file
View File

@ -0,0 +1,16 @@
# Story Manager
A lightweight storylet manager for Twee and Sugarcube.
## Feature list
- [X] Generating potential storylets based on state
- [X] Filtering to N storylets based on priority
- [ ] Allowing some storylets to interrupt and take priority
- [ ] Track storylet history (e.g. to prevent repetition)
- [ ] Storylets bound to specific data
- [ ] Storylet with any binding
- [ ] Storylet tagging and filtering (i.e. pull from only a subset of storylets)
- [ ] Macro / widget for displaying storylet links
- [ ]

312
examples/ball_game.html Normal file

File diff suppressed because one or more lines are too long

82
examples/ball_game.js Normal file
View File

@ -0,0 +1,82 @@
// Set up narrative model
// -----------------------------------------------------------------------
// Set up NPCs
// ------------------------------------------
var firstNames = {
"m": ["William", "George", "Jose", "Kwame", "Vin", "Alexander", "Martin", "Daniel",
"Nicholas", "Sorin", "Vlad", "Matthew", "Octavius"],
"f": ["Alice", "Bella", "Charlotte", "Doreen", "Francesca", "Willhelmina", "Xenia",
"Juliette", "Rosemary", "Margot"]
};
var lastNames = ["Abar", "Bridgewater", "Clarence", "Delmar", "Ellseworth", "Fox",
"Williams", "Rose"];
var pronouns = {
"m": {he: "he", his: "his", him: "him"},
"f": {he: "she", his: "hers", him: "her"}
}
var nChars = 10;
State.variables.characters = [];
for (let id=0; id<nChars; id++) {
let gender = randomChoice(["m", "f"]);
let newPerson = {
id: id,
firstname: randomChoice(firstNames[gender]),
lastname: randomChoice(lastNames),
poetry: randomChoice([0, 0, 1, 1, 2]),
gossip: randomChoice([0, 0, 1, 1, 2]),
friendship: 0,
romance: 0
};
for (let key in pronouns[gender]) newPerson[key] = pronouns[gender][key];
State.variables.characters.push(newPerson);
}
// Set up storylets themselves
// ------------------------------------------
NarrativeManager.storyletMakers = {
"Dance": function() {
let storylets = [];
if (State.variables.location != "Ballroom") return storylets;
for (let id in State.variables.characters) {
let char = State.variables.characters[id];
if (char.romance >= 0) {
let storylet = {
passage: "DancingRoot",
description: "Dance with " + char.firstname + " " + char.lastname,
priority: 0,
payload: {character: char}
}
storylets.push(storylet);
}
}
return storylets;
},
"Conversation": function() {
let storylets = [];
if (State.variables.location != "Library") return storylets;
for (let id in State.variables.characters) {
let char = State.variables.characters[id];
if (char.friendship >= 0) {
let storylet = {
passage: "ConversationRoot",
description: "Talk with " + char.firstname + " " + char.lastname,
priority: 0,
payload: {character: char}
}
storylets.push(storylet);
}
}
return storylets;
},
}

115
examples/ball_game.tw Normal file
View File

@ -0,0 +1,115 @@
:: StoryTitle
Pansexual Space Regency Party (working title)
:: StoryData
{
"ifid": "E9DEA250-BBFD-4C92-8E8C-53212FBB2083"
}
:: Story JavaScript [script]
Config.passages.nobr = true; // Deal with linebreaks.
:: Start
<<set $poetry = 0>>
<<set $gossip = 0>>
You can go to the [[Ballroom]] or the [[Library]].
:: Ballroom
<<set $location = "Ballroom">>
You're in the ballroom.<br>
<<set _possibleStories = window.NM.getNStorylets(3)>>
<<for _story range _possibleStories>>
<<capture _story>>
[[_story.description|_story.passage][$payload=_story.payload]]<br>
<</capture>>
<</for>>
Or you can go to the [[Library]]
:: DancingRoot
<<set $partner = $payload["character"]>>
You take <<print $partner.firstname>>'s hand and step onto the dance floor. As you dance, you can
talk about [[poetry|Conversation-Poetry-Dancing]], [[trade gossip|Conversation-Gossip]], the latest
[[current events|Conversation-Politics-Dancing]] -- or you can [[dance in silence|Dance-Quietly]].<br>
:: Conversation-Poetry-Dancing
<<if $poetry > $partner.poetry>>
(Poetry success)
<<elseif $poetry <= $partner.poetry>>
(Poetry fail)
<<if random(100) < 50>>
(Poetry up)
<<set $poetry = $poetry + 1>>
<</if>>
<</if>>
<br>
[[Return | Ballroom]]
:: Conversation-Politics-Dancing
(This is inappropriate to do while dancing)
<<set $characters[$partner.id].romance = $partner.romance - 1>>
[[Return | Ballroom]]
:: Dance-Quietly
(Dance quietly, some chance of improving romance goes here)
[[Return | Ballroom]]
:: Conversation-Gossip
<<if $gossip > $partner.gossip>>
"I heard-" you begin archy, and are able to share some juicy gossip you heard earlier, earning a smile.
<<elseif $gossip <= $partner.gossip>>
You bring up a rumor you read earlier, but you can see it's old news already.
<<if random(100) < 50>>
Fortunately, they reply with a rumor of their own -- one you haven't heard yet.
<<set $gossip = $gossip + 1>>
<</if>>
<</if>>
<br>
<<if $location == "Ballroom">>
[[Return | Ballroom]]
<<elseif $location == "Library">>
[[Return | Library]]
<</if>>
:: Library
<<set $location = "Library">>
You're in the library.<br>
<<set _possibleStories = window.NM.getNStorylets(3)>>
<<for _story range _possibleStories>>
<<capture _story>>
[[_story.description|_story.passage][$payload=_story.payload]]<br>
<</capture>>
<</for>>
Or you can go to the [[Ballroom]]
:: ConversationRoot
<<set $partner = $payload["character"]>>
You talk to $partner.firstname in the library. You can talk about [[poetry|Conversation-Poetry-Talking]], [[trade gossip|Conversation-Gossip]], or the latest
[[current events|Conversation-Politics-Talking]].<br>
:: Conversation-Poetry-Talking
<<if $poetry > $partner.poetry>>
(Poetry success)
<<elseif $poetry <= $partner.poetry>>
(Poetry fail)
<<if random(100) < 50>>
(Poetry up)
<<set $poetry = $poetry + 1>>
<</if>>
<</if>>
<br>
[[Return | Library]]
:: Conversation-Politics-Talking
<<if random(0, 100) < 50>>
<<set $characters[$partner.id].friendship = $partner.friendship + 1>>
(Politics conversation success)
<<else>>
<<set $characters[$partner.id].friendship = $partner.friendship - 1>>
(Politics conversation failure)
<</if>>
[[Return | Library]]

43
storymanager.js Normal file
View File

@ -0,0 +1,43 @@
// Choose one of an array
var randomChoice = function(vals) {
return vals[Math.floor(Math.random() * vals.length)];
};
// Set up the general narrative manager
// -----------------------------------------------------------------------
var NarrativeManager = {meta: []};
NarrativeManager.getAllStorylets = function() {
let allStorylets = [];
for (let key in this.storyletMakers) {
let storylets = this.storyletMakers[key]();
for (let i in storylets) allStorylets.push(storylets[i]);
}
return allStorylets;
}
NarrativeManager.getNStorylets = function(n) {
/*
Get N storylets, prioritizing the highest-priority ones first.
For now assume that priorities are integers, but it would be nice
to be more flexible.
*/
let allStorylets = this.getAllStorylets();
n = Math.min(n, allStorylets.length);
if (n == 0) return [];
let selectedStorylets = [];
// First sort by ascending priority:
allStorylets.sort((a, b) => b.priority - a.priority);
let currentPriority = allStorylets[0].priority;
while (selectedStorylets.length < n) {
let priorityStorylets = allStorylets.filter(s => s.priority==currentPriority);
priorityStorylets.sort((a, b) => (Math.random() - Math.random()));
let nAdd = Math.min(n - selectedStorylets.length, priorityStorylets.length);
for (let i=0; i<nAdd; i++) selectedStorylets.push(priorityStorylets.pop());
currentPriority--;
}
return selectedStorylets;
}
window.NM = NarrativeManager;