ID:2092199
 
(See the best response by Ter13.)
Code:
mob/verb/ViewAvailableFoods in typesof(/obj/Food/)  //This verb is intended to populate a list of all Food objs available to buy
set hidden = 1 //Makes this invis since there's an interface button for it
switch(input("Select a category") in list ("Fruits","Veggies","Meat","Beverages","Cancel")) //The options
if("Fruits")
switch(input("Select a Friut") in list (src.name in typesof(/obj/Food/Fruits),"Cancel"))
if("Cancel") return //How do I make this Cancel option go back to the previous question, "Select a Category"?
if(typesof(/obj/Food/Fruit)) //Line 213. Am I not allowed to put a typesof() proc inside an if() proc? If the player selected one of the coded fruit objs in the list...
switch(input("[src.desc","[src.name]" in list ("Buy","Cancel")))
if("Cancel") return //Again, how do I make this Cancel option go to previous question, "Select a Fruit"?
if("Buy")
if(usr.Money < src.Cost) //Line 217. I thought the src here is the /obj/Food/Fruit the user selects
usr << output("You need don't have enough for this!")
return
else
usr.Money-=src.Cost //Line 221
//Going to enter some code here to place a new /obj/Food/Fruit/[fruit name here obviously]
usr << output("You have purchased [src]!")
return


Problem description:
loading Alchemy Online.dme
loading interface.dmf
Food System.dm:217:error: src.Cost: undefined var
Food System.dm:221:error: src.Cost: undefined var
Food System.dm:213:error: : expected a constant expression
Alchemy Online.dmb - 3 errors, 0 warnings (5/31/16 10:11 am)

1. What I aimed to do is allow players to view a categorized list of food objs. Each food obj has a description, name, and cost var. Cost is a var defined under obj/Food, and each specific fruit, meat, beverage, etc obj has their own cost/number assigned to their Cost var, so maybe that undefined var error has to do with 'src'?

2. How do I get the Cancel options to go back to the previous switch option? I was thinking to make each step a proc and calling the previous one, but I feel like there's a better option.

3. My typesof() proc doesn't work as an argument inside an if() proc. I don't know if that's illegal.. The DM guide doesn't say anything about it.

if() statements are different when part of a switch() statement.

switch cases only take constants.
In response to Ter13
Ter13 wrote:
if() statements are different when part of a switch() statement.

switch cases only take constants.

Okay that explains my number 3 problem. But damnit. I really don't type out every single obj under obj/Food for this switch proc and if statements for each of them as well. Is there a more efficient way to accomplish this task?
Best response
To be honest, I don't think what you are doing makes a lot of sense in the first place, but I can probably restructure it to something that actually works. I just want to point out that this is a really bad way to present the user with information. But at the very least, I can tell you where you are going wrong.

1) types are not objects. src will not be the value returned by an input(). src will always be the object that the proc belongs to. It will never change. You can read data from a type using a special form of initial() and typecasting.

2) The cancel option will require a state machine structure. You can do this a large number of ways, goto, while loops, or with stack deepening, or a combination of the three. I'll cover a simple method of implementing it here.

3) As we already covered, switch cases take constants. in list is not a compile-time constant, but a runtime-determined statement. You don't even need the switch statement here at all. You have filtered the input in such a way that the player can't pick anything that's not in the type list except your back option, so there's no reason to invoke it. In fact, we don't even need the back option. I'll show you how to get around that.

Let's get started cleaning up:

var
food_manager/food_manager = new() //set this up before the map initializes.

food_manager
var
list/categories = list()
New() //pregenerate the category lists. We don't need to regenerate them all the time.
var/list/l = typesof(/obj/Food)
var/obj/Food/f, cat, name, cost
for(var/v in l)
f = v
cat = initial(f.category) //get the type's initial category.
if(cat) //if it has a category
name = initial(f.name)
cost = initial(f.cost)
if(name&&cost) //and a name and a cost
if(!categories[cat]) //if the category doesn't exist, create a list for it.
categories[cat] = list()
categories[cat]["[name] ([cost]g)"] = f //associate the name and cost to the type

//set up your categories for your food items:
obj
Food
var
category = null
cost = 0 //set name or cost to 0 or null for anything that shouldn't show up in the categories list
Fruit
category = "Fruit"
Vegetable
category = "Veggies"
Meat
category = "Meat"
Beverage
category = "Beverage"


