Finalizing tutorial and creating final game example

This commit is contained in:
David Masad
2021-01-29 22:46:04 -05:00
parent b598d050f4
commit 83088139e5
7 changed files with 645 additions and 15 deletions

View File

@ -17,4 +17,5 @@ A lightweight storylet manager for Twee and Sugarcube.
- [X] Storylet tagging and filtering (i.e. pull from only a subset of storylets)
- [X] Widget for displaying storylet links
- [ ] Make the widget into a macro
- [ ] Weighted random choice
- [ ] Weighted random choice
- [ ] Explore replacing storylet generators returning arrays with the `yield` keyword? **Pro:** produces cleaner code; **Con:** requires users to understand `yield` and remember to use the function * notation.

View File

@ -287,32 +287,110 @@ And we'll create the corresponding storylet passage, which will also implement t
[[Keep circulating | Start]]
```
## Finishing touches
So far we've covered all of StoryManager's core features. But let's go ahead and add some final details to our game. StoryManager is most useful when your game contains more data or logic implemented in straight JavaScript. Three people isn't very many at a party -- but more starts getting tiring to create by hand. Let's implement some simple procedural generation to populate our party. While we're at it, let's add some actual text content to the dialog topics. Finally, just as the protagonist gains knowledge from conversation, let's make sure NPC knowledge goes up too -- to make sure the player can't grind up their reputation by sharing the same fact with the same person over and over.
Note: As you start writing more JavaScript for your game, it might be worth it to split it off into its own `.js` file, to take advantage of syntax checking that text editors like VSCode or Sublime Text provide. Tweego can merge multiple JavaScript files together into your final Twee file.
Below is some simple code to generate some NPCs to populate the party. It makes sure there's one at each knowledge level per topic, to give the player an opportunity to explore all the conversation topics in full.
```javascript
// Choose one of an array
var randomChoice = function(vals) {
return vals[Math.floor(Math.random() * vals.length)];
};
### Adding content
// Names to draw from
let firstNames = ["Arabella", "Bianca", "Carlos", "Dorian", "Ellery", "Fra.",
"Gregory", "Harlowe", "Irina", "Xavier", "Yskander", "Zenia"];
let lastNames = ["Archer", "Brookhaven", "Croix", "Delamer", "Evermoore",
"Fitzparn", "Sutch", "Tremont", "Ulianov", "Van Otten"];
Let's create these topics:
State.variables.characters = [];
// Generate one character with knowledge 1-3 per topic
let id = 0;
for (let topic in State.variables.playerKnowledge) {
for (let i=1; i<=3; i++) {
let firstName = randomChoice(firstNames);
let lastName = randomChoice(lastNames);
let char = {id: id, name: firstName + " " + lastName,
poetry: 0, industry: 0, astronomy: 0};
char[topic] = i;
State.variables.characters.push(char);
id++;
}
}
```
To populate the conversations, we can also create some text to go along with each knowledge level:
```javascript
State.variables.conversationTopics = {
"Poetry": [ "the new book of praise verse by Miss Causewell",
"the recitations at Madame Bautan's salon",
"Miss Causewell's previous volume, which was banned by the Censor."],
"Industry": ["the engine factory that recently opened by the Long Bridge.",
"the new railway line being proposed to Draundle.",
"the explosion at the shipyards."],
"Astronomy": ["the new red star in the night sky.",
"the latest theories about the star from Imperial University.",
"Professor Hix, the court astronomer, has not been seen in some time."]
"poetry": [ "the new book of praise verse by Miss Causewell",
"the recitations at Madame Bautan's salon",
"Miss Causewell's previous volume, which was banned by the Censor"],
"industry": ["the engine factory that recently opened by the Long Bridge",
"the new railway line being proposed to Draundle",
"the explosion at the shipyards"],
"astronomy": ["the new red star in the night sky",
"the latest theories about the star from Imperial University",
"how Professor Hix, the court astronomer, has not been seen in some time"]
}
```
We'll probably also want more than three characters, to give the player enough people to converse with. Instead of writing them all by hand, now would be a good time to add some simple procedural generation.
Then we also tweak the `:: Conversation topic` passage, to print the topic text as needed, and to increment the NPCs' knowledge.
```
:: Conversation topic
<<set $topic = $currentStorylet.topic>>
<<if $playerKnowledge[$topic] < $talkingTo[$topic] >>
$talkingTo.name tells you about <<print $conversationTopics[$topic][$playerKnowledge[$topic]]>>.
<<set $playerKnowledge[$topic] = $playerKnowledge[$topic] + 1>>
<<set $reputation = $reputation - 0.5>>
<<elseif $playerKnowledge[$topic] == $talkingTo[$topic]>>
You and $talkingTo.name discuss <<print $conversationTopics[$topic][$playerKnowledge[$topic]]>>.
<<set $reputation = $reputation + 1>>
<<elseif $playerKnowledge[$topic] > $talkingTo[$topic]>>
You tell $talkingTo.name about <<print $conversationTopics[$topic][$talkingTo[$topic]]>>.
<<set $characters[$talkingTo.id][$topic] = $talkingTo[$topic] + 1>>
They listen intently, and seem impressed.
<<set $reputation = $reputation + 2>>
<</if>><br><br>
```
Notice that we added some reputation effects: being ignorant makes your reputation go down a bit; knowing things makes it go up. In the full source, we also updated the decision to `Snub` someone by having it lower your reputation.
Finally, we can add two new storylets: a failure condition for if your reputation goes too low, and a success condition when it gets high enough.
```javascript
firstNames
```
StoryManager.storylets["Asked to leave"] = {
name: "Asked to leave",
tags: ["circulating"],
generate: function() {
if (State.variables.reputation < -1 && Math.random() < 0.5) {
return [{
passage: "Asked to leave",
description: "You see a footman approaching you.",
interrupt: true
}]
}
}
};
StoryManager.storylets["Seeing the duchess"] = {
name: "Seeing the duchess",
tags: ["circulating"],
generate: function() {
if (State.variables.reputation > 6 && Math.random() < 0.5) {
return [{
passage: "Invited to see the Duchess",
description: "You see a footman approaching you.",
interrupt: true
}]
}
}
};
```
And that's it!

