Creating a dynamic store with a variable cost array

If you are requesting technical assistance with Twine, please specify:
Twine Version: 2.3.14
Story Format: SugarCube 2.34.1

Hi all !

I’m using the listbox macro to present the user with a list of items to purchase from a merchant. Trying to achieve the following :

  1. Picking an option prints the cost for that specific item
  2. Alongside the cost, the user will be presented with an option to attempt to negotiate the price, which will lead to something like <<set _roll to random (1, 20)>> <<if _roll gt 11>><<set _itemprice = *= 0.8>>
  3. To avoid extensive back-and-forth between passages, would love for that functionality to live within a single passage

Below is what I’ve got so far. I’ve tried using the wiki () part of the JavaScript to bake in the Twine logic for the costs of each item & its selection, but its very clumsy (and most importantly, doesn’t work :D)

I suspect that the answer lies in setting an array for _itemcosts outside of the JS, and finding a way to make it so picking an option within the _items list returns the matching value from the _itemcosts array, but I can’t work that out.

Looking for arms and armor?
<<set _items = ["", "Quarterstaff", "Spear", "Club"]>>
<<listbox "$item">>
	<<optionsfrom _items>>
<</listbox>>

<span id="iteminfo">(Pick an item)</span>

<<script>>
	$(document).one(":passagerender", function (event) {
		$(event.content).find("#listbox-item").on("change", function (event) {
			$("#iteminfo").fadeOut(500);
			setTimeout(function () {
				$("#iteminfo").empty().wiki(<<set $item = _items>>
				<<if $item is "Quarterstaff">><<set $item_price = "2 sp">>
				<<elseif $item is "Spear">><<set $item_price = "1 gp">>
				<<elseif $item is "Club">><<set $item_price = "1 sp">>
				<</if>>
				"That'll be $item_price").fadeIn(500);
			}, 500);
			
		});
	});
<</script>>

EDIT: Tried pasting my code as-is in here but parts were disappearing once posted (am very new at programming of any kind). Stripped out some >'s to make sure everything shows up.
EDIT 2: Fixed thanks to Josh’s help. Cheers Josh

Thank you in advance for your precious help !

For code, you can select it and use the Preformatted text button (looks like </>) to mark it as code so the forum software won’t mess it up.

1 Like

Hmm. Yeah, there may well be a better way to do it (maybe put the code in another passage and just do wiki('<<include "item choice response">>') or something? But this seems to work:

Looking for arms and armor?
<<set _items = ["", "Quarterstaff", "Spear", "Club"]>>
<<set _itemcosts = {"Quarterstaff": "2 sp", "Spear": "1 gp", "Club": "1 sp"}>>
<<listbox "$item">>
	<<optionsfrom _items>>
<</listbox>>

<span id="iteminfo">(Pick an item)</span>

<<script>>
	$(document).one(":passagerender", function (event) {
		console.log($(event.content).find("#listbox-item"))
		$(event.content).find("#listbox-item").on("change", function (event) {
			$("#iteminfo").fadeOut(500);
			setTimeout(function () {
				$("#iteminfo").empty().wiki("<<set $item_price to _itemcosts[$item]>>\"That'll be $item_price\"").fadeIn(500);
			}, 500);
		});
	});
<</script>>
1 Like

That does actually work just fine. Thanks Josh!

Working from your adjusted code as a baseline, I’ve tried working in the “haggle” functionality I mentioned in my initial post. Experimenting with the linkreplace and link macros but getting the following error. Error: <<script>>: bad evaluation: missing ) after argument list

My interpretation is that the code is missing a closing parenthesis but I can’t locate the missing spot. Any input on a better implementation ? Thanks again.

Looking for arms and armor?
<<set _items = ["", "Quarterstaff", "Spear", "Club"]>>
<<set _itemcosts = {"Quarterstaff": "2 sp", "Spear": "1 gp", "Club": "1 sp"}>>
<<listbox "$item">>
	<<optionsfrom _items>>
<</listbox>>

<span id="iteminfo">(Pick an item)</span>

