0 votes
by (870 points)

I'm making an RPG with multiple party members. I'm representing enemy and player parties with arrays. This works for most things, but I'm having issues keeping track of the subject and target characters (for attack damage calculation, etc.). I'm currently saving the characters' array indices to "$subject" and "$target" variables, but this requires very cumbersome code where I have to refer to everyone as "$enemies[$target]" etc.; I also have to pass which party array I'm referring to as an argument if I want to use e.g. a damage calculation formula as a widget, which produces a lot of clutter and unclear code.

It would help me a lot if I could just pass the actor objects to "target" and "subject" variables and use those for widgets and such, but this just creates a copy and leaves the original object unchanged. I searched for this topic and found this workaround, but I'm not sure if that will work for me because I need to use these objects in many passages and widgets consecutively. Is there a way to permanently store an object's address, not just a copy?

Examples of what I'd need this for:

<<for _i, _enemy range $enemies>>
<<if $targeting is true>>
<<capture _i>>
[[_enemy.name|confirm phase][$target = _i; $targeting = false]]
<</capture>>
<</if>>
<</for>>

^ this is how I'm currently handling target selection

<<widget "damagecalc">>
/* args0 is subject's party array; args1 is target's party array */
<<set $dmg to 50+$args[0][$subject].atk-$args[1][$target].def>>
<</widget>>

^ this is how I'm currently handling damage calculation

I'm sorry if I'm not giving enough information; I don't want to dump all my code on you, since as I said this current workaround makes it a huge mess. I can show more code if needed.

2 Answers

0 votes
by (63.1k points)

Newer versions of SugarCube now have the State.getVar() and State.setVar() methods meaning that while your have to largely do the same things as described in that post, you can now send the entire variable name around: 

/% assuming $enemy is an object %/
<<set $target to "$enemy">>

/% get the data from the object %/
<<set _target to State.getVar($target)>>
<<set _target.hp -= 15>>

It is the exact same workaround, the only real difference is that the new methods are more readable and clearer in what they're doing at a glance, which is enough for me to recommend them over the other way. 

That said, object references are still broken between passages. Fixing that wrinkle is probably going to be more of a SugarCube v3 thing than something you're likely to see in v2 someday. 

0 votes
by (44.7k points)

I also have to pass which party array I'm referring to as an argument if I want to use e.g. a damage calculation formula as a widget

Maybe I'm missing something, but why not just pass the attack and defense values to the "damagecalc" widget?

Just make the widget be:

<<widget "damagecalc">>
/* args0 is subject's party attack value; args1 is target's defense value */
<<set $dmg = 50 + $args[0] - $args[1]>>
<</widget>>

And call it like:

<<damagecalc $party[$subject].atk $enemies[$target].def>>

Or remove the ".atk" and ".def" from there and put them inside the widget.

But it doesn't matter how you do it, somewhere you're going to have to track the array and also the index you want to refer to in that array.

Is there a way to permanently store an object's address, not just a copy?

Not really.  Not unless you want to both totally break the "back" button and also either break or modify the save and load procedures.  Twine/SugarCube has to keep a record of the history, which means each object is copied, which means references get broken when you go to a new passage.

Getting around that would probably take more effort than simply tracking the indexes would.  (I can think of two ways, neither are very good.)

by (870 points)
edited by

Maybe I'm missing something, but why not just pass the attack and defense values to the "damagecalc" widget?

That would just mean having to type out more stuff. I'm basically doing that already, but I'd like it if I could just call <<damagecalc>> and have it automatically act on a previously defined target and subject. But if it's not possible then I'll just keep doing this. Maybe I can try juggling it through Chapel's method, but that seems easy to mess up.

Do widgets count as separate passages for the purposes of this, or would they still work if I used getVar() in a passage and then referenced the variable in the widget?

Edit: I will say I actually wouldn't mind breaking the Back button, though I would still like to have saved games.

by (44.7k points)
edited by

Do widgets count as separate passages for the purposes of this...?

Widgets are treated as though they're in the same passage, right down to being able to modify the passage's temporary variables.  For example, if you had this widget:

<<widget "ChangeTempVar">>
	<<set _i = 100>>
<</widget>>

and ran this code in your passage:

<<set _i = 5>><<ChangeTempVar>> I = _i

it would display "I = 100".  That works in both directions, so if "ChangeTempVar" looked at the value of "_i" it would see that it was set to 5 before it changed it.

would they still work if I used getVar() in a passage and then referenced the variable in the widget?

Yes, as long as you don't go to another passage after that.

