Rogue Legacy: World folder clean up and generation improvements. (#1148)

* Minor cleanup and renaming of some files/functions.

* Rename `LegacyWorld` and `LegacyWeb` to RLWorld and RLWeb.

* Undo accidental change to comment.

* Undo accidental change to comment.

* Restructure Items.py format and combine all tables into one.

* Restructure Locations.py format and combine all tables into one.

* Split boss event items into separate boss entries.

* Remove definitions folder.

* Reformatted __init__.py for Rogue Legacy.

* Allow fairy chests to be disabled.

* Add working prefill logic for early vendors.

* Re-introduce Early Architect setting.

* Revamped rules and regions and can now generate games.

* Fix normal vendors breaking everything.

* Fix early vendor logic and add fairy chest logic to require Dragons or Enchantress + runes.

* Fix issue with duplicate items being created.

* Move event placement into __init__.py and fix duplicate Vendors.

* Tweak weights and spacing.

* Update documentation and include bug report link.

* Fix relative link for template file.

* Increase amount of chest locations in `location_table`.

* Correct a refactor rename gone wrong.

* Remove unused reference in imports.

* Tweak mistake in boss name in place_events.

* English is hard.

* Tweak some lines in __init__.py to use `.settings()` method.

* Add unique id tests for Rogue Legacy.

IDs are mixed around, so let's try to avoid accidentally using the same identifier twice.

* Fix typo in doc.

* Simplify `fill_slot_data`.

* Change prefix on `_place_events` to maintain convention.

* Remove items that are **not** progression from rules.
This commit is contained in:
Zach Parks 2022-10-29 22:15:06 -05:00 committed by GitHub
parent 09d8c4b912
commit 1cad51b1af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 636 additions and 728 deletions

View File

@ -0,0 +1,23 @@
from typing import Dict
from . import RLTestBase
from worlds.rogue_legacy.Items import RLItemData, item_table
from worlds.rogue_legacy.Locations import RLLocationData, location_table
class UniqueTest(RLTestBase):
@staticmethod
def test_item_ids_are_all_unique():
item_ids: Dict[int, str] = {}
for name, data in item_table.items():
assert data.code not in item_ids.keys(), f"'{name}': {data.code}, is not unique. " \
f"'{item_ids[data.code]}' also has this identifier."
item_ids[data.code] = name
@staticmethod
def test_location_ids_are_all_unique():
location_ids: Dict[int, str] = {}
for name, data in location_table.items():
assert data.code not in location_ids.keys(), f"'{name}': {data.code}, is not unique. " \
f"'{location_ids[data.code]}' also has this identifier."
location_ids[data.code] = name

View File

@ -0,0 +1,5 @@
from test.worlds.test_base import WorldTestBase
class RLTestBase(WorldTestBase):
game = "Rogue Legacy"

View File

@ -1,136 +1,115 @@
import typing
from typing import Dict, NamedTuple, Optional
from BaseClasses import Item
from .Names import ItemName
from BaseClasses import Item, ItemClassification
class ItemData(typing.NamedTuple):
code: typing.Optional[int]
progression: bool
quantity: int = 1
event: bool = False
class LegacyItem(Item):
class RLItem(Item):
game: str = "Rogue Legacy"
# Separate tables for each type of item.
vendors_table = {
ItemName.blacksmith: ItemData(90000, True),
ItemName.enchantress: ItemData(90001, True),
ItemName.architect: ItemData(90002, False),
class RLItemData(NamedTuple):
category: str
code: Optional[int] = None
classification: ItemClassification = ItemClassification.filler
max_quantity: int = 1
weight: int = 1
@property
def is_event_item(self):
return self.code is None
def get_items_by_category(category: str) -> Dict[str, RLItemData]:
item_dict: Dict[str, RLItemData] = {}
for name, data in item_table.items():
if data.category == category:
item_dict.setdefault(name, data)
return item_dict
item_table: Dict[str, RLItemData] = {
# Vendors
"Blacksmith": RLItemData("Vendors", 90_000, ItemClassification.useful),
"Enchantress": RLItemData("Vendors", 90_001, ItemClassification.progression),
"Architect": RLItemData("Vendors", 90_002, ItemClassification.useful),
# Classes
"Progressive Knights": RLItemData("Classes", 90_003, ItemClassification.useful, 2),
"Progressive Mages": RLItemData("Classes", 90_004, ItemClassification.useful, 2),
"Progressive Barbarians": RLItemData("Classes", 90_005, ItemClassification.useful, 2),
"Progressive Knaves": RLItemData("Classes", 90_006, ItemClassification.useful, 2),
"Progressive Shinobis": RLItemData("Classes", 90_007, ItemClassification.useful, 2),
"Progressive Miners": RLItemData("Classes", 90_008, ItemClassification.useful, 2),
"Progressive Liches": RLItemData("Classes", 90_009, ItemClassification.useful, 2),
"Progressive Spellthieves": RLItemData("Classes", 90_010, ItemClassification.useful, 2),
"Dragons": RLItemData("Classes", 90_096, ItemClassification.progression),
"Traitors": RLItemData("Classes", 90_097, ItemClassification.useful),
# Skills
"Health Up": RLItemData("Skills", 90_013, ItemClassification.progression_skip_balancing, 15),
"Mana Up": RLItemData("Skills", 90_014, ItemClassification.progression_skip_balancing, 15),
"Attack Up": RLItemData("Skills", 90_015, ItemClassification.progression_skip_balancing, 15),
"Magic Damage Up": RLItemData("Skills", 90_016, ItemClassification.progression_skip_balancing, 15),
"Armor Up": RLItemData("Skills", 90_017, ItemClassification.useful, 15),
"Equip Up": RLItemData("Skills", 90_018, ItemClassification.useful, 5),
"Crit Chance Up": RLItemData("Skills", 90_019, ItemClassification.useful, 5),
"Crit Damage Up": RLItemData("Skills", 90_020, ItemClassification.useful, 5),
"Down Strike Up": RLItemData("Skills", 90_021),
"Gold Gain Up": RLItemData("Skills", 90_022),
"Potion Efficiency Up": RLItemData("Skills", 90_023),
"Invulnerability Time Up": RLItemData("Skills", 90_024),
"Mana Cost Down": RLItemData("Skills", 90_025),
"Death Defiance": RLItemData("Skills", 90_026, ItemClassification.useful),
"Haggling": RLItemData("Skills", 90_027, ItemClassification.useful),
"Randomize Children": RLItemData("Skills", 90_028, ItemClassification.useful),
# Blueprints
"Progressive Blueprints": RLItemData("Blueprints", 90_055, ItemClassification.useful, 15),
"Squire Blueprints": RLItemData("Blueprints", 90_040, ItemClassification.useful),
"Silver Blueprints": RLItemData("Blueprints", 90_041, ItemClassification.useful),
"Guardian Blueprints": RLItemData("Blueprints", 90_042, ItemClassification.useful),
"Imperial Blueprints": RLItemData("Blueprints", 90_043, ItemClassification.useful),
"Royal Blueprints": RLItemData("Blueprints", 90_044, ItemClassification.useful),
"Knight Blueprints": RLItemData("Blueprints", 90_045, ItemClassification.useful),
"Ranger Blueprints": RLItemData("Blueprints", 90_046, ItemClassification.useful),
"Sky Blueprints": RLItemData("Blueprints", 90_047, ItemClassification.useful),
"Dragon Blueprints": RLItemData("Blueprints", 90_048, ItemClassification.useful),
"Slayer Blueprints": RLItemData("Blueprints", 90_049, ItemClassification.useful),
"Blood Blueprints": RLItemData("Blueprints", 90_050, ItemClassification.useful),
"Sage Blueprints": RLItemData("Blueprints", 90_051, ItemClassification.useful),
"Retribution Blueprints": RLItemData("Blueprints", 90_052, ItemClassification.useful),
"Holy Blueprints": RLItemData("Blueprints", 90_053, ItemClassification.useful),
"Dark Blueprints": RLItemData("Blueprints", 90_054, ItemClassification.useful),
# Runes
"Vault Runes": RLItemData("Runes", 90_060, ItemClassification.progression),
"Sprint Runes": RLItemData("Runes", 90_061, ItemClassification.progression),
"Vampire Runes": RLItemData("Runes", 90_062, ItemClassification.useful),
"Sky Runes": RLItemData("Runes", 90_063, ItemClassification.progression),
"Siphon Runes": RLItemData("Runes", 90_064, ItemClassification.useful),
"Retaliation Runes": RLItemData("Runes", 90_065),
"Bounty Runes": RLItemData("Runes", 90_066),
"Haste Runes": RLItemData("Runes", 90_067),
"Curse Runes": RLItemData("Runes", 90_068),
"Grace Runes": RLItemData("Runes", 90_069),
"Balance Runes": RLItemData("Runes", 90_070, ItemClassification.useful),
# Junk
"Triple Stat Increase": RLItemData("Filler", 90_030, weight=6),
"1000 Gold": RLItemData("Filler", 90_031, weight=3),
"3000 Gold": RLItemData("Filler", 90_032, weight=2),
"5000 Gold": RLItemData("Filler", 90_033, weight=1),
}
static_classes_table = {
ItemName.knight: ItemData(90080, False),
ItemName.paladin: ItemData(90081, False),
ItemName.mage: ItemData(90082, False),
ItemName.archmage: ItemData(90083, False),
ItemName.barbarian: ItemData(90084, False),
ItemName.barbarian_king: ItemData(90085, False),
ItemName.knave: ItemData(90086, False),
ItemName.assassin: ItemData(90087, False),
ItemName.shinobi: ItemData(90088, False),
ItemName.hokage: ItemData(90089, False),
ItemName.miner: ItemData(90090, False),
ItemName.spelunker: ItemData(90091, False),
ItemName.lich: ItemData(90092, False),
ItemName.lich_king: ItemData(90093, False),
ItemName.spellthief: ItemData(90094, False),
ItemName.spellsword: ItemData(90095, False),
ItemName.dragon: ItemData(90096, False),
ItemName.traitor: ItemData(90097, False),
event_item_table: Dict[str, RLItemData] = {
"Defeat Khidr": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Alexander": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Ponce de Leon": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Herodotus": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Neo Khidr": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Alexander IV": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Ponce de Freon": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Astrodotus": RLItemData("Event", classification=ItemClassification.progression),
"Defeat The Fountain": RLItemData("Event", classification=ItemClassification.progression),
}
progressive_classes_table = {
ItemName.progressive_knight: ItemData(90003, False, 2),
ItemName.progressive_mage: ItemData(90004, False, 2),
ItemName.progressive_barbarian: ItemData(90005, False, 2),
ItemName.progressive_knave: ItemData(90006, False, 2),
ItemName.progressive_shinobi: ItemData(90007, False, 2),
ItemName.progressive_miner: ItemData(90008, False, 2),
ItemName.progressive_lich: ItemData(90009, False, 2),
ItemName.progressive_spellthief: ItemData(90010, False, 2),
}
configurable_skill_unlocks_table = {
ItemName.health: ItemData(90013, True, 15),
ItemName.mana: ItemData(90014, True, 15),
ItemName.attack: ItemData(90015, True, 15),
ItemName.magic_damage: ItemData(90016, True, 15),
ItemName.armor: ItemData(90017, True, 10),
ItemName.equip: ItemData(90018, True, 10),
ItemName.crit_chance: ItemData(90019, False, 5),
ItemName.crit_damage: ItemData(90020, False, 5),
}
skill_unlocks_table = {
ItemName.down_strike: ItemData(90021, False),
ItemName.gold_gain: ItemData(90022, False),
ItemName.potion_efficiency: ItemData(90023, False),
ItemName.invulnerability_time: ItemData(90024, False),
ItemName.mana_cost_down: ItemData(90025, False),
ItemName.death_defiance: ItemData(90026, False),
ItemName.haggling: ItemData(90027, False),
ItemName.random_children: ItemData(90028, False),
}
blueprints_table = {
ItemName.squire_blueprints: ItemData(90040, False),
ItemName.silver_blueprints: ItemData(90041, False),
ItemName.guardian_blueprints: ItemData(90042, False),
ItemName.imperial_blueprints: ItemData(90043, False),
ItemName.royal_blueprints: ItemData(90044, False),
ItemName.knight_blueprints: ItemData(90045, False),
ItemName.ranger_blueprints: ItemData(90046, False),
ItemName.sky_blueprints: ItemData(90047, False),
ItemName.dragon_blueprints: ItemData(90048, False),
ItemName.slayer_blueprints: ItemData(90049, False),
ItemName.blood_blueprints: ItemData(90050, False),
ItemName.sage_blueprints: ItemData(90051, False),
ItemName.retribution_blueprints: ItemData(90052, False),
ItemName.holy_blueprints: ItemData(90053, False),
ItemName.dark_blueprints: ItemData(90054, False),
}
progressive_blueprint_table = {
ItemName.progressive_blueprints: ItemData(90055, False),
}
runes_table = {
ItemName.vault_runes: ItemData(90060, False),
ItemName.sprint_runes: ItemData(90061, False),
ItemName.vampire_runes: ItemData(90062, False),
ItemName.sky_runes: ItemData(90063, False),
ItemName.siphon_runes: ItemData(90064, False),
ItemName.retaliation_runes: ItemData(90065, False),
ItemName.bounty_runes: ItemData(90066, False),
ItemName.haste_runes: ItemData(90067, False),
ItemName.curse_runes: ItemData(90068, False),
ItemName.grace_runes: ItemData(90069, False),
ItemName.balance_runes: ItemData(90070, False),
}
misc_items_table = {
ItemName.trip_stat_increase: ItemData(90030, False),
ItemName.gold_1000: ItemData(90031, False),
ItemName.gold_3000: ItemData(90032, False),
ItemName.gold_5000: ItemData(90033, False),
# ItemName.rage_trap: ItemData(90034, False),
}
# Complete item table.
item_table = {
**vendors_table,
**static_classes_table,
**progressive_classes_table,
**configurable_skill_unlocks_table,
**skill_unlocks_table,
**blueprints_table,
**progressive_blueprint_table,
**runes_table,
**misc_items_table,
}
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}

View File

@ -1,90 +1,98 @@
import typing
from typing import Dict, NamedTuple, Optional
from BaseClasses import Location
from .Names import LocationName
class LegacyLocation(Location):
class RLLocation(Location):
game: str = "Rogue Legacy"
base_location_table = {
# Manor Renovations
LocationName.manor_ground_base: 91000,
LocationName.manor_main_base: 91001,
LocationName.manor_main_bottom_window: 91002,
LocationName.manor_main_top_window: 91003,
LocationName.manor_main_roof: 91004,
LocationName.manor_left_wing_base: 91005,
LocationName.manor_left_wing_window: 91006,
LocationName.manor_left_wing_roof: 91007,
LocationName.manor_left_big_base: 91008,
LocationName.manor_left_big_upper1: 91009,
LocationName.manor_left_big_upper2: 91010,
LocationName.manor_left_big_windows: 91011,
LocationName.manor_left_big_roof: 91012,
LocationName.manor_left_far_base: 91013,
LocationName.manor_left_far_roof: 91014,
LocationName.manor_left_extension: 91015,
LocationName.manor_left_tree1: 91016,
LocationName.manor_left_tree2: 91017,
LocationName.manor_right_wing_base: 91018,
LocationName.manor_right_wing_window: 91019,
LocationName.manor_right_wing_roof: 91020,
LocationName.manor_right_big_base: 91021,
LocationName.manor_right_big_upper: 91022,
LocationName.manor_right_big_roof: 91023,
LocationName.manor_right_high_base: 91024,
LocationName.manor_right_high_upper: 91025,
LocationName.manor_right_high_tower: 91026,
LocationName.manor_right_extension: 91027,
LocationName.manor_right_tree: 91028,
LocationName.manor_observatory_base: 91029,
LocationName.manor_observatory_scope: 91030,
class RLLocationData(NamedTuple):
category: str
code: Optional[int] = None
@property
def is_event_location(self):
return self.code is None
def get_locations_by_category(category: str) -> Dict[str, RLLocationData]:
location_dict: Dict[str, RLLocationData] = {}
for name, data in location_table.items():
if data.category == category:
location_dict.setdefault(name, data)
return location_dict
location_table: Dict[str, RLLocationData] = {
# Manor Renovation
"Manor - Ground Road": RLLocationData("Manor", 91_000),
"Manor - Main Base": RLLocationData("Manor", 91_001),
"Manor - Main Bottom Window": RLLocationData("Manor", 91_002),
"Manor - Main Top Window": RLLocationData("Manor", 91_003),
"Manor - Main Rooftop": RLLocationData("Manor", 91_004),
"Manor - Left Wing Base": RLLocationData("Manor", 91_005),
"Manor - Left Wing Window": RLLocationData("Manor", 91_006),
"Manor - Left Wing Rooftop": RLLocationData("Manor", 91_007),
"Manor - Left Big Base": RLLocationData("Manor", 91_008),
"Manor - Left Big Upper 1": RLLocationData("Manor", 91_009),
"Manor - Left Big Upper 2": RLLocationData("Manor", 91_010),
"Manor - Left Big Windows": RLLocationData("Manor", 91_011),
"Manor - Left Big Rooftop": RLLocationData("Manor", 91_012),
"Manor - Left Far Base": RLLocationData("Manor", 91_013),
"Manor - Left Far Roof": RLLocationData("Manor", 91_014),
"Manor - Left Extension": RLLocationData("Manor", 91_015),
"Manor - Left Tree 1": RLLocationData("Manor", 91_016),
"Manor - Left Tree 2": RLLocationData("Manor", 91_017),
"Manor - Right Wing Base": RLLocationData("Manor", 91_018),
"Manor - Right Wing Window": RLLocationData("Manor", 91_019),
"Manor - Right Wing Rooftop": RLLocationData("Manor", 91_020),
"Manor - Right Big Base": RLLocationData("Manor", 91_021),
"Manor - Right Big Upper": RLLocationData("Manor", 91_022),
"Manor - Right Big Rooftop": RLLocationData("Manor", 91_023),
"Manor - Right High Base": RLLocationData("Manor", 91_024),
"Manor - Right High Upper": RLLocationData("Manor", 91_025),
"Manor - Right High Tower": RLLocationData("Manor", 91_026),
"Manor - Right Extension": RLLocationData("Manor", 91_027),
"Manor - Right Tree": RLLocationData("Manor", 91_028),
"Manor - Observatory Base": RLLocationData("Manor", 91_029),
"Manor - Observatory Telescope": RLLocationData("Manor", 91_030),
# Boss Rewards
LocationName.boss_castle: 91100,
LocationName.boss_forest: 91102,
LocationName.boss_tower: 91104,
LocationName.boss_dungeon: 91106,
# Special Rooms
LocationName.special_jukebox: 91200,
LocationName.special_painting: 91201,
LocationName.special_cheapskate: 91202,
LocationName.special_carnival: 91203,
"Castle Hamson Boss Reward": RLLocationData("Boss", 91_100),
"Forest Abkhazia Boss Reward": RLLocationData("Boss", 91_102),
"The Maya Boss Reward": RLLocationData("Boss", 91_104),
"Land of Darkness Boss Reward": RLLocationData("Boss", 91_106),
# Special Locations
LocationName.castle: None,
LocationName.garden: None,
LocationName.tower: None,
LocationName.dungeon: None,
LocationName.fountain: None,
"Jukebox": RLLocationData("Special", 91_200),
"Painting": RLLocationData("Special", 91_201),
"Cheapskate Elf's Game": RLLocationData("Special", 91_202),
"Carnival": RLLocationData("Special", 91_203),
# Diaries
**{f"Diary {i+1}": RLLocationData("Diary", 91_300 + i) for i in range(0, 25)},
# Chests
**{f"Castle Hamson - Chest {i+1}": RLLocationData("Chests", 91_600 + i) for i in range(0, 50)},
**{f"Forest Abkhazia - Chest {i+1}": RLLocationData("Chests", 91_700 + i) for i in range(0, 50)},
**{f"The Maya - Chest {i+1}": RLLocationData("Chests", 91_800 + i) for i in range(0, 50)},
**{f"Land of Darkness - Chest {i+1}": RLLocationData("Chests", 91_900 + i) for i in range(0, 50)},
**{f"Chest {i+1}": RLLocationData("Chests", 92_000 + i) for i in range(0, 200)},
# Fairy Chests
**{f"Castle Hamson - Fairy Chest {i+1}": RLLocationData("Fairies", 91_400 + i) for i in range(0, 15)},
**{f"Forest Abkhazia - Fairy Chest {i+1}": RLLocationData("Fairies", 91_450 + i) for i in range(0, 15)},
**{f"The Maya - Fairy Chest {i+1}": RLLocationData("Fairies", 91_500 + i) for i in range(0, 15)},
**{f"Land of Darkness - Fairy Chest {i+1}": RLLocationData("Fairies", 91_550 + i) for i in range(0, 15)},
**{f"Fairy Chest {i+1}": RLLocationData("Fairies", 92_200 + i) for i in range(0, 60)},
}
diary_location_table = {f"{LocationName.diary} {i + 1}": i + 91300 for i in range(0, 25)}
fairy_chest_location_table = {
**{f"{LocationName.castle} - Fairy Chest {i + 1}": i + 91400 for i in range(0, 50)},
**{f"{LocationName.garden} - Fairy Chest {i + 1}": i + 91450 for i in range(0, 50)},
**{f"{LocationName.tower} - Fairy Chest {i + 1}": i + 91500 for i in range(0, 50)},
**{f"{LocationName.dungeon} - Fairy Chest {i + 1}": i + 91550 for i in range(0, 50)},
**{f"Fairy Chest {i + 1}": i + 92200 for i in range(0, 60)},
event_location_table: Dict[str, RLLocationData] = {
"Castle Hamson Boss Room": RLLocationData("Event"),
"Forest Abkhazia Boss Room": RLLocationData("Event"),
"The Maya Boss Room": RLLocationData("Event"),
"Land of Darkness Boss Room": RLLocationData("Event"),
"Fountain Room": RLLocationData("Event"),
}
chest_location_table = {
**{f"{LocationName.castle} - Chest {i + 1}": i + 91600 for i in range(0, 100)},
**{f"{LocationName.garden} - Chest {i + 1}": i + 91700 for i in range(0, 100)},
**{f"{LocationName.tower} - Chest {i + 1}": i + 91800 for i in range(0, 100)},
**{f"{LocationName.dungeon} - Chest {i + 1}": i + 91900 for i in range(0, 100)},
**{f"Chest {i + 1}": i + 92000 for i in range(0, 120)},
}
location_table = {
**base_location_table,
**diary_location_table,
**fairy_chest_location_table,
**chest_location_table,
}
lookup_id_to_name: typing.Dict[int, str] = {id: name for name, _ in location_table.items()}

View File

@ -1,97 +0,0 @@
# Vendor Definitions
blacksmith = "Blacksmith"
enchantress = "Enchantress"
architect = "Architect"
# Progressive Class Definitions
progressive_knight = "Progressive Knights"
progressive_mage = "Progressive Mages"
progressive_barbarian = "Progressive Barbarians"
progressive_knave = "Progressive Knaves"
progressive_shinobi = "Progressive Shinobis"
progressive_miner = "Progressive Miners"
progressive_lich = "Progressive Liches"
progressive_spellthief = "Progressive Spellthieves"
# Static Class Definitions
knight = "Knights"
paladin = "Paladins"
mage = "Mages"
archmage = "Archmages"
barbarian = "Barbarians"
barbarian_king = "Barbarian Kings"
knave = "Knaves"
assassin = "Assassins"
shinobi = "Shinobis"
hokage = "Hokages"
miner = "Miners"
spelunker = "Spelunkers"
lich = "Lichs"
lich_king = "Lich Kings"
spellthief = "Spellthieves"
spellsword = "Spellswords"
dragon = "Dragons"
traitor = "Traitors"
# Skill Unlock Definitions
health = "Health Up"
mana = "Mana Up"
attack = "Attack Up"
magic_damage = "Magic Damage Up"
armor = "Armor Up"
equip = "Equip Up"
crit_chance = "Crit Chance Up"
crit_damage = "Crit Damage Up"
down_strike = "Down Strike Up"
gold_gain = "Gold Gain Up"
potion_efficiency = "Potion Efficiency Up"
invulnerability_time = "Invulnerability Time Up"
mana_cost_down = "Mana Cost Down"
death_defiance = "Death Defiance"
haggling = "Haggling"
random_children = "Randomize Children"
# Misc. Definitions
trip_stat_increase = "Triple Stat Increase"
gold_1000 = "1000 Gold"
gold_3000 = "3000 Gold"
gold_5000 = "5000 Gold"
rage_trap = "Rage Trap"
# Blueprint Definitions
progressive_blueprints = "Progressive Blueprints"
squire_blueprints = "Squire Blueprints"
silver_blueprints = "Silver Blueprints"
guardian_blueprints = "Guardian Blueprints"
imperial_blueprints = "Imperial Blueprints"
royal_blueprints = "Royal Blueprints"
knight_blueprints = "Knight Blueprints"
ranger_blueprints = "Ranger Blueprints"
sky_blueprints = "Sky Blueprints"
dragon_blueprints = "Dragon Blueprints"
slayer_blueprints = "Slayer Blueprints"
blood_blueprints = "Blood Blueprints"
sage_blueprints = "Sage Blueprints"
retribution_blueprints = "Retribution Blueprints"
holy_blueprints = "Holy Blueprints"
dark_blueprints = "Dark Blueprints"
# Rune Definitions
vault_runes = "Vault Runes"
sprint_runes = "Sprint Runes"
vampire_runes = "Vampire Runes"
sky_runes = "Sky Runes"
siphon_runes = "Siphon Runes"
retaliation_runes = "Retaliation Runes"
bounty_runes = "Bounty Runes"
haste_runes = "Haste Runes"
curse_runes = "Curse Runes"
grace_runes = "Grace Runes"
balance_runes = "Balance Runes"
# Event Definitions
boss_castle = "Defeat Castle Hamson Boss"
boss_forest = "Defeat Forest Abkhazia Boss"
boss_tower = "Defeat The Maya Boss"
boss_dungeon = "Defeat The Land of Darkness Boss"
boss_fountain = "Defeat The Fountain"

View File

@ -1,55 +0,0 @@
# Manor Piece Definitions
manor_ground_base = "Manor Renovation - Ground Road"
manor_main_base = "Manor Renovation - Main Base"
manor_main_bottom_window = "Manor Renovation - Main Bottom Window"
manor_main_top_window = "Manor Renovation - Main Top Window"
manor_main_roof = "Manor Renovation - Main Rooftop"
manor_left_wing_base = "Manor Renovation - Left Wing Base"
manor_left_wing_window = "Manor Renovation - Left Wing Window"
manor_left_wing_roof = "Manor Renovation - Left Wing Rooftop"
manor_left_big_base = "Manor Renovation - Left Big Base"
manor_left_big_upper1 = "Manor Renovation - Left Big Upper 1"
manor_left_big_upper2 = "Manor Renovation - Left Big Upper 2"
manor_left_big_windows = "Manor Renovation - Left Big Windows"
manor_left_big_roof = "Manor Renovation - Left Big Rooftop"
manor_left_far_base = "Manor Renovation - Left Far Base"
manor_left_far_roof = "Manor Renovation - Left Far Roof"
manor_left_extension = "Manor Renovation - Left Extension"
manor_left_tree1 = "Manor Renovation - Left Tree 1"
manor_left_tree2 = "Manor Renovation - Left Tree 2"
manor_right_wing_base = "Manor Renovation - Right Wing Base"
manor_right_wing_window = "Manor Renovation - Right Wing Window"
manor_right_wing_roof = "Manor Renovation - Right Wing Rooftop"
manor_right_big_base = "Manor Renovation - Right Big Base"
manor_right_big_upper = "Manor Renovation - Right Big Upper"
manor_right_big_roof = "Manor Renovation - Right Big Rooftop"
manor_right_high_base = "Manor Renovation - Right High Base"
manor_right_high_upper = "Manor Renovation - Right High Upper"
manor_right_high_tower = "Manor Renovation - Right High Tower"
manor_right_extension = "Manor Renovation - Right Extension"
manor_right_tree = "Manor Renovation - Right Tree"
manor_observatory_base = "Manor Renovation - Observatory Base"
manor_observatory_scope = "Manor Renovation - Observatory Telescope"
# Boss Chest Definitions
boss_castle = "Castle Hamson Boss"
boss_forest = "Forest Abkhazia Boss"
boss_tower = "The Maya Boss"
boss_dungeon = "The Land of Darkness Boss"
# Special Room Definitions
special_jukebox = "Jukebox"
special_painting = "Painting"
special_cheapskate = "Cheapskate Elf's Game"
special_carnival = "Carnival"
# Shorthand Definitions
diary = "Diary"
# Region Definitions
outside = "Outside Castle Hamson"
castle = "Castle Hamson"
garden = "Forest Abkhazia"
tower = "The Maya"
dungeon = "The Land of Darkness"
fountain = "Fountain Room"

View File

@ -1,6 +1,6 @@
import typing
from typing import Dict
from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList, OptionSet
from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionSet
class StartingGender(Choice):
@ -63,9 +63,9 @@ class FairyChestsPerZone(Range):
bonuses can be found in Fairy Chests.
"""
display_name = "Fairy Chests Per Zone"
range_start = 5
range_start = 0
range_end = 15
default = 5
default = 1
class ChestsPerZone(Range):
@ -74,9 +74,9 @@ class ChestsPerZone(Range):
gold or stat bonuses can be found in Chests.
"""
display_name = "Chests Per Zone"
range_start = 15
range_end = 30
default = 15
range_start = 20
range_end = 50
default = 20
class UniversalFairyChests(Toggle):
@ -111,8 +111,10 @@ class Architect(Choice):
"""
display_name = "Architect"
option_start_unlocked = 0
option_normal = 2
option_early = 1
option_anywhere = 2
option_disabled = 3
alias_normal = 2
default = 2
@ -173,7 +175,7 @@ class NumberOfChildren(Range):
default = 3
class AdditionalNames(OptionList):
class AdditionalNames(OptionSet):
"""
Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list
of names your children can have. The first value will also be your initial character's name depending on Starting
@ -337,7 +339,8 @@ class AvailableClasses(OptionSet):
default = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"}
valid_keys = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"}
legacy_options: typing.Dict[str, type(Option)] = {
rl_options: Dict[str, type(Option)] = {
"starting_gender": StartingGender,
"starting_class": StartingClass,
"available_classes": AvailableClasses,

View File

@ -1,72 +1,125 @@
import typing
from typing import Dict, List, NamedTuple, Optional
from BaseClasses import MultiWorld, Region, RegionType, Entrance, ItemClassification
from .Items import LegacyItem
from .Locations import LegacyLocation, diary_location_table, location_table, base_location_table
from .Names import LocationName, ItemName
prog = ItemClassification.progression
from BaseClasses import MultiWorld, Region, RegionType, Entrance
from .Items import RLItem
from .Locations import RLLocation, location_table, get_locations_by_category
def create_regions(world, player: int):
class RLRegionData(NamedTuple):
locations: Optional[List[str]]
exits: Optional[List[str]]
locations: typing.List[str] = []
# Add required locations.
locations += [location for location in base_location_table]
locations += [location for location in diary_location_table]
def create_regions(world: MultiWorld, player: int):
regions: Dict[str, RLRegionData] = {
"Menu": RLRegionData(None, ["Castle Hamson"]),
"The Manor": RLRegionData([], []),
"Castle Hamson": RLRegionData([], ["Forest Abkhazia",
"The Maya",
"Land of Darkness",
"The Fountain Room",
"The Manor"]),
"Forest Abkhazia": RLRegionData([], []),
"The Maya": RLRegionData([], []),
"Land of Darkness": RLRegionData([], []),
"The Fountain Room": RLRegionData([], None),
}
# Add chests per settings.
if world.universal_fairy_chests[player]:
fairies = int(world.fairy_chests_per_zone[player]) * 4
for i in range(0, fairies):
locations += [f"Fairy Chest {i + 1}"]
else:
fairies = int(world.fairy_chests_per_zone[player])
for i in range(0, fairies):
locations += [f"{LocationName.castle} - Fairy Chest {i + 1}"]
locations += [f"{LocationName.garden} - Fairy Chest {i + 1}"]
locations += [f"{LocationName.tower} - Fairy Chest {i + 1}"]
locations += [f"{LocationName.dungeon} - Fairy Chest {i + 1}"]
# Diaries
for diary in range(0, 25):
region: str
if 0 <= diary < 6:
region = "Castle Hamson"
elif 6 <= diary < 12:
region = "Forest Abkhazia"
elif 12 <= diary < 18:
region = "The Maya"
elif 18 <= diary < 24:
region = "Land of Darkness"
else:
region = "The Fountain Room"
if world.universal_chests[player]:
chests = int(world.chests_per_zone[player]) * 4
for i in range(0, chests):
locations += [f"Chest {i + 1}"]
else:
chests = int(world.chests_per_zone[player])
for i in range(0, chests):
locations += [f"{LocationName.castle} - Chest {i + 1}"]
locations += [f"{LocationName.garden} - Chest {i + 1}"]
locations += [f"{LocationName.tower} - Chest {i + 1}"]
locations += [f"{LocationName.dungeon} - Chest {i + 1}"]
regions[region].locations.append(f"Diary {diary + 1}")
# Manor & Special
for manor in get_locations_by_category("Manor").keys():
regions["The Manor"].locations.append(manor)
for special in get_locations_by_category("Special").keys():
regions["Castle Hamson"].locations.append(special)
# Boss Rewards
regions["Castle Hamson"].locations.append("Castle Hamson Boss Reward")
regions["Forest Abkhazia"].locations.append("Forest Abkhazia Boss Reward")
regions["The Maya"].locations.append("The Maya Boss Reward")
regions["Land of Darkness"].locations.append("Land of Darkness Boss Reward")
# Events
regions["Castle Hamson"].locations.append("Castle Hamson Boss Room")
regions["Forest Abkhazia"].locations.append("Forest Abkhazia Boss Room")
regions["The Maya"].locations.append("The Maya Boss Room")
regions["Land of Darkness"].locations.append("Land of Darkness Boss Room")
regions["The Fountain Room"].locations.append("Fountain Room")
# Chests
chests = int(world.chests_per_zone[player])
for i in range(0, chests):
if world.universal_chests[player]:
regions["Castle Hamson"].locations.append(f"Chest {i + 1}")
regions["Forest Abkhazia"].locations.append(f"Chest {i + 1 + chests}")
regions["The Maya"].locations.append(f"Chest {i + 1 + (chests * 2)}")
regions["Land of Darkness"].locations.append(f"Chest {i + 1 + (chests * 3)}")
else:
regions["Castle Hamson"].locations.append(f"Castle Hamson - Chest {i + 1}")
regions["Forest Abkhazia"].locations.append(f"Forest Abkhazia - Chest {i + 1}")
regions["The Maya"].locations.append(f"The Maya - Chest {i + 1}")
regions["Land of Darkness"].locations.append(f"Land of Darkness - Chest {i + 1}")
# Fairy Chests
chests = int(world.fairy_chests_per_zone[player])
for i in range(0, chests):
if world.universal_fairy_chests[player]:
regions["Castle Hamson"].locations.append(f"Fairy Chest {i + 1}")
regions["Forest Abkhazia"].locations.append(f"Fairy Chest {i + 1 + chests}")
regions["The Maya"].locations.append(f"Fairy Chest {i + 1 + (chests * 2)}")
regions["Land of Darkness"].locations.append(f"Fairy Chest {i + 1 + (chests * 3)}")
else:
regions["Castle Hamson"].locations.append(f"Castle Hamson - Fairy Chest {i + 1}")
regions["Forest Abkhazia"].locations.append(f"Forest Abkhazia - Fairy Chest {i + 1}")
regions["The Maya"].locations.append(f"The Maya - Fairy Chest {i + 1}")
regions["Land of Darkness"].locations.append(f"Land of Darkness - Fairy Chest {i + 1}")
# Set up the regions correctly.
world.regions += [
create_region(world, player, "Menu", None, [LocationName.outside]),
create_region(world, player, LocationName.castle, locations),
]
for name, data in regions.items():
world.regions.append(create_region(world, player, name, data.locations, data.exits))
# Connect entrances and set up events.
world.get_entrance(LocationName.outside, player).connect(world.get_region(LocationName.castle, player))
world.get_location(LocationName.castle, player).place_locked_item(LegacyItem(ItemName.boss_castle, prog, None, player))
world.get_location(LocationName.garden, player).place_locked_item(LegacyItem(ItemName.boss_forest, prog, None, player))
world.get_location(LocationName.tower, player).place_locked_item(LegacyItem(ItemName.boss_tower, prog, None, player))
world.get_location(LocationName.dungeon, player).place_locked_item(LegacyItem(ItemName.boss_dungeon, prog, None, player))
world.get_location(LocationName.fountain, player).place_locked_item(LegacyItem(ItemName.boss_fountain, prog, None, player))
world.get_entrance("Castle Hamson", player).connect(world.get_region("Castle Hamson", player))
world.get_entrance("The Manor", player).connect(world.get_region("The Manor", player))
world.get_entrance("Forest Abkhazia", player).connect(world.get_region("Forest Abkhazia", player))
world.get_entrance("The Maya", player).connect(world.get_region("The Maya", player))
world.get_entrance("Land of Darkness", player).connect(world.get_region("Land of Darkness", player))
world.get_entrance("The Fountain Room", player).connect(world.get_region("The Fountain Room", player))
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
# Shamelessly stolen from the ROR2 definition, lol
ret = Region(name, RegionType.Generic, name, player)
ret.world = world
if locations:
for location in locations:
loc_id = location_table.get(location, 0)
location = LegacyLocation(player, location, loc_id, ret)
for loc_name in locations:
loc_data = location_table.get(loc_name)
location = RLLocation(player, loc_name, loc_data.code if loc_data else None, ret)
# Special rule handling for fairy chests.
if "Fairy" in loc_name:
location.access_rule = lambda state: state.has("Dragons", player) or (
state.has("Enchantress", player) and (
state.has("Vault Runes", player) or
state.has("Sprint Runes", player) or
state.has("Sky Runes", player)))
ret.locations.append(location)
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
entrance = Entrance(player, exit, ret)
ret.exits.append(entrance)
return ret

View File

@ -1,177 +1,86 @@
from BaseClasses import MultiWorld
from .Names import LocationName, ItemName
from BaseClasses import MultiWorld, CollectionState
from ..AutoWorld import LogicMixin
from ..generic.Rules import set_rule
class LegacyLogic(LogicMixin):
def _legacy_has_any_vendors(self, player: int) -> bool:
return self.has_any({ItemName.blacksmith, ItemName.enchantress}, player)
def has_any_vendors(self: CollectionState, player: int) -> bool:
return self.has_any({"Blacksmith", "Enchantress"}, player)
def _legacy_has_all_vendors(self, player: int) -> bool:
return self.has_all({ItemName.blacksmith, ItemName.enchantress}, player)
def has_all_vendors(self: CollectionState, player: int) -> bool:
return self.has_all({"Blacksmith", "Enchantress"}, player)
def _legacy_has_stat_upgrades(self, player: int, amount: int) -> bool:
return self._legacy_stat_upgrade_count(player) >= amount
def has_stat_upgrades(self, player: int, amount: int) -> bool:
return self.stat_upgrade_count(player) >= amount
def _legacy_total_stat_upgrades_count(self, player: int) -> int:
def total_stat_upgrades_count(self, player: int) -> int:
return int(self.world.health_pool[player]) + \
int(self.world.mana_pool[player]) + \
int(self.world.attack_pool[player]) + \
int(self.world.magic_damage_pool[player]) + \
int(self.world.armor_pool[player]) + \
int(self.world.equip_pool[player])
int(self.world.magic_damage_pool[player])
def _legacy_stat_upgrade_count(self, player: int) -> int:
return self.item_count(ItemName.health, player) + self.item_count(ItemName.mana, player) + \
self.item_count(ItemName.attack, player) + self.item_count(ItemName.magic_damage, player) + \
self.item_count(ItemName.armor, player) + self.item_count(ItemName.equip, player)
def stat_upgrade_count(self: CollectionState, player: int) -> int:
return self.item_count("Health Up", player) + self.item_count("Mana Up", player) + \
self.item_count("Attack Up", player) + self.item_count("Magic Damage Up", player)
def set_rules(world: MultiWorld, player: int):
# Check for duplicate names.
if len(set(world.additional_lady_names[player].value)) != len(world.additional_lady_names[player].value):
raise Exception(f"Duplicate values are not allowed in additional_lady_names.")
if len(set(world.additional_sir_names[player].value)) != len(world.additional_sir_names[player].value):
raise Exception(f"Duplicate values are not allowed in additional_sir_names.")
if not world.allow_default_names[player]:
# Check for quantity.
name_count = len(world.additional_lady_names[player].value)
if name_count < int(world.number_of_children[player]):
raise Exception(f"allow_default_names is off, but not enough names are defined in additional_lady_names. Expected {int(world.number_of_children[player])}, Got {name_count}")
name_count = len(world.additional_sir_names[player].value)
if name_count < int(world.number_of_children[player]):
raise Exception(f"allow_default_names is off, but not enough names are defined in additional_sir_names. Expected {int(world.number_of_children[player])}, Got {name_count}")
# Chests
if world.universal_chests[player]:
for i in range(0, world.chests_per_zone[player]):
set_rule(world.get_location(f"Chest {i + 1 + (world.chests_per_zone[player] * 1)}", player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(f"Chest {i + 1 + (world.chests_per_zone[player] * 2)}", player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(f"Chest {i + 1 + (world.chests_per_zone[player] * 3)}", player),
lambda state: state.has(ItemName.boss_tower, player))
else:
for i in range(0, world.chests_per_zone[player]):
set_rule(world.get_location(f"{LocationName.garden} - Chest {i + 1}", player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(f"{LocationName.tower} - Chest {i + 1}", player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(f"{LocationName.dungeon} - Chest {i + 1}", player),
lambda state: state.has(ItemName.boss_tower, player))
# Fairy Chests
if world.universal_fairy_chests[player]:
for i in range(0, world.fairy_chests_per_zone[player]):
set_rule(world.get_location(f"Fairy Chest {i + 1 + (world.fairy_chests_per_zone[player] * 1)}", player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(f"Fairy Chest {i + 1 + (world.fairy_chests_per_zone[player] * 2)}", player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(f"Fairy Chest {i + 1 + (world.fairy_chests_per_zone[player] * 3)}", player),
lambda state: state.has(ItemName.boss_tower, player))
else:
for i in range(0, world.fairy_chests_per_zone[player]):
set_rule(world.get_location(f"{LocationName.garden} - Fairy Chest {i + 1}", player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(f"{LocationName.tower} - Fairy Chest {i + 1}", player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(f"{LocationName.dungeon} - Fairy Chest {i + 1}", player),
lambda state: state.has(ItemName.boss_tower, player))
# Vendors
if world.vendors[player] == "early":
set_rule(world.get_location(LocationName.boss_castle, player),
lambda state: state._legacy_has_all_vendors(player))
elif world.vendors[player] == "normal":
set_rule(world.get_location(LocationName.garden, player),
lambda state: state._legacy_has_any_vendors(player))
# Diaries
for i in range(0, 5):
set_rule(world.get_location(f"Diary {i + 6}", player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(f"Diary {i + 11}", player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(f"Diary {i + 16}", player),
lambda state: state.has(ItemName.boss_tower, player))
set_rule(world.get_location(f"Diary {i + 21}", player),
lambda state: state.has(ItemName.boss_dungeon, player))
if world.vendors[player] == "normal":
set_rule(world.get_location("Forest Abkhazia Boss Reward", player),
lambda state: state.has_all_vendors(player))
# Scale each manor location.
set_rule(world.get_location(LocationName.manor_left_wing_window, player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(LocationName.manor_left_wing_roof, player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(LocationName.manor_right_wing_window, player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(LocationName.manor_right_wing_roof, player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(LocationName.manor_left_big_base, player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(LocationName.manor_right_big_base, player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(LocationName.manor_left_tree1, player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(LocationName.manor_left_tree2, player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(LocationName.manor_right_tree, player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(LocationName.manor_left_big_upper1, player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(LocationName.manor_left_big_upper2, player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(LocationName.manor_left_big_windows, player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(LocationName.manor_left_big_roof, player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(LocationName.manor_left_far_base, player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(LocationName.manor_left_far_roof, player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(LocationName.manor_left_extension, player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(LocationName.manor_right_big_upper, player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(LocationName.manor_right_big_roof, player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(LocationName.manor_right_extension, player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(LocationName.manor_right_high_base, player),
lambda state: state.has(ItemName.boss_tower, player))
set_rule(world.get_location(LocationName.manor_right_high_upper, player),
lambda state: state.has(ItemName.boss_tower, player))
set_rule(world.get_location(LocationName.manor_right_high_tower, player),
lambda state: state.has(ItemName.boss_tower, player))
set_rule(world.get_location(LocationName.manor_observatory_base, player),
lambda state: state.has(ItemName.boss_tower, player))
set_rule(world.get_location(LocationName.manor_observatory_scope, player),
lambda state: state.has(ItemName.boss_tower, player))
manor_rules = {
"Defeat Khidr" if world.khidr[player] == "vanilla" else "Defeat Neo Khidr": [
"Manor - Left Wing Window",
"Manor - Left Wing Rooftop",
"Manor - Right Wing Window",
"Manor - Right Wing Rooftop",
"Manor - Left Big Base",
"Manor - Right Big Base",
"Manor - Left Tree 1",
"Manor - Left Tree 2",
"Manor - Right Tree",
],
"Defeat Alexander" if world.alexander[player] == "vanilla" else "Defeat Alexander IV": [
"Manor - Left Big Upper 1",
"Manor - Left Big Upper 2",
"Manor - Left Big Windows",
"Manor - Left Big Rooftop",
"Manor - Left Far Base",
"Manor - Left Far Roof",
"Manor - Left Extension",
"Manor - Right Big Upper",
"Manor - Right Big Rooftop",
"Manor - Right Extension",
],
"Defeat Ponce de Leon" if world.leon[player] == "vanilla" else "Defeat Ponce de Freon": [
"Manor - Right High Base",
"Manor - Right High Upper",
"Manor - Right High Tower",
"Manor - Observatory Base",
"Manor - Observatory Telescope",
]
}
for event, locations in manor_rules.items():
for location in locations:
set_rule(world.get_location(location, player), lambda state: state.has(event, player))
# Standard Zone Progression
set_rule(world.get_location(LocationName.garden, player),
lambda state: state._legacy_has_stat_upgrades(player, 0.125 * state._legacy_total_stat_upgrades_count(player)) and state.has(ItemName.boss_castle, player))
set_rule(world.get_location(LocationName.tower, player),
lambda state: state._legacy_has_stat_upgrades(player, 0.3125 * state._legacy_total_stat_upgrades_count(player)) and state.has(ItemName.boss_forest, player))
set_rule(world.get_location(LocationName.dungeon, player),
lambda state: state._legacy_has_stat_upgrades(player, 0.5 * state._legacy_total_stat_upgrades_count(player)) and state.has(ItemName.boss_tower, player))
world.get_entrance("Forest Abkhazia", player).access_rule = \
(lambda state: state.has_stat_upgrades(player, 0.125 * state.total_stat_upgrades_count(player)) and
(state.has("Defeat Khidr", player) or state.has("Defeat Neo Khidr", player)))
world.get_entrance("The Maya", player).access_rule = \
(lambda state: state.has_stat_upgrades(player, 0.25 * state.total_stat_upgrades_count(player)) and
(state.has("Defeat Alexander", player) or state.has("Defeat Alexander IV", player)))
world.get_entrance("Land of Darkness", player).access_rule = \
(lambda state: state.has_stat_upgrades(player, 0.375 * state.total_stat_upgrades_count(player)) and
(state.has("Defeat Ponce de Leon", player) or state.has("Defeat Ponce de Freon", player)))
world.get_entrance("The Fountain Room", player).access_rule = \
(lambda state: state.has_stat_upgrades(player, 0.5 * state.total_stat_upgrades_count(player)) and
(state.has("Defeat Herodotus", player) or state.has("Defeat Astrodotus", player)))
# Bosses
set_rule(world.get_location(LocationName.boss_castle, player),
lambda state: state.has(ItemName.boss_castle, player))
set_rule(world.get_location(LocationName.boss_forest, player),
lambda state: state.has(ItemName.boss_forest, player))
set_rule(world.get_location(LocationName.boss_tower, player),
lambda state: state.has(ItemName.boss_tower, player))
set_rule(world.get_location(LocationName.boss_dungeon, player),
lambda state: state.has(ItemName.boss_dungeon, player))
set_rule(world.get_location(LocationName.fountain, player),
lambda state: state._legacy_has_stat_upgrades(player, 0.625 * state._legacy_total_stat_upgrades_count(player))
and state.has(ItemName.boss_castle, player)
and state.has(ItemName.boss_forest, player)
and state.has(ItemName.boss_tower, player)
and state.has(ItemName.boss_dungeon, player))
world.completion_condition[player] = lambda state: state.has(ItemName.boss_fountain, player)
world.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player)

View File

@ -36,4 +36,3 @@ traits = [
"Glaucoma",
"Adopted",
]

View File

@ -1,176 +1,265 @@
import typing
from typing import List
from BaseClasses import Item, ItemClassification, Tutorial
from .Items import LegacyItem, ItemData, item_table, vendors_table, static_classes_table, progressive_classes_table, \
skill_unlocks_table, blueprints_table, runes_table, misc_items_table
from .Locations import LegacyLocation, location_table, base_location_table
from .Options import legacy_options
from BaseClasses import Tutorial
from .Items import RLItem, RLItemData, event_item_table, item_table, get_items_by_category
from .Locations import RLLocation, location_table
from .Options import rl_options
from .Regions import create_regions
from .Rules import set_rules
from .Names import ItemName
from ..AutoWorld import World, WebWorld
class LegacyWeb(WebWorld):
class RLWeb(WebWorld):
theme = "stone"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Rogue Legacy Randomizer software on your computer. This guide covers single-player, multiworld, and related software.",
"A guide to setting up the Rogue Legacy Randomizer software on your computer. This guide covers single-player, "
"multiworld, and related software.",
"English",
"rogue-legacy_en.md",
"rogue-legacy/en",
["Phar"]
)]
bug_report_page = "https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=" \
"report-an-issue---.md&title=%5BIssue%5D"
class LegacyWorld(World):
class RLWorld(World):
"""
Rogue Legacy is a genealogical rogue-"LITE" where anyone can be a hero. Each time you die, your child will succeed
you. Every child is unique. One child might be colorblind, another might have vertigo-- they could even be a dwarf.
But that's OK, because no one is perfect, and you don't have to be to succeed.
"""
game: str = "Rogue Legacy"
option_definitions = legacy_options
topology_present = False
data_version = 3
required_client_version = (0, 2, 3)
web = LegacyWeb()
option_definitions = rl_options
topology_present = True
data_version = 4
required_client_version = (0, 3, 5)
web = RLWeb()
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = location_table
location_name_to_id = {name: data.code for name, data in location_table.items()}
def _get_slot_data(self):
return {
"starting_gender": self.world.starting_gender[self.player],
"starting_class": self.world.starting_class[self.player],
"new_game_plus": self.world.new_game_plus[self.player],
"fairy_chests_per_zone": self.world.fairy_chests_per_zone[self.player],
"chests_per_zone": self.world.chests_per_zone[self.player],
"universal_fairy_chests": self.world.universal_fairy_chests[self.player],
"universal_chests": self.world.universal_chests[self.player],
"vendors": self.world.vendors[self.player],
"architect_fee": self.world.architect_fee[self.player],
"disable_charon": self.world.disable_charon[self.player],
"require_purchasing": self.world.require_purchasing[self.player],
"gold_gain_multiplier": self.world.gold_gain_multiplier[self.player],
"number_of_children": self.world.number_of_children[self.player],
"khidr": self.world.khidr[self.player],
"alexander": self.world.alexander[self.player],
"leon": self.world.leon[self.player],
"herodotus": self.world.herodotus[self.player],
"allow_default_names": self.world.allow_default_names[self.player],
"additional_sir_names": self.world.additional_sir_names[self.player],
"additional_lady_names": self.world.additional_lady_names[self.player],
"death_link": self.world.death_link[self.player],
}
item_pool: List[RLItem] = []
prefill_items: List[RLItem] = []
def _create_items(self, name: str):
data = item_table[name]
return [self.create_item(name) for _ in range(data.quantity)]
def setting(self, name: str):
return getattr(self.world, name)[self.player]
def fill_slot_data(self) -> dict:
slot_data = self._get_slot_data()
for option_name in legacy_options:
option = getattr(self.world, option_name)[self.player]
slot_data[option_name] = option.value
return {option_name: self.setting(option_name).value for option_name in rl_options}
return slot_data
def generate_early(self):
self.prefill_items = []
# Check validation of names.
additional_lady_names = len(self.setting("additional_lady_names").value)
additional_sir_names = len(self.setting("additional_sir_names").value)
if not self.setting("allow_default_names"):
# Check for max_quantity.
if additional_lady_names < int(self.setting("number_of_children")):
raise Exception(
f"allow_default_names is off, but not enough names are defined in additional_lady_names. "
f"Expected {int(self.setting('number_of_children'))}, Got {additional_lady_names}")
if additional_sir_names < int(self.setting("number_of_children")):
raise Exception(
f"allow_default_names is off, but not enough names are defined in additional_sir_names. "
f"Expected {int(self.setting('number_of_children'))}, Got {additional_sir_names}")
if self.setting("vendors") == "early":
self.prefill_items += [self.create_item("Blacksmith"), self.create_item("Enchantress")]
if self.setting("architect") == "early":
self.prefill_items += [self.create_item("Architect")]
def generate_basic(self):
itempool: typing.List[LegacyItem] = []
total_required_locations = 64 + (self.world.chests_per_zone[self.player] * 4) + (self.world.fairy_chests_per_zone[self.player] * 4)
self.item_pool = []
total_locations = 64 + (self.setting("chests_per_zone") * 4) + (self.setting("fairy_chests_per_zone") * 4)
# Fill item pool with all required items
for item in {**skill_unlocks_table, **runes_table}:
# if Haggling, do not add if Disable Charon.
if item == ItemName.haggling and self.world.disable_charon[self.player] == 1:
# Add items to item pool. Anything with a "continue" will not be added to the item pool.
for name, data in item_table.items():
quantity = data.max_quantity
# Architect
if name == "Architect":
if self.setting("architect") == "disabled" or self.setting("architect") == "early":
continue
if self.setting("architect") == "start_unlocked":
self.world.push_precollected(self.create_item(name))
continue
# Blacksmith and Enchantress
if name == "Blacksmith" or name == "Enchantress":
if self.setting("vendors") == "start_unlocked":
self.world.push_precollected(self.create_item(name))
continue
if self.setting("vendors") == "early":
continue
# Haggling
if name == "Haggling" and self.setting("disable_charon"):
continue
itempool += self._create_items(item)
# Blueprints
if self.world.progressive_blueprints[self.player]:
itempool += [self.create_item(ItemName.progressive_blueprints) for _ in range(15)]
else:
for item in blueprints_table:
itempool += self._create_items(item)
# Blueprints
if data.category == "Blueprints":
# No progressive blueprints if progressive_blueprints are disabled.
if name == "Progressive Blueprints" and not self.setting("progressive_blueprints"):
continue
# No distinct blueprints if progressive_blueprints are enabled.
elif name != "Progressive Blueprints" and self.setting("progressive_blueprints"):
continue
# Check Pool settings to add a certain amount of these items.
itempool += [self.create_item(ItemName.health) for _ in range(self.world.health_pool[self.player])]
itempool += [self.create_item(ItemName.mana) for _ in range(self.world.mana_pool[self.player])]
itempool += [self.create_item(ItemName.attack) for _ in range(self.world.attack_pool[self.player])]
itempool += [self.create_item(ItemName.magic_damage) for _ in range(self.world.magic_damage_pool[self.player])]
itempool += [self.create_item(ItemName.armor) for _ in range(self.world.armor_pool[self.player])]
itempool += [self.create_item(ItemName.equip) for _ in range(self.world.equip_pool[self.player])]
itempool += [self.create_item(ItemName.crit_chance) for _ in range(self.world.crit_chance_pool[self.player])]
itempool += [self.create_item(ItemName.crit_damage) for _ in range(self.world.crit_damage_pool[self.player])]
# Classes
if data.category == "Classes":
if name == "Progressive Knights":
if "Knight" not in self.setting("available_classes"):
continue
classes = self.world.available_classes[self.player]
if "Dragon" in classes:
itempool.append(self.create_item(ItemName.dragon))
if "Traitor" in classes:
itempool.append(self.create_item(ItemName.traitor))
if self.world.starting_class[self.player] == "knight":
itempool.append(self.create_item(ItemName.progressive_knight))
elif "Knight" in classes:
itempool.extend(self._create_items(ItemName.progressive_knight))
if self.world.starting_class[self.player] == "mage":
itempool.append(self.create_item(ItemName.progressive_mage))
elif "Mage" in classes:
itempool.extend(self._create_items(ItemName.progressive_mage))
if self.world.starting_class[self.player] == "barbarian":
itempool.append(self.create_item(ItemName.progressive_barbarian))
elif "Barbarian" in classes:
itempool.extend(self._create_items(ItemName.progressive_barbarian))
if self.world.starting_class[self.player] == "knave":
itempool.append(self.create_item(ItemName.progressive_knave))
elif "Knave" in classes:
itempool.extend(self._create_items(ItemName.progressive_knave))
if self.world.starting_class[self.player] == "shinobi":
itempool.append(self.create_item(ItemName.progressive_shinobi))
elif "Shinobi" in classes:
itempool.extend(self._create_items(ItemName.progressive_shinobi))
if self.world.starting_class[self.player] == "miner":
itempool.append(self.create_item(ItemName.progressive_miner))
elif "Miner" in classes:
itempool.extend(self._create_items(ItemName.progressive_miner))
if self.world.starting_class[self.player] == "lich":
itempool.append(self.create_item(ItemName.progressive_lich))
elif "Lich" in classes:
itempool.extend(self._create_items(ItemName.progressive_lich))
if self.world.starting_class[self.player] == "spellthief":
itempool.append(self.create_item(ItemName.progressive_spellthief))
elif "Spellthief" in classes:
itempool.extend(self._create_items(ItemName.progressive_spellthief))
if self.setting("starting_class") == "knight":
quantity = 1
if name == "Progressive Mages":
if "Mage" not in self.setting("available_classes"):
continue
# Check if we need to start with these vendors or put them in the pool.
if self.world.vendors[self.player] == "start_unlocked":
self.world.push_precollected(self.world.create_item(ItemName.blacksmith, self.player))
self.world.push_precollected(self.world.create_item(ItemName.enchantress, self.player))
else:
itempool += [self.create_item(ItemName.blacksmith), self.create_item(ItemName.enchantress)]
if self.setting("starting_class") == "mage":
quantity = 1
if name == "Progressive Barbarians":
if "Barbarian" not in self.setting("available_classes"):
continue
# Add Architect.
if self.world.architect[self.player] == "start_unlocked":
self.world.push_precollected(self.world.create_item(ItemName.architect, self.player))
elif self.world.architect[self.player] != "disabled":
itempool.append(self.create_item(ItemName.architect))
if self.setting("starting_class") == "barbarian":
quantity = 1
if name == "Progressive Knaves":
if "Knave" not in self.setting("available_classes"):
continue
# Fill item pool with the remaining
for _ in range(len(itempool), total_required_locations):
item = self.world.random.choice(list(misc_items_table.keys()))
itempool.append(self.create_item(item))
if self.setting("starting_class") == "knave":
quantity = 1
if name == "Progressive Miners":
if "Miner" not in self.setting("available_classes"):
continue
self.world.itempool += itempool
if self.setting("starting_class") == "miner":
quantity = 1
if name == "Progressive Shinobis":
if "Shinobi" not in self.setting("available_classes"):
continue
if self.setting("starting_class") == "shinobi":
quantity = 1
if name == "Progressive Liches":
if "Lich" not in self.setting("available_classes"):
continue
if self.setting("starting_class") == "lich":
quantity = 1
if name == "Progressive Spellthieves":
if "Spellthief" not in self.setting("available_classes"):
continue
if self.setting("starting_class") == "spellthief":
quantity = 1
if name == "Dragons":
if "Dragon" not in self.setting("available_classes"):
continue
if name == "Traitors":
if "Traitor" not in self.setting("available_classes"):
continue
# Skills
if name == "Health Up":
quantity = self.setting("health_pool")
elif name == "Mana Up":
quantity = self.setting("mana_pool")
elif name == "Attack Up":
quantity = self.setting("attack_pool")
elif name == "Magic Damage Up":
quantity = self.setting("magic_damage_pool")
elif name == "Armor Up":
quantity = self.setting("armor_pool")
elif name == "Equip Up":
quantity = self.setting("equip_pool")
elif name == "Crit Chance Up":
quantity = self.setting("crit_chance_pool")
elif name == "Crit Damage Up":
quantity = self.setting("crit_damage_pool")
# Ignore filler, it will be added in a later stage.
if data.category == "Filler":
continue
self.item_pool += [self.create_item(name) for _ in range(0, quantity)]
# Fill any empty locations with filler items.
while len(self.item_pool) + len(self.prefill_items) < total_locations:
self.item_pool.append(self.create_item(self.get_filler_item_name()))
self.world.itempool += self.item_pool
def pre_fill(self) -> None:
reachable = [loc for loc in self.world.get_reachable_locations(player=self.player) if not loc.item]
self.world.random.shuffle(reachable)
items = self.prefill_items.copy()
for item in items:
reachable.pop().place_locked_item(item)
def get_pre_fill_items(self) -> List[RLItem]:
return self.prefill_items
def get_filler_item_name(self) -> str:
return self.world.random.choice(list(misc_items_table.keys()))
fillers = get_items_by_category("Filler")
weights = [data.weight for data in fillers.values()]
return self.world.random.choices([filler for filler in fillers.keys()], weights, k=1)[0]
def create_regions(self):
create_regions(self.world, self.player)
def create_item(self, name: str) -> Item:
def create_item(self, name: str) -> RLItem:
data = item_table[name]
return LegacyItem(name, ItemClassification.progression if data.progression else ItemClassification.filler, data.code, self.player)
return RLItem(name, data.classification, data.code, self.player)
def create_event(self, name: str) -> RLItem:
data = event_item_table[name]
return RLItem(name, data.classification, data.code, self.player)
def set_rules(self):
set_rules(self.world, self.player)
def create_regions(self):
create_regions(self.world, self.player)
self._place_events()
def _place_events(self):
# Fountain
self.world.get_location("Fountain Room", self.player).place_locked_item(
self.create_event("Defeat The Fountain"))
# Khidr / Neo Khidr
if self.setting("khidr") == "vanilla":
self.world.get_location("Castle Hamson Boss Room", self.player).place_locked_item(
self.create_event("Defeat Khidr"))
else:
self.world.get_location("Castle Hamson Boss Room", self.player).place_locked_item(
self.create_event("Defeat Neo Khidr"))
# Alexander / Alexander IV
if self.setting("alexander") == "vanilla":
self.world.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item(
self.create_event("Defeat Alexander"))
else:
self.world.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item(
self.create_event("Defeat Alexander IV"))
# Ponce de Leon / Ponce de Freon
if self.setting("leon") == "vanilla":
self.world.get_location("The Maya Boss Room", self.player).place_locked_item(
self.create_event("Defeat Ponce de Leon"))
else:
self.world.get_location("The Maya Boss Room", self.player).place_locked_item(
self.create_event("Defeat Ponce de Freon"))
# Herodotus / Astrodotus
if self.setting("herodotus") == "vanilla":
self.world.get_location("Land of Darkness Boss Room", self.player).place_locked_item(
self.create_event("Defeat Herodotus"))
else:
self.world.get_location("Land of Darkness Boss Room", self.player).place_locked_item(
self.create_event("Defeat Astrodotus"))

View File

@ -2,13 +2,15 @@
## Where is the settings page?
The [player settings page for this game](../player-settings) is located contains all the options you need to configure
and export a config file.
The [player settings page for this game](../player-settings) contains most of the options you need to
configure and export a config file. Some settings can only be made via YAML, but an explaination can be found in the
[template yaml here](../../../static/generated/configs/Rogue%20Legacy.yaml).
## What does randomization do to this game?
You are not able to buy skill upgrades in the manor upgrade screen, and instead, need to find them in order to level up
your character to make fighting the 5 bosses easier.
Rogue Legacy Randomizer takes all the classes, skills, runes, and blueprints and spreads them out into chests, the manor
upgrade screen, bosses, and some special individual locations. The goal is to become powerful enough to defeat the four
zone bosses and then defeat The Fountain.
## What items and locations get shuffled?
@ -16,6 +18,8 @@ All the skill upgrades, class upgrades, runes packs, and equipment packs are shu
checks, chests and fairy chests, and boss rewards. Skill upgrades are also grouped in packs of 5 to make the finding of
stats less of a chore. Runes and Equipment are also grouped together.
Some additional locations that can contain items are the Jukebox, the Portraits, and the mini-game rewards.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
@ -25,3 +29,8 @@ certain items to your own world.
When the player receives an item, your character will hold the item above their head and display it to the world. It's
good for business!
## What do I do if I encounter a bug with the game?
Please reach out to Phar#4444 on Discord or you can drop a bug report on the
[GitHub page for Rogue Legacy Randomizer](https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=report-an-issue---.md&title=%5BIssue%5D).

View File

@ -28,25 +28,8 @@ provides an alternative one to the default values.
Once you have entered the required values, you go to Connect and then select Confirm on the "Ready to Start" screen. Now
you're off to start your legacy!
## Manual Installation
## Recommended Installation Instructions
In order to run Rogue Legacy Randomizer you will need to have Rogue Legacy installed on your local machine. Extract the
Randomizer release into a desired folder **outside** of your Rogue Legacy install. Copy the following files from your
Rogue Legacy install into the main directory of your Rogue Legacy Randomizer install:
- DS2DEngine.dll
- InputSystem.dll
- Nuclex.Input.dll
- SpriteSystem.dll
- Tweener.dll
And copy the directory from your Rogue Legacy install as well into the main directory of your Rogue Legacy Randomizer
install:
- Content/
Then copy the contents of the CustomContent directory in your Rogue Legacy Randomizer into the newly copied Content
directory and overwrite all files.
**BE SURE YOU ARE REPLACING THE COPIED FILES IN YOUR ROGUE LEGACY RANDOMIZER DIRECTORY AND NOT REPLACING YOUR ROGUE
LEGACY FILES!**
Please read the README file on the
[Rogue Legacy Randomizer GitHub](https://github.com/ThePhar/RogueLegacyRandomizer/blob/master/README.md) page for up to
date installation instructions.