<<script>>
	$(document).one(":passagerender", function (event) {
		console.log($(event.content).find("#listbox-item"))
		$(event.content).find("#listbox-item").on("change", function (event) {
			$("#iteminfo").fadeOut(500);
			setTimeout(function () {
				$("#iteminfo").empty().wiki("<<set $item_price to _itemcosts[$item]>>\"That'll be $item_price\""
				<<link "Attempt to haggle for a better price.">>
				<<set _roll = random(1, 20)>>
				<<if _roll gt 11>>
				"<<set $item_price to _itemcosts[$item] *= 0.8>>\"That'll be $item_price\""
				<<elseif _roll lt 12>>
				"<<set $item_price to _itemcosts[$item]>>\"No, sorry, best I can do is $item_price\.""
				<</if>>
				<</link>>
				).fadeIn(500);
			}, 500);
		});
	});
<</script>>

You could also try using a Map object as the data-source of the Listbox, and use a Generic Object to represent the properties associated with each item.

<<silently>>
	<<set _items to new Map()>>	
	<<run _items.set('', {name: '', cost: 0, price: 0})>>
	<<run _items.set('Quarterstaff', {name: 'Quarterstaff', cost: 5, price: 5})>>
	<<run _items.set('Spear', {name: 'Spear', cost: 10, price: 10})>>
	<<run _items.set('Club', {name: 'Club', cost: 15, price: 15})>>
<</silently>>

Looking for arms and armor?
<<listbox "$item">>
	<<optionsfrom _items>>
<</listbox>>

<<link "Negotiate Price">>
	<<if $item.price is $item.cost>>
		<<if random(1, 20) gt 11>>
			<<set $item.price to $item.cost * 0.8>>
		<<else>>
			/* Some message about the negotiations failing */
		<</if>>
	<<else>>
		/* Some message about the price having already been negotiated */
	<</if>>
<</link>>

Thanks @Greyelf. Map + Generic Object seems interesting, unfortunately I haven’t been able to integrate the latter half of your suggestion to the existing code. Is the <<link>> portion meant to be integrated into the wiki() portion of the <<script>>, or on its own after the <<script>>? I’ve tried both, and below is the only approach that doesn’t yield an error, but also doesn’t quite work haha. Think we’re pretty close.

Cheers

<<silently>>
	<<set _items to new Map()>>	
	<<run _items.set('', {name: '', cost: 0, price: 0})>>
	<<run _items.set('Quarterstaff', {name: 'Quarterstaff', cost: '2sp', price: '2sp'})>>
	<<run _items.set('Spear', {name: 'Spear', cost: '1gp', price: '1gp'})>>
	<<run _items.set('Club', {name: 'Club', cost: '1sp', price: '1sp'})>>
<</silently>>

Looking for arms and armor?
<<listbox "$item">>
	<<optionsfrom _items>>
<</listbox>>\

<span id="iteminfo">(Pick an item)</span>\

<<script>>
	$(document).one(":passagerender", function (event) {
		console.log($(event.content).find("#listbox-item"))
		$(event.content).find("#listbox-item").on("change", function (event) {
			$("#iteminfo").fadeOut(500);
			setTimeout(function () {
				$("#iteminfo").empty().wiki("<<set $item.price to $item.cost[_items]>>\"That'll be $item.price\"").fadeIn(500);
			}, 500);
		});
	});
<</script>>

<<link "Negotiate Price">>
	<<if $item.price is $item.cost>>
		<<if random(1, 20) gt 11>>
			"<<set $item.price to $item.cost * 0.8>> 
			"Fine. You can grab it for $item.price"" 
		<<else>>
			/* "No chance. Best I can do is $item.cost" */
		<</if>>
	<<else>>
		/* "Already gave you my best price. $item.cost. Take it or leave it" */
	<</if>>
<</link>>

You’ll need to either remove the console.log($(event.content).find("#listbox-item")) line or add a semicolon at the end. All it does it print something out to the console window, so you probably don’t need it except for testing purposes to make sure that it’s finding the listbox element.

Also, your code refers to $item.cost[_items], but that doesn’t make any sense, because _items is a map and $item.cost is a string. Also, the price and cost already start out the same, so there’s no need to set it again. I’m pretty sure you just want this instead:

$("#iteminfo").empty().wiki('"That'll be $item.price."').fadeIn(500);

Also, because the price/cost will be a string like “1gp”, this means that you can’t do math on it, thus <<set $item.price to $item.cost * 0.8>> won’t work.

Rather than trying to do math on it, I’d just add a “discount” property and use that. So the data may be like this:

<<run _items.set('Spear', { name: 'Spear', cost: '1gp', discount: '8sp', price: '1gp' })>>

and then you’d replace this:

"<<set $item.price to $item.cost * 0.8>>

with this:

<<set $item.price to $item.discount>>

(That also gets rid of the stray double-quote mark.)

Hope that helps! :slight_smile:

1 Like

Thank you HiEv, super useful.

Code was turning into a mess as I was haphazardly combining stuff I found online with folk’s feedback here. As it stands, after integrating your suggestions, the last missing part is the <<link>> macro for Negotiation that comes after the JS bit. As it stands, the passage doesn’t return an error but clicking the link doesn’t show anything. I’m not really sure how to structure the IF statement’s syntax and its outputs within the macro, and I suspect that’s the issue. Here’s what I’ve got right now. Played around with stripping off the quotes around the text segments, but no difference to the result.

<<link "Negotiate Price">>
	<<if $item.price is $item.cost>>
		<<set _persuasion to random(1, 20)>>
		<<if _persuasion gt 11>>
		<<set $item.price to $item.discount>>
			"You roll _persuasion !"
			"Fine. You can grab it for $item.price"
		<<else>>
			"You roll _persuasion !"
			"No chance. Best I can do is $item.cost"
		<</if>>
	<<else>>
		 "Already gave you my best price. $item.price, take it or leave it"
	<</if>>
<</link>>

Again, really appreciate your help :slight_smile:

My fault. I totally missed that you were trying to print text within a <<link>> macro. That won’t work, since you can only run code or go to another passage with a <<link>> macro, any text within that macro will simply be ignored.

For what you want, you’ll need to use the <<linkreplace>> macro instead. That macro replaces the link text with whatever text is inside the macro once clicked on. You literally just have to change <<link... <</link>> to <<linkreplace... <</linkreplace>> and then your code should work as-is. (Well, almost as-is. You may need to add a \ to the end of any lines that you don’t want to add a blank line to the page. See the documentation on it here.)

Hopefully that does the trick! :slight_smile:

1 Like

Ahhh you’re a legend. Yeah that works just fine. Funny thing is I had looked at linkreplace first but couldn’t decipher the documentation well enough to tell it was the better option.

Now that I got far enough to find out; noticing another issue. Triggering the linkreplace macro prints the result exactly as intended, but choosing another option in the listbox doesn’t reset the Linkreplace and allow the user to attempt negotiating for a different item. Need to figure out some way to “loop” the linkreplace, basically.

Does that potentially involve storing the linkreplace into the wiki() part of the JS so that it resets as part of the on("change", function (event) { action ?

That’s actually one of the first things I had tried doing initially, but clumsily.

Thanks again!!

If that’s what you want to do, then you might want to put the whole <<linkreplace>> code into it’s own passage, and then use a combination of the <<replace>> and <<include>> macros to re-run the <<linkreplace>> code as needed.

Basically, put all of the <<linkreplace>> code into a “Haggle” passage, then put:

<span id="haggle"></span>

in the passage where you want the <<linkreplace>> code to appear, and then call:

<<replace "#haggle">><<include "Haggle">><</replace>>

when you want it to appear there.

(Note: The “#” in front of “#haggle” tells it that you’re looking for the element with an ID of “haggle” (i.e. the <span id="haggle"></span> part). In CSS a “#” in front of a name refers to a CSS ID, which should be unique on the page, while a “.” in front of a name refers to a CSS class, which can be used by multiple HTML elements on a page.)

You might also want to add a “haggled” property to each item in the _items object after the item is haggled for in the <<linkreplace>> code, that way you can make it so that people can’t just re-toggle the item to let them keep haggling until they roll high enough. So, instead of <<if $item.price is $item.cost>>, do something like this:

<<if ndef $item.haggled>>
	... haggling code goes here ...
	<<set $item.haggled = true>>
	<<run _items.set($item.name, $item)>>
<<else>>
	"Already gave you my best price. $item.price, take it or leave it."
<</if>>

That way it will only allow haggling if the “haggled” property hasn’t been defined yet (ndef = not defined), and adds the “haggled” property after haggling.

Enjoy! :slight_smile: