mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-08-04 12:36:14 -07:00
Gauntlet Legends: Implement New Game (#3499)
* Update GauntletLegendsClient.py * Continuation of obelisk update * APPP Implementation Groundwork. * Continuation of APPP Implementation * Create Pain.asm * ROM verification modified since crc32 hash can no longer be calculated. * Initial upload of barrel/chest locations. * Finished adding and naming all chests/barrels. * Continued locations. * Added new yaml options * Work on mirror shards * Python 3.8 compatibility * Finalized location names. Fixed lambda bug. * v1.0 * Removed excess Docs and Changed item counts. * Update Archipelago Unittests.run.xml * Started working on Docs. * Updated item arrays for client * Fix client bugs * Flake8 fixes + Yaml fix * Update speed boots * v1.1 * Update GauntletLegendsClient.py * Update GauntletLegendsClient.py * Update GauntletLegendsClient.py * Update GauntletLegendsClient.py * v1.0.4 * Initial Ruff format * Formatting part-2 * More formatting + Doc changes * Updated Docs * Move client to root * Update Client Name * Make Imports Relative * Add new yaml options + rename obelisks * v1.1 Initial Upload * v1.1.0 + some additions * Client Update * Client Updates * pre-v1.1.1 * Update GauntletLegendsClient.py * pre-1.2 * 1.2.0 * v1.2.1 * Client fixes * v1.3.0 * v1.4.0 * v1.4.1 * Merged Changes * Experimental * pre-v1.5.0 * Changes * v1.5.0 * Update GauntletLegendsClient.py * Death Update * pre v1.5.1 * Obelisk rule changes * pre-v1.5.1 * Update GauntletLegendsClient.py * v1.5.1 * v1.5.1 * Update GauntletLegendsClient.py * Small fix * Update GauntletLegendsClient.py * v1.5.2 * pre-v1.6.0 * pre-v1.6.0 * v1.6.0 * Update * Update GauntletLegendsClient.py * Test * pre-v1.6.0 * Client Updtaes * Update GauntletLegendsClient.py * Update GauntletLegendsClient.py * Update GauntletLegendsClient.py * Update Rom.py * Update GauntletLegendsClient.py * Update Rom.py * Added Logging on stage enter * v1.6.2 * v1.6.2 * Update Rom.py * Update GauntletLegendsClient.py * v1.6.3 * Small Update * Update GauntletLegendsClient.py * Update GauntletLegendsClient.py * Update GauntletLegendsClient.py * Changes ready for review * Update Rom.py * Remove root client * Update inno_setup.iss * Update client * Initial Review Refactor * Update Arrays.py * Review refactor * Update __init__.py * Small tweaks after test generations * Update GauntletLegendsClient.py * Implement new check_locations() function * Add name for Skorn to level_names Dict * Fix for #4671 * Update __init__.py * Adjust item frequency + Place 50% of filler as local * Oops I modified the itempool after create_items teehee * Update __init__.py * Check MD5 hash before patching ROM * Update __init__.py * Fix get_filler_item_name() * Update MD5 check to built in method in FilePath * Update Rom.py * Update worlds/gl/Rom.py Co-authored-by: qwint <[email protected]> * WIP Generation fixes * v1.7.1 * Update Archipelago Unittests.run.xml * 0.7.2 * Update GauntletLegendsClient.py * Review Edits * Update World description * Fuzz fixes * Update GauntletLegendsClient.py * Make static data immutable * Update option comparisons since no longer a Choice * Update worlds/gl/Rom.py Co-authored-by: Doug Hoskisson <[email protected]> * Update __init__.py * Initial Update commit * Progress * Remove un-used import + Add manifest * Add missing docstring for client command * Progress * pre-v2.0.0-BETA * Updates * Progress * Working Generations * Working IN-Game * v2.0.0-BETA * Create archipelago.json * v2.0.1 * v2.0.1 FINAL * pre v2.0.2 * Delete report.json * Cleanup * pre v2.0.2 * v2.0.2 * PR Ready * Update Items.py * Delete fuzz.py * Update Items.py * Update GauntletLegendsClient.py * Fix scouting disabled locations * Client Updates * v2.0.3 * Increment World Version * Docstring + slot_data updates * Option name update + Remove redundant option * Code Cleanup * Trenches is level 24 * Frequencies * More cleanup and removal of stale code. MISSING BASEPATCH * v2.1.0 * Minor enemy changes * Small fix * Update Rom.py * v2.1.1 * Small fixes * Update basepatch.bsdiff4 * Update GauntletLegendsClient.py * Socket error clarity * Delete settings.local.json * Update GauntletLegendsClient.py * v2.1.2 fixes * Boss portal fix * Small client fix + Item Frequencies * Update archipelago.json * Fix for games playing with max difficulty lower than 4 not sending locations * Update archipelago.json * v2.1.5 * Fix for Chimera's lair mirror shard * Update archipelago.json * Mod Re-Base + Queue limits + Item Receiving fixes * Options cleanup and newlines for some files * Include v2.x features in docs * Make sure players has at least 1 item * v2.1.6 * Update archipelago.json * Review Edits + Client Tweaks --------- Co-authored-by: qwint <[email protected]> Co-authored-by: Doug Hoskisson <[email protected]> Co-authored-by: Fabian Dill <[email protected]> Co-authored-by: NewSoupVi <[email protected]>
This commit is contained in:
co-authored by
qwint
Doug Hoskisson
Fabian Dill
NewSoupVi
parent
f9c5868f7b
commit
f110a7efbc
@@ -0,0 +1,346 @@
|
||||
import os
|
||||
|
||||
import settings
|
||||
|
||||
from BaseClasses import ItemClassification, Tutorial, Item, CollectionState
|
||||
from Fill import fast_fill
|
||||
from typing import ClassVar
|
||||
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
|
||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch
|
||||
from .Data import obelisks, mirror_shards, portals, excluded_portals, \
|
||||
excluded_obelisks, level_locations
|
||||
from .Items import GLItem, item_table, item_list, gauntlet_item_name_groups
|
||||
from .Locations import LocationData, all_locations, location_table, get_locations_by_tags, locationName_to_data
|
||||
from .Options import GLOptions, IncludedAreas, IncludedTraps
|
||||
from .Regions import connect_regions, create_regions
|
||||
from .Rom import GLProcedurePatch, write_files
|
||||
from .Rules import set_rules, goal_conditions
|
||||
|
||||
|
||||
def launch_client(*args):
|
||||
from .GauntletLegendsClient import launch as launch_glclient
|
||||
launch(launch_glclient, name="GauntletLegendsClient", args=args)
|
||||
|
||||
|
||||
components.append(
|
||||
Component(
|
||||
"Gauntlet Legends Client",
|
||||
func=launch_client,
|
||||
component_type=Type.CLIENT,
|
||||
file_identifier=SuffixIdentifier(".apgl"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class GauntletLegendsWebWorld(WebWorld):
|
||||
theme = "partyTime"
|
||||
tutorials = [
|
||||
Tutorial(
|
||||
tutorial_name="Setup Guide",
|
||||
description="A guide to playing Gauntlet Legends",
|
||||
language="English",
|
||||
file_name="setup_en.md",
|
||||
link="setup/en",
|
||||
authors=["jamesbrq"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class GLSettings(settings.Group):
|
||||
class RetorarchPath(settings.UserFolderPath):
|
||||
"""The location of your Retroarch folder"""
|
||||
description = "Retroarch Folder"
|
||||
|
||||
class RomFile(settings.UserFilePath):
|
||||
"""File name of the GL US rom"""
|
||||
copy_to = "Gauntlet Legends (U) [!].z64"
|
||||
description = "Gauntlet Legends ROM File"
|
||||
md5s = ["9cb963e8b71f18568f78ec1af120362e"]
|
||||
|
||||
retroarch_path: RetorarchPath = RetorarchPath(None)
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
rom_start: bool = True
|
||||
|
||||
|
||||
class GauntletLegendsWorld(World):
|
||||
"""
|
||||
Adventure through the 5 realms to collect 13 runestones
|
||||
and defeat the evil skorne. Treasure, enemies, and death
|
||||
awaits.
|
||||
"""
|
||||
|
||||
game = "Gauntlet Legends"
|
||||
web = GauntletLegendsWebWorld()
|
||||
options_dataclass = GLOptions
|
||||
options: GLOptions
|
||||
settings: ClassVar[GLSettings]
|
||||
item_name_groups = gauntlet_item_name_groups
|
||||
item_name_to_id = {name: data.id for name, data in item_table.items()}
|
||||
location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations}
|
||||
excluded_regions: set
|
||||
items: list[Item]
|
||||
unlockable: set
|
||||
|
||||
disabled_locations: set[str]
|
||||
|
||||
def generate_early(self) -> None:
|
||||
self.disabled_locations = set()
|
||||
self.excluded_regions = set()
|
||||
self.unlockable = set()
|
||||
self.items = []
|
||||
|
||||
def create_regions(self) -> None:
|
||||
if self.options.chests_barrels == "none":
|
||||
self.disabled_locations.update([
|
||||
location.name
|
||||
for location in all_locations
|
||||
if "Chest" in location.name or ("Barrel" in location.name and "Barrel of Gold" not in location.name)
|
||||
])
|
||||
elif self.options.chests_barrels == "all_chests":
|
||||
self.disabled_locations.update([
|
||||
location.name
|
||||
for location in all_locations
|
||||
if "Barrel" in location.name and "Barrel of Gold" not in location.name
|
||||
])
|
||||
elif self.options.chests_barrels == "all_barrels":
|
||||
self.disabled_locations.update([location.name for location in all_locations if "Chest" in location.name])
|
||||
|
||||
self.disabled_locations.update([location.name for location in all_locations
|
||||
if location.difficulty > self.options.max_difficulty])
|
||||
self.excluded_regions.update([region for region in IncludedAreas.valid_keys if region not in self.options.included_areas.value])
|
||||
self.options.boss_goal_count.value = min(self.options.boss_goal_count.value,
|
||||
6 - len([region for region in self.excluded_regions if region != "Battlefield"]))
|
||||
|
||||
create_regions(self)
|
||||
connect_regions(self)
|
||||
if not self.options.infinite_keys:
|
||||
self.lock_item("Valley of Fire - Key 1", "Key")
|
||||
self.lock_item("Valley of Fire - Key 5", "Key")
|
||||
|
||||
if not self.options.obelisks:
|
||||
self.lock_item("Valley of Fire - Obelisk", "Valley of Fire Obelisk")
|
||||
self.lock_item("Dagger Peak - Obelisk", "Dagger Peak Obelisk")
|
||||
self.lock_item("Cliffs of Desolation - Obelisk", "Cliffs of Desolation Obelisk")
|
||||
self.lock_item("Castle Courtyard - Obelisk", "Castle Courtyard Obelisk")
|
||||
self.lock_item("Dungeon of Torment - Obelisk", "Dungeon of Torment Obelisk")
|
||||
self.lock_item("Poisoned Fields - Obelisk", "Poisoned Fields Obelisk")
|
||||
self.lock_item("Haunted Cemetery - Obelisk", "Haunted Cemetery Obelisk")
|
||||
|
||||
if not self.options.mirror_shards:
|
||||
self.lock_item("Dragon's Lair - Dragon Mirror Shard", "Dragon Mirror Shard")
|
||||
self.lock_item("Chimera's Keep - Chimera Mirror Shard", "Chimera Mirror Shard")
|
||||
self.lock_item("Vat of the Plague Fiend - Plague Fiend Mirror Shard", "Plague Fiend Mirror Shard")
|
||||
self.lock_item("Yeti's Cavern - Yeti Mirror Shard", "Yeti Mirror Shard")
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
characters = [
|
||||
self.options.unlock_character_one.value,
|
||||
self.options.unlock_character_two.value,
|
||||
self.options.unlock_character_three.value,
|
||||
self.options.unlock_character_four.value,
|
||||
]
|
||||
chests_barrels = self.options.chests_barrels.value
|
||||
return {
|
||||
"chests": int(chests_barrels == 3 or chests_barrels == 1),
|
||||
"barrels": int(chests_barrels == 3 or chests_barrels == 2),
|
||||
"speed": self.options.permanent_speed.value,
|
||||
"keys": self.options.infinite_keys.value,
|
||||
"characters": characters,
|
||||
"max": self.options.max_difficulty.value,
|
||||
"instant_max": self.options.instant_max.value,
|
||||
"death_link": self.options.death_link.value,
|
||||
"portals": self.options.portals.value,
|
||||
"included_areas": [area for area in IncludedAreas.valid_keys if area in self.options.included_areas.value],
|
||||
"mirror_shards": self.options.mirror_shards.value,
|
||||
"obelisks": self.options.obelisks.value,
|
||||
"goal": self.options.goal.value,
|
||||
"boss_goal_count": self.options.boss_goal_count.value,
|
||||
}
|
||||
|
||||
def create_items(self) -> None:
|
||||
# First add in all progression and useful items
|
||||
required_items = []
|
||||
precollected = [item for item in item_list if item.item_name in [item.name for item in self.multiworld.precollected_items[self.player]]]
|
||||
skipped_items = set()
|
||||
items_required_count = len(self.multiworld.get_unfilled_locations(self.player))
|
||||
if self.options.infinite_keys:
|
||||
skipped_items.add("Key")
|
||||
if self.options.permanent_speed:
|
||||
skipped_items.add("Speed Boots")
|
||||
skipped_items.update([item for item in IncludedTraps.valid_keys if item not in self.options.included_traps.value])
|
||||
if not self.options.obelisks:
|
||||
skipped_items.update([obelisk for obelisk in obelisks if obelisk not in self.unlockable])
|
||||
if not self.options.mirror_shards:
|
||||
skipped_items.update([shard for shard in mirror_shards if shard not in self.unlockable])
|
||||
if not self.options.portals:
|
||||
skipped_items.update([item for item in portals.keys()])
|
||||
if len(self.excluded_regions) > 0:
|
||||
for region in self.excluded_regions:
|
||||
skipped_items.update(excluded_portals.get(region, []))
|
||||
skipped_items.update(excluded_obelisks.get(region, []))
|
||||
for item in [item_ for item_ in item_list
|
||||
if (ItemClassification.progression in item_.progression
|
||||
or ItemClassification.useful in item_.progression)
|
||||
and item_.item_name not in precollected
|
||||
and item_.item_name not in skipped_items]:
|
||||
freq = item.frequency
|
||||
required_items += [item.item_name for _ in range(freq)]
|
||||
items_required_count -= freq
|
||||
|
||||
self.multiworld.itempool += [self.create_item(item_name) for item_name in required_items]
|
||||
|
||||
# Then, get a random amount of fillers until we have as many items as we have locations
|
||||
filler_items = []
|
||||
for item in [item_ for item_ in item_list
|
||||
if ItemClassification.progression not in item_.progression
|
||||
and ItemClassification.useful not in item_.progression
|
||||
and item_.item_name not in skipped_items]:
|
||||
|
||||
freq = item.frequency
|
||||
if item.item_name == "Anti-Death Halo" and "Death" not in skipped_items and self.options.traps_frequency.value >= 30:
|
||||
freq *= 2
|
||||
|
||||
filler_items += [item.item_name for _ in range(freq)]
|
||||
self.random.shuffle(filler_items)
|
||||
|
||||
if len(self.options.included_traps.value) != 0:
|
||||
traps_cap = items_required_count - len(
|
||||
[location for location in self.multiworld.get_unfilled_locations(self.player) if
|
||||
"no_spawner" in locationName_to_data[location.name].tags])
|
||||
traps_frequency = int(len(self.get_locations()) * (self.options.traps_frequency / 100)) // len(self.options.included_traps.value)
|
||||
traps_frequency = min(traps_frequency, traps_cap // len(self.options.included_traps.value))
|
||||
for item in self.options.included_traps.value:
|
||||
if items_required_count == 0:
|
||||
break
|
||||
self.multiworld.itempool += [self.create_item(item) for _ in range(min(traps_frequency, items_required_count))]
|
||||
items_required_count = max(items_required_count - traps_frequency, 0)
|
||||
|
||||
for i in range(items_required_count):
|
||||
if i < int(items_required_count * (self.options.local_filler_frequency / 100)):
|
||||
if self.multiworld.players > 1:
|
||||
self.items.append(self.create_item(filler_items.pop()))
|
||||
else:
|
||||
self.multiworld.itempool.append(self.create_item(filler_items.pop()))
|
||||
else:
|
||||
self.multiworld.itempool.append(self.create_item(filler_items.pop()))
|
||||
|
||||
|
||||
def set_rules(self) -> None:
|
||||
set_rules(self)
|
||||
self.multiworld.completion_condition[self.player] = goal_conditions(self)
|
||||
|
||||
def pre_fill(self) -> None:
|
||||
local_item_count = len(self.items)
|
||||
unfilled_locations = self.multiworld.get_unfilled_locations(self.player)
|
||||
unfilled_names = {loc.name for loc in unfilled_locations}
|
||||
skipped = get_locations_by_tags("skipped_local")
|
||||
no_obelisk_locs = get_locations_by_tags("no_obelisks")
|
||||
|
||||
# Group available locations by level
|
||||
level_locs = {}
|
||||
for level_id, level in level_locations.items():
|
||||
if level_id & 0x8 == 0x8:
|
||||
continue
|
||||
available = [loc for loc in level if loc.name in unfilled_names and loc.name not in skipped]
|
||||
if available:
|
||||
level_locs[level_id] = sorted(available, key=lambda l: l.difficulty, reverse=True)
|
||||
|
||||
all_available = [loc for locs in level_locs.values() for loc in locs]
|
||||
|
||||
# Calculate target per level, ensuring we don't fill entire regions
|
||||
target_per_level = (local_item_count // len(level_locs)) + 2 if level_locs else 0
|
||||
local_locations = []
|
||||
|
||||
for level_id, locs in level_locs.items():
|
||||
# Leave at least 1 location unfilled per region if possible
|
||||
max_from_level = max(1, len(locs) - 1) if len(locs) > 1 else len(locs)
|
||||
count = min(target_per_level, max_from_level)
|
||||
|
||||
# Split into harder/easier halves
|
||||
mid = len(locs) // 2
|
||||
harder, easier = locs[:mid], locs[mid:]
|
||||
self.random.shuffle(harder)
|
||||
self.random.shuffle(easier)
|
||||
|
||||
# Take 60% from harder, 40% from easier
|
||||
harder_count = int(count * 0.6)
|
||||
easier_count = count - harder_count
|
||||
local_locations.extend(self.get_location(l.name) for l in harder[:harder_count])
|
||||
local_locations.extend(self.get_location(l.name) for l in easier[:easier_count])
|
||||
|
||||
# If still need more, pull from levels with most remaining availability
|
||||
if len(local_locations) < local_item_count:
|
||||
used_names = {loc.name for loc in local_locations}
|
||||
remaining = []
|
||||
for level_id, locs in level_locs.items():
|
||||
available = [l for l in locs if l.name not in used_names]
|
||||
for loc in available:
|
||||
remaining.append((loc, len(available)))
|
||||
|
||||
remaining.sort(key=lambda x: x[1], reverse=True)
|
||||
needed = local_item_count - len(local_locations)
|
||||
local_locations.extend(self.get_location(loc.name) for loc, _ in remaining[:needed])
|
||||
|
||||
# Ensure obelisk accessibility
|
||||
used_names = {loc.name for loc in local_locations}
|
||||
remaining_unfilled = [loc.name for loc in all_available if loc.name not in used_names]
|
||||
|
||||
if remaining_unfilled and all(name in no_obelisk_locs for name in remaining_unfilled):
|
||||
chosen_no_obelisk = [loc for loc in local_locations if loc.name in no_obelisk_locs]
|
||||
unchosen_obelisk_ok = [loc for loc in all_available
|
||||
if loc.name not in used_names and loc.name not in no_obelisk_locs]
|
||||
|
||||
swap_count = min(len(chosen_no_obelisk) // 2, len(unchosen_obelisk_ok))
|
||||
if swap_count > 0:
|
||||
self.random.shuffle(chosen_no_obelisk)
|
||||
self.random.shuffle(unchosen_obelisk_ok)
|
||||
|
||||
for i in range(swap_count):
|
||||
local_locations.remove(chosen_no_obelisk[i])
|
||||
local_locations.append(self.get_location(unchosen_obelisk_ok[i].name))
|
||||
|
||||
# Final shuffle and fill
|
||||
local_locations = local_locations[:local_item_count]
|
||||
self.random.shuffle(self.items)
|
||||
self.random.shuffle(local_locations)
|
||||
fast_fill(self.multiworld, self.items, local_locations)
|
||||
|
||||
def create_item(self, name: str) -> GLItem:
|
||||
item = item_table[name]
|
||||
return GLItem(item.item_name, item.progression, item.id, self.player)
|
||||
|
||||
def lock_item(self, location: str, item_name: str) -> None:
|
||||
item = self.create_item(item_name)
|
||||
if location in self.disabled_locations:
|
||||
self.unlockable.update({item_name})
|
||||
return
|
||||
self.get_location(location).place_locked_item(item)
|
||||
|
||||
def collect(self, state: "CollectionState", item: "Item") -> bool:
|
||||
change = super().collect(state, item)
|
||||
if change and "Runestone" in item.name:
|
||||
state.prog_items[item.player]["stones"] += 1
|
||||
if change and (ItemClassification.progression in item.classification):
|
||||
state.prog_items[item.player]["progression"] += 1
|
||||
return change
|
||||
|
||||
def remove(self, state: "CollectionState", item: "Item") -> bool:
|
||||
change = super().remove(state, item)
|
||||
if change and "Runestone" in item.name:
|
||||
state.prog_items[item.player]["stones"] -= 1
|
||||
if change and (ItemClassification.progression in item.classification):
|
||||
state.prog_items[item.player]["progression"] -= 1
|
||||
return change
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choice(list(filter(lambda item: item.progression == ItemClassification.filler, item_list))).item_name
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
patch = GLProcedurePatch(player=self.player, player_name=self.player_name)
|
||||
write_files(self, patch)
|
||||
rom_path = os.path.join(
|
||||
output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}",
|
||||
)
|
||||
patch.write(rom_path)
|
||||
Reference in New Issue
Block a user