Relationships between current party members affecting stats

I want this game to lean heavily on relationships between PCs and NPCs, but also NPCs with each other. I currently am using a system where NPCs can be added to the party in one of three ‘ally’ slots. (Preferably they’ll decline to join if their relationship with the PC is too weak, but that’s for another day).

What I want to do is:
If Ally2 is part of Ally1’s ‘Like’ list, increase all Ally1’s stats by +5. In reverse, if they’re in the ‘Dislike’ list, decrease all Ally1’s stats by -5.
The issue I’m having is how to have a repeatable subscript that knows which ‘Dis/Like’ list to draw from without me having to ask it to search a specific character’s list. Currently, the ‘Dis/Like’ lists are written as a variable called ‘XLikes’ (where X is the character’s name) and then a string of names. Each character has their own ‘XLikes’ variable, and who’s on there will change over time. I planned on using the ‘contains’ command from CSLIB to retrieve whether or not Ally2 is in Ally1’s ‘Likes’ variable. Increasing all stats for a specific ally has the same issue of not being able to identify X’s stat variable without directly asking for Xstat.

There wouldn’t be much point in showing you the code I’ve got, as it hasn’t gotten off the floor yet due to my inability to comprehend how to get around the problem (so sorry, I’m a complete beginner with overly complicated dreams).

*gosub_scene friends Like "MikeLikes" "Sofia"
*comment except I need 'Mike' to be {ally1_slot}Likes and 'Sofia' to be {ally2_slot}

[The scene below is called friends]

*label Like
*params likes ally_slot2
*gosub_scene cslib_string contains likes {ally_slot2}
*if cslib_ret
    *comment YAY (+5 to all ally1 stats)
    *goto Dislike
    *comment Dislike would be the same as Like except for the XDislike list

In short, what I want to resolve:

How to search/change a specific variable attributed to an NPC without writing out the name of the variable


Edit: References were not an ideal solution as per my first answer, but you can make something work with bidimensional arrays (matrices).

Consider storing your allies and their attributes in an mxn matrix, where m is the number of allies and n is their attributes.

Strength Dexterity Intelligence Charisma
Tim 5 10 5 5
Jim 1 15 10 6
Kim 15 10 4 12

In CS, this table would look something like this

*create_array chars_1 4 10 5 5 5
*create_array chars_2 4 1 15 10 6
*create_array chars_3 4 15 10 4 12

In your subroutine that sets the buffs/ debuffs, you merely pass the index of the character you want to edit the attributes (but don’t forget to remove the buffs/debuffs from the previous characters if there are any - a variable with the index of the character with buffs/debuffs would appropriate here I think)

*comment buff is either -1(debuff) or +1 (buff)
*label update_attributes
*params character_index, buff

*comment insert code here to remove old buffs/debuffs
*char[character_index][1] += buff * 5
*char[character_index][2] += buff * 5
*char[character_index][3] += buff * 5
*comment insert code here to remember who has the current buffs_debuffs

If you want to pass the name of the variable you can use string concatenation:

*gosub_scene friends Like (ally1_slot & "Likes") "Sofia"

Or string interpolation:

*gosub_scene friends Like "${ally1_slot}Likes" "Sofia"

Either way, you’d need to wrap the parameter “likes” in brackets to make a reference call inside the subroutine.

*gosub_scene cslib_string contains {likes} {ally_slot2}

That said, if I were you, I’d avoid simulating arrays. This can easily get out of hand even for someone with more experience. This may not be the most elegant way, but I’d have a variable for each NPC, for each other NPC. Keep in mind the combinatorial explosion. For example:

*comment startup.txt

*create mike_likes_sofia true
*create mike_likes_helen false

Notice that I use snake case instead of camel case like in your example. This allows you to use “array notation” later.

*if ({ally_slot_1}["likes"][ally_slot_2])
    *set {ally_slot_1}["strength"] =+ 5
    *comment apply buff to the other attributes

I wrote the code from the top of my head, there may be a syntax error somewhere, but the general gist is the same.

Thank you to both of you, that’s far out of my understanding range :slight_smile: so bare with me if I ask obvious questions.

To quartz, I like the idea of an array for the buffs/debuffs. What would ‘character_index’ be? chars_1/2/3? Also, what do you mean by the final comment?

To cup_half_empty, the apparent simplicity of string concatenation/interpolation makes my eyes very happy! Is it as simple as that to find XLikes… It seems a lot easier and cleaner than the second option. (I sadly can’t keep in mind combinatorial explosion as I have no idea what that is - idem for the animal cases). For the array notation section, how does that work? Particularly, how does it identify the ‘strength’ string?
Edit: I tried merging the two together (probably blasphemous). The result is:

*gosub_scene friends Like "${Ally1}Likes" "${Ally2}"

[friends scene]