That's the basic structure for constructing a series of categories in a multi-dimensional, associative list based on categories that are easy to set up.

Let's move on to actually displaying the list and dealing with it properly:

//this is an example. Do not use this code. Just look at the logic flow.

var/category = input("What kind of food are you looking for?") as null|anything in food_manager.categories
if(category)
var/selection = input("Sure, here's what we have") as null|anything in food_manager.categories[category]
if(selection)
var/obj/Food/f = food_manager.categories[category][selection]
var/cost = initial(f.cost)
if(cost<=usr.money)
usr.money -= cost
usr << "You buy \a [initial(f.name)]."
else
usr << "You don't have enough money to buy \a [initial(f.name)]."


Alright, that's our basic structure. Notice I got rid of the "Back" option? That's because you can allow the input box to show a "cancel" button by allowing the type of the input to be null.

Let's create the state machine using this core functionality:

//plug the following into the verb that starts the shopping business.

var/category, selection, obj/Food/f, cost //set up our down-state variables ahead of time.
do //start looping behavior for category
category = input("What kind of food are you looking for?") as null|anything in food_manager.categories //select from the categories set up in the food manager.
if(category) //if the user didn't click cancel.
do //start looping behavior for item selection
selection = input("Sure, here's what we have:") as null|anything in food_manager.categories[category] //select the category subitem
if(selection) //if the user didn't click cancel
f = food_manager.categories[category][selection] //get the type from the associative list
cost = initial(f.cost) //query the type for its cost
if(cost<=usr.money) //make sure the user has enough money
usr.money -= cost
usr << "You buy \a [initial(f.name)]."
else
usr << "You don't have enough money to buy \a [initial(f.name)]."
while(selection!=null) //repeat until the user clicks cancel.
while(category!=null) //repeat until the user clicks cancel.


And there's your basic state machine using do..while loops.

I'd really recommend looking back at the last bit of advice I gave you about polymorphism in the last thread. It's looking like you are making your work a lot harder than it needs to be by not thinking through hierarchical structures and instead preferring to think of the type tree like a database.
Damn what a doozy. I appreciate you typing all that out. I stored all this in my .dm forum file for future reference and applied what you have to my game's code. I understand the hierarchical structures concept; I just don't know the syntax for it to work properly enough to write it on my own. I just realized I never read the Associative Lists chapter of the DM Guide so I'll give that a read to gain more understanding.
Hey Ter13, what was the purpose of using initial() to get the initial category if all the food items have already been categorized? The initial category under /obj/Food is null, so it looks like to me that null will be used as the category throughout the code. I'm probably wrong though.
Hey Ter13, what was the purpose of using initial() to get the initial category if all the food items have already been categorized?

What that bit of code does is loop through all types in the /obj/Food tree and build a list of types according to their categories, excluding any items without names or costs.

This saves us from having to use typesof() later and build a list of name-value pairs when we run the verb. Since in your example, the data is always the same, we can build the list of data at runtime once rather than every time we call the verb.

initial() can be used to get information about the initial state of an object. If you pass in a variable that hasn't been initialized, but has been typecast, you can get information about the type the variable has been cast to.

That's why we're using initial() to store information about the type tree in a list structure.

The initial category under /obj/Food is null, so it looks like to me that null will be used as the category throughout the code. I'm probably wrong though.

typesof() returns a list of that type and all types that inherit from that type. Just because the category under /obj/Food is null doesn't mean that all children will have the same category as the parent. Hence the subcategories we define polymorphically inheriting, but overriding their parents' values, which further propagate to each frond of the tree.
Ah alright I gotcha. I had the thought that initial() would just grab null and disregard the categories specified farther down the tree of objs. With the insight you provided I attempted to remedy my problem on my own, but couldn't. I made some word replacement changes and a couple other slight changes due to a bad index runtime error at the this line.
                    if(!categories[cat]) //if the category doesn't exist, create a list for it.


Problem: I get no runtime or compile errors, but when I press the verb to run the last chunk, nothing happens. Since there's no errors anywhere, I'm lost as to where the problem in the code resides.

