forked from ArchipelagoMW/Archipelago
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathitems.py
More file actions
166 lines (138 loc) · 9.81 KB
/
Copy pathitems.py
File metadata and controls
166 lines (138 loc) · 9.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
from __future__ import annotations
from typing import TYPE_CHECKING
from BaseClasses import Item, ItemClassification
if TYPE_CHECKING:
from .world import APQuestWorld
# Every item must have a unique integer ID associated with it.
# We will have a lookup from item name to ID here that, in world.py, we will import and bind to the world class.
# Even if an item doesn't exist on specific options, it must be present in this lookup.
ITEM_NAME_TO_ID = {
"Key": 1,
"Sword": 2,
"Shield": 3,
"Hammer": 4,
"Health Upgrade": 5,
"Confetti Cannon": 6,
"Math Trap": 7,
}
# Items should have a defined default classification.
# In our case, we will make a dictionary from item name to classification.
DEFAULT_ITEM_CLASSIFICATIONS = {
"Key": ItemClassification.progression,
"Sword": ItemClassification.progression | ItemClassification.useful, # Items can have multiple classifications.
"Shield": ItemClassification.progression,
"Hammer": ItemClassification.progression,
"Health Upgrade": ItemClassification.useful,
"Confetti Cannon": ItemClassification.filler,
"Math Trap": ItemClassification.trap,
}
# Each Item instance must correctly report the "game" it belongs to.
# To make this simple, it is common practice to subclass the basic Item class and override the "game" field.
class APQuestItem(Item):
game = "APQuest"
# Ontop of our regular itempool, our world must be able to create arbitrary amounts of filler as requested by core.
# To do this, it must define a function called world.get_filler_item_name(), which we will define in world.py later.
# For now, let's make a function that returns the name of a random filler item here in items.py.
def get_random_filler_item_name(world: APQuestWorld) -> str:
# APQuest has an option called "trap_chance".
# This is the percentage chance that each filler item is a Math Trap instead of a Confetti Cannon.
# For this purpose, we need to use a random generator.
# IMPORTANT: Whenever you need to use a random generator, you must use world.random.
# This ensures that generating with the same generator seed twice yields the same output.
# DO NOT use a bare random object from Python's built-in random module.
if world.random.randint(0, 99) < world.options.trap_chance:
return "Math Trap"
return "Confetti Cannon"
def create_item_with_correct_classification(world: APQuestWorld, name: str) -> APQuestItem:
# Our world class must have a create_item() function that can create any of our items by name at any time.
# So, we make this helper function that creates the item by name with the correct classification.
# Note: This function's content could just be the contents of world.create_item in world.py directly,
# but it seemed nicer to have it in its own function over here in items.py.
classification = DEFAULT_ITEM_CLASSIFICATIONS[name]
# It is perfectly normal and valid for an item's classification to differ based on the player's options.
# In our case, Health Upgrades are only relevant to logic (and thus labeled as "progression") in hard mode.
if name == "Health Upgrade" and world.options.hard_mode:
classification = ItemClassification.progression
return APQuestItem(name, classification, ITEM_NAME_TO_ID[name], world.player)
# With those two helper functions defined, let's now get to actually creating and submitting our itempool.
def create_all_items(world: APQuestWorld) -> None:
# This is the function in which we will create all the items that this world submits to the multiworld item pool.
# There must be exactly as many items as there are locations.
# In our case, there are either six or seven locations.
# We must make sure that when there are six locations, there are six items,
# and when there are seven locations, there are seven items.
# Creating items should generally be done via the world's create_item method.
# First, we create a list containing all the items that always exist.
itempool: list[Item] = [
world.create_item("Key"),
world.create_item("Sword"),
world.create_item("Shield"),
world.create_item("Health Upgrade"),
world.create_item("Health Upgrade"),
]
# Some items may only exist if the player enables certain options.
# In our case, If the hammer option is enabled, the sixth item is the Hammer.
# Otherwise, we add a filler Confetti Cannon.
if world.options.hammer:
# Once again, it is important to stress that even though the Hammer doesn't always exist,
# it must be present in the worlds item_name_to_id.
# Whether it is actually in the itempool is determined purely by whether we create and add the item here.
itempool.append(world.create_item("Hammer"))
# Archipelago requires that each world submits as many locations as it submits items.
# This is where we can use our filler and trap items.
# APQuest has two of these: The Confetti Cannon and the Math Trap.
# (Unfortunately, Archipelago is a bit ambiguous about its terminology here:
# "filler" is an ItemClassification separate from "trap", but in a lot of its functions,
# Archipelago will use "filler" to just mean "an additional item created to fill out the itempool".
# "Filler" in this sense can technically have any ItemClassification,
# but most commonly ItemClassification.filler or ItemClassification.trap.
# Starting here, the word "filler" will be used to collectively refer to APQuest's Confetti Cannon and Math Trap,
# which are ItemClassification.filler and ItemClassification.trap respectively.)
# Creating filler items works the same as any other item. But there is a question:
# How many filler items do we actually need to create?
# In regions.py, we created either six or seven locations depending on the "extra_starting_chest" option.
# In this function, we have created five or six items depending on whether the "hammer" option is enabled.
# We *could* have a really complicated if-else tree checking the options again, but there is a better way.
# We can compare the size of our itempool so far to the number of locations in our world.
# The length of our itempool is easy to determine, since we have it as a list.
number_of_items = len(itempool)
# The number of locations is also easy to determine, but we have to be careful.
# Just calling len(world.get_locations()) would report an incorrect number, because of our *event locations*.
# What we actually want is the number of *unfilled* locations. Luckily, there is a helper method for this:
number_of_unfilled_locations = len(world.multiworld.get_unfilled_locations(world.player))
# Now, we just subtract the number of items from the number of locations to get the number of empty item slots.
needed_number_of_filler_items = number_of_unfilled_locations - number_of_items
# Finally, we create that many filler items and add them to the itempool.
# To create our filler, we could just use world.create_item("Confetti Cannon").
# But there is an alternative that works even better for most worlds, including APQuest.
# As discussed above, our world must have a get_filler_item_name() function defined,
# which must return the name of an infinitely repeatable filler item.
# Defining this function enables the use of a helper function called world.create_filler().
# You can just use this function directly to create as many filler items as you need to complete your itempool.
itempool += [world.create_filler() for _ in range(needed_number_of_filler_items)]
# But... is that the right option for your game? Let's explore that.
# For some games, the concepts of "regular itempool filler" and "additionally created filler" are different.
# These games might want / require specific amounts of specific filler items in their regular pool.
# To achieve this, they will have to intentionally create the correct quantities using world.create_item().
# They may still use world.create_filler() to fill up the rest of their itempool with "repeatable filler",
# after creating their "specific quantity" filler and still having room left over.
# But there are many other games which *only* have infinitely repeatable filler items.
# They don't care about specific amounts of specific filler items, instead only caring about the proportions.
# In this case, world.create_filler() can just be used for the entire filler itempool.
# APQuest is one of these games:
# Regardless of whether it's filler for the regular itempool or additional filler for item links / etc.,
# we always just want a Confetti Cannon or a Math Trap depending on the "trap_chance" option.
# We defined this behavior in our get_random_filler_item_name() function, which in world.py,
# we'll bind to world.get_filler_item_name(). So, we can just use world.create_filler() for all of our filler.
# Anyway. With our world's itempool finalized, we now need to submit it to the multiworld itempool.
# This is how the generator actually knows about the existence of our items.
world.multiworld.itempool += itempool
# Sometimes, you might want the player to start with certain items already in their inventory.
# These items are called "precollected items".
# They will be sent as soon as they connect for the first time (depending on your client's item handling flag).
# Players can add precollected items themselves via the generic "start_inventory" option.
# If you want to add your own precollected items, you can do so via world.push_precollected().
if world.options.start_with_one_confetti_cannon:
# We're adding a filler item, but you can also add progression items to the player's precollected inventory.
starting_confetti_cannon = world.create_item("Confetti Cannon")
world.push_precollected(starting_confetti_cannon)