*label Like
*params likes Ally2
*gosub_scene cslib_string contains {likes} {Ally2}
*if cslib_ret
    *set ${Ally1}["Investig"] =+ 5

It doesn’t come up with an error, but it goes straight to *else, even though Ally2 is in Ally1’s Like list. Am I doing the *gosub wrong?

Thanks again guys for being so helpful!

1 Like

Can you print the contents of likes and Ally2 in the subroutine?

If you have 5 NPCs, then you will have a combination of 5 by 4 (5 x 4 = 20) variables (mike_likes_sofia, mike_likes_helen, mike_likes_john, mike_likes_peter, sofia_likes_mike, sofia_likes_helen, etc). If you add another NPC in the future, the number of variables increases to 6 x 5 (30).

Array notation is just some alternative syntax. Under the hood, ChoiceScript will convert all array notation to variable names. So the variables must exist in your code. For example, mike_likes can be written as mike["likes"]. Note that ChoiceScript adds an underscore to the name, that’s why I said snake case is better than camel case for ChoiceScript. Note that ChoiceScript doesn’t have true arrays. You can read more about arrays in the wikia.

In my example, suppose the value of ally_slot_1 is mike, then {ally_slot_1}["strength"] is equivalent to writing mike_strength.

1 Like

Right, thank you cup_half_empty, that actually makes a lot of sense!
Good idea with the subroutine, I ran it and it is returning the right variables (Mike, MikeLikes and Sofia). However, for whatever reason the CSLIB contains is not working. (I have checked, I’ve got CSLIB files in my project scenes folder). There’s no error, but it still just goes to *else even when the condition for *if should be met.
Current code:

*gosub_scene friends Like "${Ally1}Likes" "${Ally2}"

*label Like
*params likes Ally2
*gosub_scene cslib_string contains {likes} {Ally2}
*if cslib_ret
    ${Ally1} ${likes} ${Ally2}
    *set ${Ally1}["Investig"] =+ 5
    ${Ally1} ${likes} ${Ally2}

Thanks for explaining arrays. Given what you’ve demonstrated, why wouldn’t you use the “${Ally1}Likes” system rather than the mike_likes_sofia method?
Also, interesting to hear the background of what ChoiceScript is doing. Looking up the camel/snake cases, it says camel is with no spaces and capitals - do you know whether ChoiceScript is Caps sensitive? Otherwise yes I’ll probably go change everything to underscores.
I tried running

*set ${Ally1}["Investig"] =+ 5

On it’s own and it comes back with the error: “Invalid expression: couldn’t extract another token” even if I add ${Ally1} to the params/gosub.

Edit: So I’ve changed the stats to being all XStat1/2/3 (will put a _ if becomes needed for the code to work) because they’re not all the same stat type (eg. Mike might have Investigation but Sofia might not).

Thank you for linking the wikia arrays. I did read it. I understood most of it but not sure how to apply it. I suppose like you’re both indicating, a proper array is needed to group the stats. CSLIB does have a *set_all. I tried concatenation but it’s strings only. Also, as I’ve just discovered, I have an older version of CS so will update and get back to you on the subject of arrays :slight_smile:
Edit: Not going to happen (software version issues). So failing that, how do I make an array without using *create_array?

Sorry, I just tested and you can’t use array notation like this. Also, my bad too, but you don’t use = in an assignment (in the set command). This works, though:

*set {Ally1 & "Investig"} +5

For the most part, ChoiceScript is case-insensitive (with a few odd exceptions). In other words, creating a variable called ally and another Ally and other aLLy would all be the same for ChoiceScript. The only benefit of using snake case is because of the array notation. But like I showed above, you can easily replace array notation for string concatenation.

*create_array is syntax sugar. Behind the curtains, ChoiceScript will create individual variables for each array slot. That’s why I said it doesn’t have true arrays. This is just a convenience command. If you want to create an array with 5 slots, just create each variable individually.

*create array_1 ""
*create array_2 ""
*create array_3 ""
*create array_4 ""
*create array_5 ""
1 Like

Ah thank you that works! Good to know about caps. I’ve decided that considering there’s just three stats atm it’ll be easier to do them 1 by 1 than use an array in this case. Unless there’s a simpler way of making the LikeA1 code repeatable rather than doing Ally1xAlly2, Ally2xAlly1, Ally1xAlly3 and so on, using arrays? For now this works though.
I’m afraid I’ve got three last questions!

1 - Why is it that whenever I use parameters the first one (and likely other ones if it got to it) is not recognised/‘passed’ when the parameters were set out in a previously-parametered subscript. Eg. in my code:

*gosub_scene friends LikeA1 "${Ally1}Likes" "${Ally2}" "${Ally1}Dislikes" "${Ally1}Mid"