Code:
var/perk_manager/perk_manager = new()
perk_manager
var
list/categories
proc/MakePerkList() //Changed New() to proc/MakePerkList because I was getting a bad index runtime error at the if(!categories[cat]) line below
var/list/l = typesof(/obj/Perk) //All the perks are in the list l.
var/obj/Perk/p, cat, name, cost
for(var/v in l) //For all the vars of the perk objs that are in list l
p = v //All the vars of the perk objs are assigned to p
cat = initial(p.category) //get the type's initial category.
if(cat) //if it has a category
name = initial(p.name)
cost = initial(p.cost)
if(name&&cost) //and a name and a cost
if(!categories[cat]) //if the category doesn't exist, create a list for it. This is where I got the bad index runtime error
categories[cat] = list()
categories[cat]["[name] ([cost]g)"] = p //associate the name and cost to the type

mob/verb/ViewAvailablePerks()
var/category, selection, obj/Perk/p, cost //set up our down-state variables ahead of time.
do //start looping behavior for category
category = input("What kind of perk are you looking for?") as null|anything in perk_manager.categories //select from the categories set up in the perk manager.
if(category) //if the user didn't click cancel.
do //start looping behavior for item selection
selection = input("Sure, here's what we have:") as null|anything in perk_manager.categories[category] //select the category subitem
if(selection) //if the user didn't click cancel
p = perk_manager.categories[category][selection] //get the type from the associative list
cost = initial(p.cost) //query the type for its cost
if(cost<=usr.Points) //make sure the user has enough money
usr.Points -= cost
usr << "You've learned [initial(p.name)]!"
else
usr << "You don't have enough points to learn [initial(p.name)]."
while(selection!=null) //repeat until the user clicks cancel.
while(category!=null) //repeat until the user clicks cancel.

//Here's a top portion of my "food" (perk) categories
obj/Perk/
var
category
cost = null
Profession
cost = 3
category = "Profession"

General_Blacksmith
name = "General Blacksmith"
desc = "This person has entered an apprenticeship and has learned basic knowledge of different metals, structures, and ideas people lacking this skill wouldn’t have. They can also repair and create a number of basic items, such as house keys, bows and arrows, and typewriters."
cost = 7
icon = 'Perks.dmi'
icon_state = "Blackmsith"
Adept_General_Blacksmith
name = "Adept General Blacksmith"
desc = "This blacksmith has took the time in strengthening their fundamental learnings during their tutelage. This blacksmith can apply strengthened metal alloys to all their creations, lengthening their durability to take less wear and require less repairs."
Professional_Gunsmith
name = "Professional Gunsmith"
desc = "This blacksmith has concentrated knowledge in the mechanics and workings of firearms. They are able to create and repair pistols, grenades, and automatic weapons."


EDIT: Here's the runtime error from changing that New() proc

runtime error: bad index
proc name: New (/perk_manager/New)
source file: Perk System.dm,213
usr: null
src: /perk_manager (/perk_manager)
call stack:
/perk_manager (/perk_manager): New()
world: ()
connected


I previously changed everything to relate to food so I could understand your responses more easily.
I made a really small mistake:

these lines:

food_manager
var
list/categories


should be:

food_manager
var
list/categories = list()


That runtime error is attempting to index a null value, which is invalid. I just forgot to initialize the list. My bad.
Lol damnit. I looked at that and raised an eyebrow right after I read the DM reference on lists and list associations. I didn't question it though. But thanks a lot man, it works perfectly. I can finally finish this part.
In response to Ter13
Ter13 wrote:
I made a really small mistake:

these lines:

> food_manager
> var
> list/categories
>

should be:

> food_manager
> var
> list/categories = list()
>

That runtime error is attempting to index a null value, which is invalid. I just forgot to initialize the list. My bad.

Bad! init proc creation, you should initialize the list when you first go to use it, not at compile-time, and ideally, null them out when you're not using them.
Bad! init proc creation, you should initialize the list when you first go to use it, not at compile-time, and ideally, null them out when you're not using them.

Ideally, yes. However, this is a singleton. The impact of either will approach is inconsequential. Were it not a singleton, I'd fully agree.
In response to Ter13
Ter13 wrote:

categories[cat]["[name] ([cost]g)"] = f //associate the name and cost to the type

In this line I tried adding another dimension to the categories list: the objs' desc var. Then I encountered a runtime error where only the first category showed up in the list during the do while loop. When I press that first category (Profession), it just repeats that input screen and I can go no further. The runtime error reported that the error was in that same line I attempted the change.

                    categories[cat]["[name]"]["[desc]"] = f //associate the name and cost to the type, and the desc

I don't understand the connection between adding [desc] to the categories list and the runtime error that occurs.