For example, if you do this:

<<set $A = { test: "test" }>>
<<set $B = State.getVar("$A")>>
A & B: <<if $A === $B>>Same reference<<else>>Cloned<</if>>

it will display "A & B: Same reference", meaning that if you modify the value of "$A.test", it will also be modified on "$B.test".  However, if you go to another passage and just run that third line by itself, it will display "A & B: Cloned", meaning that they're now separate variables.  (The "if" can tell the difference in this case because objects are only equal if they have the same reference, even if they otherwise have the same contents.)

I will say I actually wouldn't mind breaking the Back button, though I would still like to have saved games.

When I say "break the Back button" I mean you could get unpredictable results when clicking it.  I wouldn't recommend that route.

If the above doesn't help, show me some code that calls the "damagecalc" widget and I'll see if I can think of a better way to do what you want.

P.S. I spent the last 15 minutes editing this, so if you didn't see this "P.S." before then you should go back and re-read this.

by (870 points)

Okay, this was a huge help. I've discovered that this works:

:: Widgets
<<widget "chain">>
<<for _i, _a range $args>>
<<set $args[_i][0] = State.getVar($args[_i][1])>>
<</for>>
<</widget>>

:: PassageReady
<<chain $target $subject>>

:: a-passage
[[Testing][$target = [$enemies[0],"$enemies[0]"];$subject = [$enemies[1],"$enemies[1]"]]]

By storing the name of the variable as a string and attaching it like this, I can maintain the object reference for any variable I want. Using the test you gave me here, $target and $subject both read as "same reference" when compared to what they should be referencing, no matter how many passages I go through. This is very good.

However, I'm running into a weird issue when I try to pass this as an argument to a widget. I made the command buttons widgets for... some reason I don't remember, and it looks like this:

:: Widgets
<<widget "act">>[[Act|actions][$subject = $args[0]]]<</widget>>

<<widget "rest">>[[Rest|confirm phase][$subject = $args[0]; $action = "rest"; $target = null; $cost = 0]]<</widget>>

<<widget "items">>[[Item|items][$subject = $args[0]]]<</widget>>

:: command phase
<span class="actors">
<<for _i, _puppet range $puppets>>
<div class="commands">
<<if _puppet.dead is true>>
/* If puppet is defeated, display no commands. */
<<else>>
<<act [$puppets[_i],"$puppets["+_i+"]"]>><br />
<<rest [$puppets[_i],"$puppets["+_i+"]"]>><br />
<<items [$puppets[_i],"$puppets["+_i+"]"]>><br />
<</if>>
</div>
<</for>>
</span>

When I try clicking the command buttons for the character at index 0, it gives me this error:

<<chain>>: error within widget contents (Error: <<set>>: bad evaluation: Cannot assign to read only property '0' of string '[$puppets[_i],').

And $subject has indeed been set to "[$puppets[_i]," , the string, for no reason I can discern.

In this case I can just un-widget this functionality and add the links to the command phase directly (which seems to work fine even though I don't change anything else), but this is going to be a problem if I ever need to manually set a target or subject through a widget. Do any of you know what's going on here?

But otherwise, thanks, this actually works:

<<widget "damagecalc">>
<<if $pierce is false>>
 <<set $dmg to 50+(($weight*$subject[0].atk)-$target[0].def)>>
<<elseif $pierce is true>>
 <<set $dmg to 50+($weight*$subject[0].atk)>>
 <<set $pierce to false>>
<</if>>
<</widget>>

<<widget "echodamage">>
<<if $target[0].dead is false>>
$target[0].name takes $dmg damage!<br />
<<set $target[0].hp -= $dmg>>
<</widget>>

Now I won't have to worry about mixing up the target and subject every time I call a damage function.

by (44.7k points)
edited by

The problem is that this:

<<act [$puppets[_i],"$puppets["+_i+"]"]>>

doesn't pass an array object to the "act" widget, like you might think, it passes a string.  To get SugarCube to evaluate it as an array object you need to put it inside backticks/backquotes (the accent mark on the tilde ~ key) like this:

<<act `[$puppets[_i],"$puppets["+_i+"]"]`>>

You don't run into the same problem when passing variables, because the "_" or "$" indicates to SugarCube that you're passing a variable, and not a string.  You can use the backticks if you want to pass the value of a JavaScript function as well:

<<widgetName `parseInt($someVar)`>>

See the "Macro Arguments" section of the SugarCube documentation for details.

Hope that helps! smiley

by (870 points)
Okay, that works! Thanks!
...