*label LikeA1
*params likes Ally2 dislikes mid
*gosub_scene cslib_string contains {likes} {Ally2}
*if cslib_ret
    *set {Ally1 & "Stat1"} +5
    *set {Ally1 & "Stat2"} +5
    *set {Ally1 & "Stat3"} +5
    ${Ally1} and ${Ally2} seem to get on well.
    *gosub_scene cslib_string contains {dislikes} {Ally2}
    *if cslib_ret
      *set {Ally1 & "Stat1"} -5
      *set {Ally1 & "Stat2"} -5
      *set {Ally1 & "Stat3"} -5
      ${Ally1} and ${Ally2} seem to have it out for each other.
        *gosub_scene cslib_string contains {mid} {Ally2}
        *if cslib_ret
            ${Ally1} and ${Ally2} don't seem to care about each other.
*goto LikeA2 "${Ally2}Likes" "${Ally1}" "${Ally2}Dislikes" "${Ally2}Mid"

*label LikeA2
*params likes Ally1 dislikes mid
*gosub_scene cslib_string contains {likes} {Ally1}
*if cslib_ret
    *set {Ally2 & "Stat1"} +5

The *goto LikeA2 “${Ally2}Likes” “${Ally1}” “${Ally2}Dislikes” “${Ally2}Mid” sets the parameters but ‘likes’ in *params likes Ally1 dislikes mid does not seem to see it.
Edit: I’ve solved this! Apparently, *params only works with *gosub_scene commands, so changed all the *goto LikeA2 etc. to *gosub_scene friends LikeA2

2 - Why does CSLIB’s contains command not seem to return what is being searched (even when it should), and instead always goes to *else? What am I doing wrong?

3 - I understand that I can add strings to pre-existing string variables via &“”, but how do you remove a string from within a variable?? It seems a little odd to have addition without substraction (and I’ve tried -, that’s only for values apparently). From the code:

*gosub_scene friends Movezone "MikeLikes" "Sofia" "MikeDislikes"


*label Movezone
*params zone name prev
*set {zone} &", {name}"
*set {prev} -", {name}"

Me with a toddlers understanding of code.

I am curious why is this a thing in your game? Is it like a magical thing that can’t be controlled? Because I don’t know about you, but if I’m fighting a monster that wants to kill me, I don’t care if I’m fighting besides somebody I don’t like I will fight with all that I have!


Good point, but if you were asked by someone working with your worst enemy to go on a mission that could possibly involve fighting a monster that wants to kill you - would you say yes and go all in?

In theory, if you (player) get on well enough with both A and B, even if A hates B they’ll still come along and use their skills. In the middle of a fight, it’s true you might fight with all you have. But if half way through B asks for your help to go around the back side of the monster to attack it there, away from the rest of the party - would you give it your all then, or trust them? Maybe you decide to summon up a small hurricane to unbalance the monster, just as B knocks a bunch of rocks to crash down on the monster thus breaking up the subtleties of your magic because you know each other poorly and don’t coordinate as well.

Or if you were in a situation that’s less dramatic and immediate than fighting to the death, like instead trying to get out of a system of caverns as they fill with lava, or negotiate with a trigger-happy group of adventurers, maybe you wouldn’t care too much if B falls behind a little or takes an arrow to the leg (why risk yourself and your skills for them!).
If you really tone it down to stressful-but-not-immediately-dangerous activities, like creating a costume to sneak past guards, or trying to decipher ancient writing on a temple wall, having someone you don’t like saying snarky things or generally not clicking with your personality would definitely break up the flow of the work! (For me anyway).

You are right, in some ways you’ll work together if you have to. Though I do feel like there’s a certain something extra when you’re with a team of people you really trust and like; you get more motivation and respond more instinctively to support them. The ‘MoveZone’ script is also designed to allow characters to change their feelings towards other characters if they overcome something together.

In truth, I wanted relationships to be more dynamic, with the player having to consider who will work best together as well as what skills they might need and how well the player themselves gets on with the NPC. I’ve always felt like simply being able to have X work for you, always, doing exactly what you say when you say it, makes the characters seem a little too much like mindless servants.

Thanks for making me think haha!

You also have a Point but tell me what’s really stopping me from giving it my all in that fight and then turning around and fighting the person I don’t like after the whole battle is done? Maybe if I was a magic user or if I have to conserve my spirit Energy or something i’m not saying what you’re doing is wrong I was just curious why that would happen in the world maybe everyone’s stats tied to emotion in this world you are creating good luck with your project and thank you for interesting conversation

Hi Phantomblade - no worries I knew you weren’t criticising, I enjoyed the theoretical questions too :slight_smile:
You could always give the emotion-stats a go in your piece, it sounds like a fun idea!
You could do that^ indeed, but constantly changing people’s stats to reflect whether they’d have the time/will to have issues with the other NPC would probably become a little confusing. So it’s more of a blanket statement for most situations (where you probably aren’t in extreme danger).
For your good questions Phantom, I’ve added two ‘TempAlly’ slots for the occasional time when someone has to work with you/your party - TempAlly slots won’t affect stats and will be irrespective of how good your relationship with that character is.