355
examples/duchess_party.html Normal file

File diff suppressed because one or more lines are too long

130
examples/duchess_party.js Normal file
View File

@ -0,0 +1,130 @@
// Game stats
State.variables.reputation = 0;
State.variables.playerKnowledge = {poetry: 0, industry: 0, astronomy: 0};
// Generate characters
// Choose one of an array
var randomChoice = function(vals) {
return vals[Math.floor(Math.random() * vals.length)];
};
// Names to draw from
let firstNames = ["Arabella", "Bianca", "Carlos", "Dorian", "Ellery", "Fra.",
"Gregory", "Harlowe", "Irina", "Xavier", "Yskander", "Zenia"];
let lastNames = ["Archer", "Brookhaven", "Croix", "Delamer", "Evermoore",
"Fitzparn", "Sutch", "Tremont", "Ulianov", "Van Otten"];
State.variables.characters = [];
// Generate one character with knowledge 1-3 per topic
let id = 0;
for (let topic in State.variables.playerKnowledge) {
for (let i=1; i<=3; i++) {
let firstName = randomChoice(firstNames);
let lastName = randomChoice(lastNames);
let char = {id: id, name: firstName + " " + lastName,
poetry: 0, industry: 0, astronomy: 0};
char[topic] = i;
State.variables.characters.push(char);
id++;
}
}
// Conversation topics
State.variables.conversationTopics = {
"poetry": [ "the new book of praise verse by Miss Causewell",
"the recitations at Madame Bautan's salon",
"Miss Causewell's previous volume, which was banned by the Censor"],
"industry": ["the engine factory that recently opened by the Long Bridge",
"the new railway line being proposed to Draundle",
"the explosion at the shipyards"],
"astronomy": ["the new red star in the night sky",
"the latest theories about the star from Imperial University",
"how Professor Hix, the court astronomer, has not been seen in some time"]
}
// Set up the storylets
// ==========================================================================
StoryManager.storylets["Conversation"] = {
name: "Conversation",
tags: ["circulating"],
generate: function() {
let storylets = [];
for (let i in State.variables.characters) {
let character = State.variables.characters[i];
let storylet = {
passage: "Conversation",
description: "Talk to " + character.name,
priority: 0,
character: character
}
storylets.push(storylet);
}
return storylets;
}
};
StoryManager.storylets["Buttonholed"] = {
name: "Buttonholed",
tags: ["circulating"],
generate: function() {
if (Math.random() < 0.2) {
let char = randomChoice(State.variables.characters);
return [{
passage: "Being approached",
description: "You see " + char.name + " approaching you.",
interrupt: true,
character: char
}]
}
}
};
StoryManager.storylets["Conversation topic"] = {
name: "Conversation topic",
tags: ["during conversation"],
generate: function() {
let storylets = [];
for (let topic in State.variables.playerKnowledge) {
if (State.variables.playerKnowledge[topic] > 0 |
State.variables.talkingTo[topic] > 0)
storylets.push({
passage: "Conversation topic",
description: "Talk about " + topic,
priority: 0,
topic: topic
})
}
return storylets;
}
};
StoryManager.storylets["Asked to leave"] = {
name: "Asked to leave",
tags: ["circulating"],
generate: function() {
if (State.variables.reputation < -1 && Math.random() < 0.5) {
return [{
passage: "Asked to leave",
description: "You see a footman approaching you.",
interrupt: true
}]
}
}
};
StoryManager.storylets["Seeing the duchess"] = {
name: "Seeing the duchess",
tags: ["circulating"],
generate: function() {
if (State.variables.reputation > 6 && Math.random() < 0.5) {
return [{
passage: "Invited to see the Duchess",
description: "You see a footman approaching you.",
interrupt: true
}]
}
}
};

