forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
446 lines
21 KiB
Python
446 lines
21 KiB
Python
from typing import Dict, Any, Optional, List
|
|
from copy import deepcopy
|
|
|
|
from .types import Race, StorylineEnum, Profession
|
|
from .storylines import storyline_from_str, get_owned_storylines
|
|
from .rules import get_map_rule, get_region_rule
|
|
from BaseClasses import Region, Tutorial, ItemClassification
|
|
from .Util import random_round
|
|
from .options import GuildWars2Options, GroupContent, StartingMainhandWeapon, CharacterProfession, CharacterRace, \
|
|
StartingOffhandWeapon, Storyline, HealSkill, GearSlots, StorylineItems
|
|
from worlds.AutoWorld import World, WebWorld
|
|
from .items import item_table, Gw2ItemData, Gw2Item, weapons_by_slot, item_groups, item_data, elite_specs
|
|
from .regions import MapType, group_content, ten_man_content, competitive_content, map_data
|
|
from .locations import location_table, LocationType, location_groups, Gw2Location, LocationData, storyline_items, \
|
|
storyline_pois
|
|
|
|
|
|
class Gw2Web(WebWorld):
|
|
tutorials = [Tutorial(
|
|
"Mod Setup and Use Guide",
|
|
"A guide to playing Guild Wars 2 with Archipelago.",
|
|
"English",
|
|
"setup_en.md",
|
|
"setup/en",
|
|
["Feldar"]
|
|
)]
|
|
|
|
|
|
class Gw2World(World):
|
|
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
|
location_name_to_id = {name: data.code for name, data in location_table.items()}
|
|
|
|
options_dataclass = GuildWars2Options
|
|
options: GuildWars2Options
|
|
|
|
region_table: dict[str, Region]
|
|
player_items: List[Gw2ItemData]
|
|
|
|
web = Gw2Web()
|
|
|
|
"""
|
|
Guild Wars 2 is an online role-playing game with fast-paced action combat, a rich and detailed universe of
|
|
stories, awe-inspiring landscapes to explore, two challenging player vs. player modes—and no subscription fees!
|
|
"""
|
|
game = "Guild Wars 2"
|
|
|
|
starting_mainhand: Optional[Gw2ItemData]
|
|
starting_offhand: Optional[Gw2ItemData]
|
|
starting_map: Gw2ItemData
|
|
|
|
def includes_storyline(self, storyline: StorylineEnum):
|
|
if storyline is StorylineEnum.CORE:
|
|
return True
|
|
if self.options.storyline_items.value == StorylineItems.option_all:
|
|
return True
|
|
elif self.options.storyline_items.value == StorylineItems.option_storyline_plus:
|
|
return storyline.value <= self.options.storyline.value
|
|
elif self.options.storyline_items.value == StorylineItems.option_storyline:
|
|
if self.options.storyline.value == Storyline.option_season_3:
|
|
return storyline == StorylineEnum.HEART_OF_THORNS
|
|
elif self.options.storyline.value == Storyline.option_season_4:
|
|
return storyline == StorylineEnum.PATH_OF_FIRE
|
|
elif self.options.storyline.value == Storyline.option_icebrood_saga:
|
|
return storyline == StorylineEnum.PATH_OF_FIRE
|
|
else:
|
|
return storyline.value == self.options.storyline.value
|
|
else:
|
|
return False
|
|
|
|
def includes_map_type(self, map_type: MapType):
|
|
if map_type in group_content and self.options.group_content == GroupContent.option_none:
|
|
return False
|
|
if map_type in ten_man_content and self.options.group_content != GroupContent.option_ten_man:
|
|
return False
|
|
if map_type in competitive_content and not self.options.include_competitive:
|
|
return False
|
|
|
|
return True
|
|
|
|
def item_is_usable(self, item: Gw2ItemData, allow_elite_spec: bool) -> bool:
|
|
#Map Items
|
|
if item.name in map_data:
|
|
gw2_map = map_data[item.name]
|
|
|
|
if not self.includes_map_type(gw2_map.type):
|
|
# print(item.name + " not usable: Map Type")
|
|
return False
|
|
|
|
storyline = StorylineEnum(self.options.storyline.value)
|
|
for owned_storyline in get_owned_storylines(storyline):
|
|
if owned_storyline in gw2_map.storylines:
|
|
# print(item.name)
|
|
return True
|
|
|
|
|
|
# print(item.name + " not usable: Storyline")
|
|
return False
|
|
|
|
|
|
if item.storyline is not None and not self.includes_storyline(item.storyline):
|
|
return False
|
|
|
|
if len(item.specs) > 0:
|
|
match = False
|
|
for spec in item.specs:
|
|
if not allow_elite_spec and spec.elite_spec is not None:
|
|
continue
|
|
if spec.profession.value == self.options.character_profession.value:
|
|
if spec.elite_spec is not None:
|
|
storyline = storyline_from_str(spec.elite_spec) or elite_specs[spec.elite_spec]
|
|
if storyline is not None and not self.includes_storyline(storyline):
|
|
continue
|
|
match = True
|
|
break
|
|
if not match:
|
|
return False
|
|
|
|
# Revenants can't use racial skills
|
|
if item.race is not None and self.options.character_profession == CharacterProfession.option_revenant:
|
|
return False
|
|
|
|
if item.race is not None and item.race.value != self.options.character_race.value:
|
|
return False
|
|
|
|
return True
|
|
|
|
def generate_early(self) -> None:
|
|
#Update Mist Fragment count
|
|
mist_fragments_required = self.options.mist_fragments_required.value
|
|
bonus_mist_fragments = random_round(self.random, mist_fragments_required * (
|
|
self.options.extra_mist_fragment_percent / 100.0))
|
|
mist_fragment_count = mist_fragments_required + bonus_mist_fragments
|
|
item_table["Mist Fragment"].quantity = mist_fragment_count
|
|
|
|
self.options.local_items.value.add("Mist Fragment")
|
|
|
|
if self.options.heal_skill.value == HealSkill.option_early:
|
|
skill = self.get_random_heal_skill()
|
|
self.multiworld.early_items[self.player][skill.name] = 1
|
|
|
|
if self.options.gear_slots.value == GearSlots.option_early:
|
|
for item in item_groups["Gear"]:
|
|
self.multiworld.early_items[self.player][item.name] = 1
|
|
|
|
self.select_starting_weapons()
|
|
|
|
def get_random_heal_skill(self):
|
|
skill = None
|
|
item_group = "Healing"
|
|
if self.options.character_profession.value == CharacterProfession.option_revenant:
|
|
item_group = "Legend"
|
|
self.random.shuffle(item_groups[item_group])
|
|
for heal_skill in item_groups[item_group]:
|
|
if self.item_is_usable(heal_skill, False):
|
|
skill = heal_skill
|
|
return skill
|
|
|
|
def create_regions(self) -> None:
|
|
|
|
self.region_table = {}
|
|
|
|
#Create Menu region
|
|
menu = Region(name="Menu", player=self.player, multiworld=self.multiworld)
|
|
self.multiworld.regions.append(menu)
|
|
open_world = None
|
|
|
|
# Create map regions
|
|
map_connections = []
|
|
start_map = None
|
|
for gw2_map in map_data.values():
|
|
region = Region(name=gw2_map.name, player=self.player, multiworld=self.multiworld)
|
|
self.region_table[gw2_map.name] = region
|
|
for entrance_map_name in gw2_map.entrances:
|
|
map_connections.append((entrance_map_name, region, get_map_rule(gw2_map, self.player, self.options.character_profession)))
|
|
storyline = StorylineEnum(self.options.storyline.value)
|
|
# if len(gw2_map.start) > 0:
|
|
# print(storyline, gw2_map.start)
|
|
if storyline in gw2_map.start:
|
|
requirements = set(gw2_map.start[storyline])
|
|
race = Race(self.options.character_race.value).name
|
|
profession = Profession(self.options.character_profession.value).name
|
|
# print(race, Race(self.options.character_race.value))
|
|
if "ANY" in requirements or race in requirements or profession in requirements:
|
|
# print(region)
|
|
start_map = region
|
|
|
|
# print(menu.name, start_map.name)
|
|
menu.connect(start_map)
|
|
|
|
for map_connection in map_connections:
|
|
self.region_table[map_connection[0]].connect(map_connection[1], rule=map_connection[2])
|
|
|
|
# Generic Regions for achievements and quests until I can get those fully cataloged
|
|
for region_enum in MapType:
|
|
if not self.includes_map_type(region_enum):
|
|
continue
|
|
|
|
region = Region(name=region_enum.name, player=self.player, multiworld=self.multiworld)
|
|
start_map.connect(region,
|
|
rule=get_region_rule(region_enum,
|
|
self.player,
|
|
StorylineEnum(self.options.storyline.value),
|
|
self.options.character_profession))
|
|
|
|
self.region_table[region_enum.name] = region
|
|
|
|
#determine which items will be generated by logic
|
|
self.player_items = []
|
|
item_count = 0
|
|
for item_name, item in item_table.items():
|
|
if not self.item_is_usable(item,
|
|
True):
|
|
continue
|
|
|
|
# print(item_name)
|
|
self.player_items.append(item)
|
|
item_count += item.quantity
|
|
|
|
# need a location for every map except the starting map
|
|
item_count -= 1
|
|
# self.player_items.extend(item_groups["Maps"])
|
|
self.starting_map = map_data[start_map.name].item
|
|
|
|
|
|
if self.options.gear_slots.value == GearSlots.option_starting:
|
|
item_count -= len(item_groups["Gear"])
|
|
|
|
if self.options.heal_skill.value == GearSlots.option_starting:
|
|
item_count -= 1
|
|
|
|
if self.starting_offhand is not None:
|
|
item_count -= 1
|
|
|
|
if self.starting_mainhand is not None:
|
|
item_count -= 1
|
|
|
|
#create a number of locations equal to the number of items that will be generated
|
|
location_count = 0
|
|
unused_locations = deepcopy(location_groups)
|
|
unused_items = deepcopy(storyline_items[self.options.storyline.value]) if self.options.storyline in storyline_items else []
|
|
unused_pois = deepcopy(storyline_pois[self.options.storyline.value]) if self.options.storyline in storyline_pois else []
|
|
early_items = [item for item in unused_items if item.map.name == start_map.name]
|
|
early_pois = [poi for poi in unused_pois if poi.map.name == start_map.name]
|
|
# print(self.starting_map)
|
|
# print(early_items)
|
|
# print(early_pois)
|
|
|
|
max_counts = (item_count,
|
|
0,
|
|
self.options.max_quests.value,
|
|
0,
|
|
0,
|
|
len(unused_items),
|
|
len(unused_pois),
|
|
)
|
|
counts = [0, 0, 0, 0, 0, 0, 0]
|
|
|
|
#start at one for the first map unlock item
|
|
early_item_count = 1
|
|
for item_name in self.multiworld.early_items[self.player]:
|
|
early_item_count += self.multiworld.early_items[self.player][item_name]
|
|
|
|
early_location_count = 0
|
|
|
|
weights = [self.options.achievement_weight.value,
|
|
self.options.achievement_weight.value if max_counts[1] > 0 else 0,
|
|
self.options.quest_weight.value if max_counts[2] > 0 else 0,
|
|
self.options.training_weight.value if max_counts[3] > 0 else 0,
|
|
self.options.world_boss_weight.value if max_counts[4] > 0 else 0,
|
|
self.options.unique_item_weight.value if max_counts[5] > 0 else 0,
|
|
self.options.poi_weight.value if max_counts[6] > 0 else 0,
|
|
]
|
|
while location_count < item_count:
|
|
location_type = self.random.choices([location for location in LocationType], weights=weights, k=1)[0]
|
|
|
|
location_region_data = unused_locations[location_type]
|
|
region = None
|
|
while region is None:
|
|
region_enum = self.random.choice(location_type.get_valid_regions())
|
|
|
|
# Core story has no achievements
|
|
if (self.options.storyline == Storyline.option_core
|
|
and region_enum == MapType.STORY
|
|
and location_type == LocationType.ACHIEVEMENT):
|
|
continue
|
|
|
|
if region_enum.name in self.region_table.keys():
|
|
region = self.region_table[region_enum.name]
|
|
|
|
if location_type == LocationType.UNIQUE_ITEM:
|
|
if early_location_count < early_item_count and len(early_items) > 0:
|
|
location_index = self.random.randint(0, len(early_items) - 1)
|
|
location_data = early_items.pop(location_index)
|
|
unused_items.remove(location_data)
|
|
early_location_count += 1
|
|
else:
|
|
location_index = self.random.randint(0, len(unused_items) - 1)
|
|
# print(location_index, " / ", len(unused_items))
|
|
location_data = unused_items.pop(location_index)
|
|
elif location_type == LocationType.POINT_OF_INTEREST:
|
|
if early_location_count < early_item_count and len(early_pois) > 0:
|
|
location_index = self.random.randint(0, len(early_pois) - 1)
|
|
location_data = early_pois.pop(location_index)
|
|
unused_pois.remove(location_data)
|
|
early_location_count += 1
|
|
else:
|
|
location_index = self.random.randint(0, len(unused_pois) - 1)
|
|
location_data = unused_pois.pop(location_index)
|
|
else:
|
|
location_data_objects = location_region_data[region_enum]
|
|
location_data = location_data_objects.pop(0)
|
|
|
|
if location_data.map is not None:
|
|
region = self.region_table[location_data.map.name]
|
|
|
|
location = Gw2Location(self.player, location_data.name, location_data.code, region)
|
|
region.locations.append(location)
|
|
|
|
counts[location_type.value] += 1
|
|
location_count += 1
|
|
if counts[location_type.value] >= max_counts[location_type.value]:
|
|
# print("No more ", location_type)
|
|
weights[location_type.value] = 0
|
|
|
|
self.multiworld.regions.extend(self.region_table.values())
|
|
|
|
def create_item(self, item_name: str) -> Gw2Item:
|
|
return Gw2Item(item_name, ItemClassification.progression, self.item_name_to_id[item_name], self.player)
|
|
|
|
def create_item_from_data(self, data: Gw2ItemData) -> Gw2Item:
|
|
return Gw2Item(data.name, ItemClassification.progression, data.code, self.player)
|
|
|
|
def create_items(self) -> None:
|
|
self.precollect_starting_items()
|
|
|
|
for item_name, item_data in item_table.items():
|
|
if item_name in {item.name for item in self.multiworld.precollected_items[self.player]}:
|
|
continue
|
|
|
|
if not self.item_is_usable(item_data, True):
|
|
continue
|
|
|
|
quantity = item_data.quantity
|
|
for i in range(quantity):
|
|
self.multiworld.itempool.append(self.create_item_from_data(item_data))
|
|
|
|
def precollect_starting_items(self) -> None:
|
|
self.multiworld.push_precollected(self.create_item_from_data(self.starting_map))
|
|
if self.starting_mainhand is not None:
|
|
self.multiworld.push_precollected(self.create_item_from_data(self.starting_mainhand))
|
|
if self.starting_offhand is not None:
|
|
self.multiworld.push_precollected(self.create_item_from_data(self.starting_offhand))
|
|
|
|
if self.options.heal_skill.value == HealSkill.option_starting:
|
|
skill = self.get_random_heal_skill()
|
|
|
|
self.multiworld.push_precollected(self.create_item_from_data(skill))
|
|
|
|
if self.options.gear_slots.value == GearSlots.option_starting:
|
|
for item in item_groups["Gear"]:
|
|
self.multiworld.push_precollected(self.create_item_from_data(item))
|
|
|
|
def select_starting_weapons(self) -> None:
|
|
two_handed_weapons = list(filter(lambda weapon: self.item_is_usable(weapon, False),
|
|
weapons_by_slot["TwoHanded"]))
|
|
self.starting_mainhand = self.select_starting_mainhand(two_handed_weapons)
|
|
if self.starting_mainhand in two_handed_weapons:
|
|
self.starting_offhand = None
|
|
else:
|
|
self.starting_offhand = self.select_starting_offhand()
|
|
|
|
def select_starting_offhand(self) -> Optional[Gw2ItemData]:
|
|
offhand_weapons = list(filter(lambda weapon: self.item_is_usable(weapon, False),
|
|
weapons_by_slot["Offhand"]))
|
|
if self.options.starting_offhand_weapon == StartingOffhandWeapon.option_random_proficient:
|
|
return self.random.choice(offhand_weapons)
|
|
elif self.options.starting_offhand_weapon == StartingOffhandWeapon.option_scepter:
|
|
return item_table["Offhand Scepter"]
|
|
elif self.options.starting_offhand_weapon == StartingOffhandWeapon.option_focus:
|
|
return item_table["Offhand Focus"]
|
|
elif self.options.starting_offhand_weapon == StartingOffhandWeapon.option_shield:
|
|
return item_table["Offhand Shield"]
|
|
elif self.options.starting_offhand_weapon == StartingOffhandWeapon.option_torch:
|
|
return item_table["Offhand Torch"]
|
|
elif self.options.starting_offhand_weapon == StartingOffhandWeapon.option_warhorn:
|
|
return item_table["Offhand Warhorn"]
|
|
return None
|
|
|
|
def select_starting_mainhand(self, two_handed_weapons: list[Gw2ItemData]) -> Optional[Gw2ItemData]:
|
|
mainhand_weapons = list(filter(lambda weapon: self.item_is_usable(weapon, False),
|
|
weapons_by_slot["Mainhand"]))
|
|
if self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_random_proficient:
|
|
return self.random.choice(mainhand_weapons + two_handed_weapons)
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_random_proficient_one_handed:
|
|
return self.random.choice(mainhand_weapons)
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_random_proficient_two_handed:
|
|
return self.random.choice(two_handed_weapons)
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_axe:
|
|
return item_table["Mainhand Axe"]
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_dagger:
|
|
return item_table["Mainhand Dagger"]
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_mace:
|
|
return item_table["Mainhand Mace"]
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_pistol:
|
|
return item_table["Mainhand Pistol"]
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_sword:
|
|
return item_table["Mainhand Sword"]
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_scepter:
|
|
return item_table["Mainhand Scepter"]
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_greatsword:
|
|
return item_table["TwoHanded Greatsword"]
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_hammer:
|
|
return item_table["TwoHanded Hammer"]
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_longbow:
|
|
return item_table["TwoHanded Longbow"]
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_rifle:
|
|
return item_table["TwoHanded Rifle"]
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_short_bow:
|
|
return item_table["TwoHanded ShortBow"]
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_staff:
|
|
return item_table["TwoHanded Staff"]
|
|
elif self.options.starting_mainhand_weapon == StartingMainhandWeapon.option_spear:
|
|
return item_table["TwoHanded Spear"]
|
|
return None
|
|
|
|
def fill_slot_data(self) -> Dict[str, Any]:
|
|
return self.options.as_dict("storyline", "mist_fragments_required", "character", "character_race",
|
|
"character_profession", casing="pascal")
|
|
|
|
def set_rules(self) -> None:
|
|
self.multiworld.completion_condition[self.player] = \
|
|
lambda state: state.has("Mist Fragment", self.player, self.options.mist_fragments_required.value)
|
|
|
|
# def generate_output(self, output_directory: str) -> None:
|
|
# data = {}
|
|
# for region in self.multiworld.regions:
|
|
# if region.player != self.player:
|
|
# continue
|
|
# data[region.name] = []
|
|
# for location in region.locations:
|
|
# data[region.name].append(location.name)
|
|
#
|
|
# filename = f"{self.multiworld.get_out_file_name_base(self.player)}.apgw2"
|
|
# with open(os.path.join(output_directory, filename), 'w') as f:
|
|
# json.dump(data, f)
|