1 Like

So I solved question 1 (why params wasn’t working). The new issue is, because all of these are now nested gosub_scenes (for params to work), I can’t do *return without it going back to the last *gosub_scene’d label (eg. *Label LikeA5, in my case) which then causes an error because in going backwards the parameters for LikeA5 are no longer defined. Aaaaaah
Anyone know any way to go straight back to the scene the player was on (outside of the ‘friends’ sub_scene)? Maybe if it were possible to make a *label a variable that could be increased by 1 each time so it is a unique identifier of the ‘page’ the *gosub came from?
Edit: Ok solved the new issue. (Writing out the solutions in case others have this issue - note, *label cannot have variables it would seem). I’ve added

*label LikeA6
*if back = true
    *params likes Ally3 dislikes mid
    *gosub_scene cslib_string contains {likes} {Ally3}
    *if cslib_ret
        *set {Ally2 & "Stat1"} +5

At the start of each LikeA[num] section, and made ‘back’ true at the end of A6 (and then make it false again once we get back to the scene). That way, when returning to the previous *gosub, it doesn’t trigger the *params and cause an error, and I can keep returning one-by-one until it eventually returns back to the scene.

Edit2: Solved question 2. It was a matter of syntax. It needs to be written like:

*gosub_scene cslib_string contains {likes} "${Ally1}"

And I was missing the quotation marks and $.
Now it’s just question 3 (substracting a string from a variable) that causes issue. Oh and figuring out why the cslib is returning for the wrong variables (eg. saying MikeLikes contains Sofia when it doesn’t etc). Perhaps it’s random but it seems to go wrong in pairs (specifically A2 [ally2 x ally1] and A4 [ally3 x ally2], or A3 [ally3 x ally1] and A5 [ally1 x ally3]). Interestingly, making all ‘XLikes’ 'XDislikes ‘XMid’ variables empty then all A[num] subscripts run fine (go to the right *else).

Ally3 seems to always put Ally1 and Ally2 in the same category (Like/Dislike/Mid based on whichever is filled first) even if one or the other are not present in any category (eg. if Ally2 is liked by Ally3 then Ally1 is liked. If Ally1 is mid and Ally2 is disliked then will return as Ally1 and Ally2 are disliked because my nested *if order is Like/Dislike/Mid).

Ally1 works fine with all variations. (A1, A5)

Ally2 has the same issues as Ally3, or similar.

Ok update (this is more for myself) - It’s not about Allies but about who the Ally is - eg. Mike works but Sofia and Rick don’t, regardless of if they’re Ally1/2/3.
It’s not the name length (Mike v Sofia/Rick), it’s not the order in [startup], it’s not the order in [friends]. Restarting changes nothing. Why would one variable (Mike) work but not others??

I don’t think that’s the issue. The parameters should still be defined. But if in the other subroutines, you define parameters with the same name they will overwrite each other, so when you go back, the parameter has the value of one of the subroutines instead of their original value. All subroutines exist in the same scope is what I’m trying to say.

That’s right. Every time you call a subroutine you put it on a stack (a pile), to get back to the original line before calling the first subroutine you have to remove each plate from the stack one by one.

That’s not possible. Strings are not like numbers. To do what you want you’d have to perform heavy string manipulation, like copying and pasting. That’s another reason why I said it would be best to use variables instead of a long string to emulate an array. One variable to each companion like this instead of a string:

Cslib have some subroutine that can help you manipulate string, but again, this is not something I recommend. It’s convoluted and hard even for more advanced programmers. If you’re still set on doing it like that, I’d suggest taking a look at @Twiger_Fluff’s lib for emulating arrays with string (which you’re already trying to do).

1 Like

Riiiight ok I see now why you said that. I’ll have a look at Twiger_Fluff’s code, but if that doesn’t work I will have to do it your way after all :slight_smile:
Edit: ReplaceAll sounds like it’d work. I’ll give it a shot.

Thanks for the help cup_half_empty!

Ok this seems to work. So cup thanks for solving my third question on how to ‘substract’ a string from a variable :slight_smile:

*gosub_scene friends Movezone "MikeLikes" "Sofia" "MikeMid"  "Mike"


*label Movezone
*params zone name prev accepter
*set {zone} &", ${name}"
${accepter}'s feelings towards ${name} seem to have changed.
*gosub_scene twiger_functions replaceFirst {prev} "${name}" " " false "${prev}"

Now I just need to figure out why Mike works in the ally x ally script when the others don’t.

1 Like

This topic was automatically closed 24 hours after the last reply. If you want to reopen your WiP, contact the moderators.