64
examples/duchess_party.tw Normal file
View File

@ -0,0 +1,64 @@
:: StoryTitle
At the Duchess's Party
:: StoryData
{
"ifid": "BE18C022-A213-466C-8DD1-DCCD5CB1DF48"
}
:: Story JavaScript [script]
Config.passages.nobr = true; // Deal with linebreaks.
:: Start
The footmen at the door to the duchess's city residence bows over your forged invitation, seemingly not examining at all. The uniform you wear is authentic, at least, though Frin had found a tailor who would accept some extra florins to not demand to see a letter of appointment before sewing on captain's bars. And just like that, you're in. Could it be that easy, you wonder? <br> <br>
Of course it isn't. The hall is filled with aristocrats in evening-wear, making small talk in an ever-shifting constellation. Across the room, the doors to the duchess's private rooms are firmly closed. No way to sneak in without being seen. You're going to have to find a way to [[talk your way in | Circulating]].
:: Circulating
You mingle through the crowd, keeping a wary eye around you.<br>
<<set _possibleStories = window.SM.getStorylets(3, "circulating")>>
<<ShowStoryletLinks _possibleStories>>
:: Conversation
<<set $talkingTo = $currentStorylet.character>>
You talk with $talkingTo.name. <br>
<<set _possibleStories = window.SM.getStorylets(3, "during conversation")>>
<<ShowStoryletLinks _possibleStories>>
[[Keep circulating | Circulating]]
:: Being approached
$currentStorylet.character.name is coming toward you to talk. <br>
You can [[talk to them | Conversation]], or risk snubbing them by [[trying to get away | Circulating][$reputation = $reputation - 1]].
:: Conversation topic
<<set $topic = $currentStorylet.topic>>
<<if $playerKnowledge[$topic] < $talkingTo[$topic] >>
$talkingTo.name tells you about <<print $conversationTopics[$topic][$playerKnowledge[$topic]]>>.
<<set $playerKnowledge[$topic] = $playerKnowledge[$topic] + 1>>
<<set $reputation = $reputation - 0.5>>
<<elseif $playerKnowledge[$topic] == $talkingTo[$topic]>>
You and $talkingTo.name discuss <<print $conversationTopics[$topic][$playerKnowledge[$topic]]>>.
<<set $reputation = $reputation + 1>>
<<elseif $playerKnowledge[$topic] > $talkingTo[$topic]>>
You tell $talkingTo.name about <<print $conversationTopics[$topic][$talkingTo[$topic]]>>.
<<set $characters[$talkingTo.id][$topic] = $talkingTo[$topic] + 1>>
They listen intently, and seem impressed.
<<set $reputation = $reputation + 2>>
<</if>><br><br>
[[Keep circulating | Circulating]]
:: Asked to leave
The footman demands to see your invitation.
Before you know it, you are firmly escorted through a back hallway, past the kitchens, and finally
out through the servant's entrance. A single guard glares at you, as of committing your face
to memory to make sure you'll never be able to come back.<br><br>
FAILURE
:: Invited to see the Duchess
The footman discreetly bows his head under his high cap. "Her grace wishes to speak with you," he says. <br>
You follow him as he leads you away from the main hall, and toward the Duchess's private rooms. <br><br>
VICTORY

View File

@ -172,6 +172,7 @@ window.SM = StoryManager;
/* twine-user-script #2: "Story JavaScript" */
Config.passages.nobr = true; // Deal with linebreaks.
State.variables.reputation = 0;
State.variables.playerKnowledge = {poetry: 0, industry: 0, astronomy: 0};
State.variables.characters = [

View File

@ -9,6 +9,7 @@ At the Duchess's Party
:: Story JavaScript [script]
Config.passages.nobr = true; // Deal with linebreaks.
State.variables.reputation = 0;
State.variables.playerKnowledge = {poetry: 0, industry: 0, astronomy: 0};
State.variables.characters = [