Twine Version: 2.3.9
Story Format: SugarCube 2.33.2
(edit: minor rephrasing for sake of clarity)
TL;DR
Im afraid my code for NPC action randomization is unnecessarily complicated and I might not be able to expand/maintain it. Can I do better? How?
I’m using Twine with Sugarcube 2 to create an RPG with a substantial amount of randomized Non Player Character action. I am a newbie to programming/scripting; most probably my goals are way to ambitious and I am in over my head. But I am willing to learn.
Thanks to examples, discussions and documentation provided by the awesome Twine community I was able to glue some working code together that allows me …
- to track where NPCs are and what they are doing. (Only a subset of all NPCs are “active”. I am concerned about the performance as I want to be able to use/generate dozens, maybe even between 100 and 200 randomized in the future, so I to limit the number of NPCs the code has to deal with.)
- identify a set of potential actions for each NPC based on the NPC’s class and location. (I want to expand this functionality with action options based on e.g. time schedules, weather, their characteristics, their current action etc. and make use of probabilities for each action option.)
- from the set of potential actions for each active NPC randomly choose a next action, including movement to other locations/passages. (I am thinking about giving actions a - partly randomized - duration, allowing interactions between NPCs etc.)
Given my inexperience I am concerned that my approach (basically nesting an awful lot of “if”-macros in a way that looks shady even to myself) may prove too pedestrian and will prevent me from accomplish my ambitious goals.
So I am asking not for help with a specific problem but for your critical feedback: Am I on the right track? Is there an easier way to do what I am doing that might also be better expandable?
As I said, I am only starting to learn. Please bear with me being slow.
Here is part of the code. I hope it is not too much and not too little information.
I have not included:
- the StoryInit where I include passages like “locations” and “characters” and register some of the arrays.
- the “movement” widget (copied idling’s code from by http://twinery.org/questions/40582/wandering-npc-type-character)
- the “time” widget (copied Mad Exile’s Gregorian Date & Time Widgets from http://twinery.org/questions/20222/help-making-simple-clock-calendar-system-for-sugarcube-twine)
- passages like “classes” and the location passages (like “innCommonRoom”)
Passage “locations” (incomplete, just in here so you get the basic idea):
<<set $innCommonRoom to {
id: "innCommonRoom",
region: "town",
area: "inn",
name: "commmon room",
exits: ["innStairwell", "innStable", "innKitchen", "mainStreetLower"],
}>>
[...]
<<set $locations to [$innCommonRoom, $innStable, $innStairwell, $innCellar, $innKitchen, $innFirstFloor, $innGuestroom1, $innGuestroom2, $innGuestroom3, $mainStreetLower, $marketplace, $castleTowerGround, $castleTowerFirst, $harborPier, $shipUpperDeck]>>
Passage "characters"
<<set $pc to {
id: "pc",
name: "Dana Sugarcube",
sex: "female",
race: "human",
class: "rogue",
location: $innCommonRoom,
area: "inn",
region: "town",
destination: $innCommonRoom,
focus: false,
currentAction: "",
}>>
<<set $npc1 to {
id: "npc1",
name: "Testriel Brightmoon",
sex: "female",
race: "elven",
class: "wizard",
location: $innCommonRoom,
area: "inn",
region: "town",
destination: $innCommonRoom,
focus: false,
currentAction: "sitAtTable",
}>>
[...]
<<set $chars to [$npc1, $npc2, $npc3, $npc4, $npc5]>>
Passage "actions"
<<set $leave to {
id: "leave",
description: "leaves",
standard: true,
locations: [],
classes: [],
probability: 20,
}>>
<<set $sitAtTable to {
id: "sitAtTable",
description: "sits at a table.",
standard: false,
locations: ["innCommonRoom"],
classes: [],
probability: 50,
}>>
[...]
<<set $actions to [$leave, $sitAtTable, $orderBeer, $drinkBeer, $napChair, $napStraw, $smallTalk, $steal]>>
Passage “hereComesTheMess” (merged from several passages that are in the original several passages linked via <>)
<<nobr>>
/* Identify PC's current whereabouts (passage and area) and store them in PC array. */
<<for _i to 0; _i lt $locations.length; _i++>>
<<if $locations[_i].id == passage()>>
<<set $pc.area to $locations[_i].area>>
<<set $pc.location to $locations[_i].id>>
<<set $pc.locationName to $locations[_i].name>>
<</if>>
<</for>>
/* Reset the presentChars, regionChars and focusChars arrays; identify all characters present at PC's current whereabouts and store them in the presentChars array; identify all characters present in area of PC's current whereabouts and store them in the regionChars array; identify all focused characters and store them in the focusChars array. */
<<set $presentChars = []>>
<<set $regionChars = []>>
<<set $focusChars = []>>
<<for _q to 0; _q lt $chars.length; _q++>>
<<if $chars[_q].location == passage()>>
<<set $presentChars.push($chars[_q].id)>>
<</if>>
<<if $chars[_q].region == $pc.region>>
<<set $regionChars.push($chars[_q].id)>>
<</if>>
<<if $chars[_q].focus == true>>
<<set $focusChars.push($chars[_q].id)>>
<</if>>
<</for>>
/* Combine regionChars array and focusChars array in the new activeChars array which defines all characters that are actively manipulated in the background. */
<<set $activeChars to [].concat($regionChars, $focusChars)>>
/* Display PC's current whereabouts (passage and area). */
I'm in the $pc.area's $pc.locationName.
/* Identify and display if the location is private, public, populated or crowded. */
<<if tags().includes('private')>>
This is a private place<<else>>This is a public place<</if>><<if tags().includes('populated')>> and there are some people around<<elseif tags().includes('crowded')>>and there are many people around<<else>><</if>>.<br>
/* Display all characters present at PC's current whereabouts; based on the present chars being known/unknown to the PC display either name or description. */
<<for _l to 0; _l lt $chars.length; _l++>>
<<if $chars[_l].location == passage()>>
<<if $chars[_l].unknown == true>>
A $chars[_l].sex $chars[_l].race $chars[_l].class is here.
<<else>><<hovertip "A $chars[_l].sex $chars[_l].race $chars[_l].class.">>$chars[_l].name<</hovertip>> is here.
<</if>>
<</if>>
<</for>>
/* Display if noone is there. */
<<if $presentChars.length == 0 and not tags().includes('populated', 'crowded')>><<set _alone to true>>I'm alone.
<</if>>
/* For each active char go through the actions list and select an action as available for this char if it is
1) a standard action
2) a class action matching the char's class
3) a location actions matching the char's location.
From this array, randomly select one action to be the char's current actions. */
/* Go through all registered characters that are active and reset their possible actions and their probabilities. */
<<for _q1 to 0; _q1 lt $chars.length; _q1++>>
<<for _q2 to 0; _q2 lt $activeChars.length; _q2++>>
<<if $chars[_q1].id is $activeChars[_q2]>>
<<set $activeCharsActions[_q2] to {
id: [],
probability: [],
}>>
/* Go through all registered actions. */
<<for _q4 to 0; _q4 lt $actions.length; _q4++>>
/* Make all standard actions available to the active char. */
<<if $actions[_q4].standard is true>>
<<set $activeCharsActions[_q2].id.push($actions[_q4].id)>>
<<set $activeCharsActions[_q2].probability.push($actions[_q4].probability)>>
<</if>>
/* Make all actions available to the active char that match his char's class. */
<<for _q5 to 0; _q5 lt $actions[_q4].classes.length; _q5++>>
<<if $chars[_q1].class is $actions[_q4].classes[_q5]>>
<<set $activeCharsActions[_q2].id.push($actions[_q4].id)>>
<<set $activeCharsActions[_q2].probability.push($actions[_q4].probability)>>
<</if>>
<</for>>
/* Make all actions available to the active char that match his current location. */
<<for _q6 to 0; _q6 lt $actions[_q4].locations.length; _q6++>>
<<if $chars[_q1].location is $actions[_q4].locations[_q6]>>
<<set $activeCharsActions[_q2].id.push($actions[_q4].id)>>
<<set $activeCharsActions[_q2].probability.push($actions[_q4].probability)>>
<</if>>
<</for>>
<</for>>
/* Randomly select one of the char's available actions as the current action. */
<<set $chars[_q1].currentAction = either($activeCharsActions[_q2].id)>>
/* In case of the "leave" action trigger the "move" widget (see separate passage). */
<<if $chars[_q1].currentAction == "leave">>
<<move $chars[_q1]>>
<</if>>
<</if>>
<</for>>
<</for>>
<</nobr>>