My reasoning behind it was to have another input box in the do/while loop where it displays the obj's desc, and gives the option to press Buy (I can handle this particular part).
Alright, you don't want to add another dimension to the list in that case.

This is the structure that we've created in memory:

{
"Meat" = {
"pork chop (10g)" = /obj/Food/Meat/pork_chop,
"lamb shank (15g)" = /obj/Food/Meat/lamb_shank
},
"Veggies" = {
"broccoli (2g)" = /obj/Food/Veggies/broccoli,
"carrot (1g)" = /obj/Food/Veggies/carrot
}
}


An associative list has two parts to each entry: The key, and the value:

list(key1 = value1, key2 = value2, ...)

l[1]=="key1"
l["key1"]==value1
//therefore:
l[l[1]]==value1


In the first dimension of the list, we're storing a second list based on the name of the category. When the user picks the category they want to shop from, we can retrieve all items that belong to that category by getting the list that's stored by the category name:

//user picks the category: Meat:
var/list/l = food_manager.categories["Meat"]
//l is an associative list containing key-value pairs:
//l[1]=="pork chop (10g)" (key), l["pork chop (10g)"]==/obj/Food/Meat/pork_chop (value)


Since the item's description is defined in the prototype, and we're already storing the prototype, we actually have enough information already in our two-dimensional list to get the description without having to store it. Why? Because all the information about the item is already stored in the type.

So you might be asking yourself: Why did we store the prototype by name and value as the key if we already had that information in the prototype? The answer to that is that we needed to display the name and value of the item in a format that would make sense to the player as to what they are buying and how much it will cost off-hand in the input() proc. We could have built this information at runtime, but it doesn't make sense to rebuild a list that will never change over and over again. That's why we're storing this information right when the game starts up --because after the game starts, the data is immutable and therefore the list will never change.

So let's look at how to get that description information to the player during the function. Remember this guy?

//plug the following into the verb that starts the shopping business.

var/category, selection, obj/Food/f, cost //set up our down-state variables ahead of time.
do //start looping behavior for category
category = input("What kind of food are you looking for?") as null|anything in food_manager.categories //select from the categories set up in the food manager.
if(category) //if the user didn't click cancel.
do //start looping behavior for item selection
selection = input("Sure, here's what we have:") as null|anything in food_manager.categories[category] //select the category subitem
if(selection) //if the user didn't click cancel
f = food_manager.categories[category][selection] //get the type from the associative list
cost = initial(f.cost) //query the type for its cost
if(cost<=usr.money) //make sure the user has enough money
usr.money -= cost
usr << "You buy \a [initial(f.name)]."
else
usr << "You don't have enough money to buy \a [initial(f.name)]."
while(selection!=null) //repeat until the user clicks cancel.
while(category!=null) //repeat until the user clicks cancel.


We're going to modify it.

//plug the following into the verb that starts the shopping business.

var/category, selection, obj/Food/f, cost, desc //set up our down-state variables ahead of time.
do //start looping behavior for category
category = input("What kind of food are you looking for?") as null|anything in food_manager.categories //select from the categories set up in the food manager.
if(category) //if the user didn't click cancel.
do //start looping behavior for item selection
selection = input("Sure, here's what we have:") as null|anything in food_manager.categories[category] //select the category subitem
if(selection) //if the user didn't click cancel
f = food_manager.categories[category][selection] //get the type from the associative list
//CHANGES BEGIN HERE
if(alert("[selection]\n[initial(f.desc)]","Buy [selection]?","Buy","Cancel")=="Cancel")
continue //go back to subitem selection; skip the code below this point in the current loop.
//CHANGES END HERE
cost = initial(f.cost) //query the type for its cost
if(cost<=usr.money) //make sure the user has enough money
usr.money -= cost
usr << "You buy \a [initial(f.name)]."
else
usr << "You don't have enough money to buy \a [initial(f.name)]."
while(selection!=null) //repeat until the user clicks cancel.
while(category!=null) //repeat until the user clicks cancel.


TL;DR: Don't add the third dimension because the information you are looking for can be obtained at runtime according to the item picked from the two-dimensional list.
Daaaaawwwwwg, you my nigga. If I could post the fist-in-the-air emoji I would. I was thinking we could only use the name and cost vars with the list. But earlier in your code all the vars in each obj/Perk item was assigned to to a list within that New() proc, so I should've guessed. Your examples and explanations help me understand the syntax and concepts. Thanks man I really appreciate it.