First commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.vscode
|
16
Readme.md
Normal file
16
Readme.md
Normal 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
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
82
examples/ball_game.js
Normal 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
115
examples/ball_game.tw
Normal 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
43
storymanager.js
Normal 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;
|
Reference in New Issue
Block a user