EarthBound: Implement New Game (#5159)

* Add the world

* doc update

* docs

* Fix Blast/Missile not clearing Reflect

* Update worlds/earthbound/__init__.py

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Update worlds/earthbound/__init__.py

remove unused import

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Update worlds/earthbound/__init__.py

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Update worlds/earthbound/modules/dungeon_er.py

make bool optional

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Update worlds/earthbound/modules/boss_shuffle.py

typing update

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Update worlds/earthbound/modules/boss_shuffle.py

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Filter events out of item name to id

* we call it a glorp

* Update worlds/earthbound/Regions.py

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Update worlds/earthbound/__init__.py

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Update worlds/earthbound/Items.py

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Update worlds/earthbound/Regions.py

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Fix missing optional import

* hint stuff

* -Fix Apple Kid text being wrong
-Fix Slimy Pile text being wrong

* -Fix some sprite corruption if PSI was used when an enemy loaded another enemy
-Fixed a visible artifact tile during some cutscenes

* Update ver

* Update docs

* Fix some money scripting issues

* Add argument to PSI fakeout attack

* Updated monkey caves shop description

* Remove closing markdown from doc

* Add new flavors

* Make flavors actually work

* Update platforms

* Fix common gear getting duplicated

* Split region initialization

* Condense checks for start inventory + some other junk

* Fix some item groups - change receiver phone to warp pad

* wow that one was really bad :glorp:

* blah

* Fix cutoff option text

* switch start inventory concatenation to itertools

* Fix sky runner scripting bug - added some new comm suggestions

* Fix crash when generating with spoiler_only

* Fix happy-happy teleport not unlocking after beating carpainter

* Hint man hints can now use CreateHint packets to create hints in other games

* Adjust some filler rarity

* Update world to use CreateHints and deprecate old method

* Fix epilogue skip being offset

* Rearrange a couple regions

* Fix tendapants getting deleted in battle

* update doc

* i got scared and forgot i had multiple none checks and am worried about this triggering but tested and it works

* Fix mostly typing errors from silvris

* More type checks

* More typing

* Typema

* Type

* Fix enemy levels overwriting music

* Fix gihugic blunder

* Fix Lumine Hall enabling OSS

* del world

* Rel 4.2.7

* Remove some debug logs

* Fix vanilla bug with weird ambush detection

* Fix Starman Junior having an unscaled Freeze

* Change shop scaling

* Fix shops using the wrong thankful script

* Update some bosses in boss shuffle

* Loc group adjustment

* Update some boss shuffle stuff | Fix Enemizer attacks getting overwritten by Shuffle data | Fix flunkies not updating and still being used with enemizer

* Get rid of some debug stuff

* Get boss shuffle running, dont merge

* Fix json and get boss shuffle no plando back up

* Fix Magicant Boost not initializing to Ness if party count = 4

* Fix belch shop using wrong logic

* Don't re-send goal status

* EBitem

* remove :

* idk if this is whatvi wanted

* All client messagesnow only send when relevant instead of constantly

* Patch up the rest of boss plando

* Fix Giygas being not excluded from enemizer

* Fix epilogue again

* adjust the sphere scaling name

* add the things

* Fix Ness being placed onto monotoli when monotoli was in sea of eden

* Fix prefill properly

* Fix boss shuffle on vanilla slots.

* rename this, apparently

* Update archipelago.json

---------

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
PinkSwitch
2025-12-19 07:52:27 -06:00
committed by GitHub
parent ebbdd7bfda
commit 55c70a5ba8
54 changed files with 47638 additions and 0 deletions

View File

@@ -84,6 +84,7 @@ Currently, the following games are supported:
* Choo-Choo Charles * Choo-Choo Charles
* APQuest * APQuest
* Satisfactory * Satisfactory
* EarthBound
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -70,6 +70,9 @@
# DOOM II # DOOM II
/worlds/doom_ii/ @Daivuk @KScl /worlds/doom_ii/ @Daivuk @KScl
# EarthBound
/worlds/earthbound/ @PinkSwitch
# Factorio # Factorio
/worlds/factorio/ @Berserker66 /worlds/factorio/ @Berserker66

View File

@@ -208,6 +208,11 @@ Root: HKCR; Subkey: "{#MyAppName}apcivvipatch"; ValueData: "
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apeb"; ValueData: "{#MyAppName}ebpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ebpatch"; ValueData: "Archipelago EarthBound Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ebpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ebpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";

516
worlds/earthbound/Client.py Normal file
View File

@@ -0,0 +1,516 @@
import logging
import struct
import typing
import time
import uuid
from struct import pack
from .game_data.local_data import client_specials, world_version, hint_bits, item_id_table, money_id_table
from .game_data.text_data import text_encoder
from .gifting.gift_tags import gift_properties
from .gifting.trait_parser import wanted_traits, trait_interpreter, gift_exclusions
from NetUtils import ClientStatus, color
from worlds.AutoSNIClient import SNIClient
if typing.TYPE_CHECKING:
from SNIClient import SNIContext
else:
SNIContext = typing.Any
snes_logger = logging.getLogger("SNES")
ROM_START = 0x000000
WRAM_START = 0xF50000
WRAM_SIZE = 0x20000
SRAM_START = 0xE00000
EB_ROMHASH_START = 0x00FFC0
WORLD_VERSION = 0x3FF0A0
ROMHASH_SIZE = 0x15
ITEM_MODE = ROM_START + 0x04FD76
ITEMQUEUE_HIGH = WRAM_START + 0xB576
ITEM_RECEIVED = WRAM_START + 0xB570
SPECIAL_RECEIVED = WRAM_START + 0xB572
MONEY_RECIVED = WRAM_START + 0xB5F1
SAVE_FILE = WRAM_START + 0xB4A1
GIYGAS_CLEAR = WRAM_START + 0x9C11
GAME_CLEAR = WRAM_START + 0x9C85
OPEN_WINDOW = WRAM_START + 0x8958
MELODY_TABLE = WRAM_START + 0x9C1E
EARTH_POWER_FLAG = WRAM_START + 0x9C82
CUR_SCENE = WRAM_START + 0x97B8
IS_IN_BATTLE = WRAM_START + 0x9643
DEATHLINK_ENABLED = ROM_START + 0x04FD74
DEATHLINK_TYPE = ROM_START + 0x04FD75
IS_CURRENTLY_DEAD = WRAM_START + 0xB582
GOT_DEATH_FROM_SERVER = WRAM_START + 0xB583
PLAYER_JUST_DIED_SEND_DEATHLINK = WRAM_START + 0xB584
IS_ABLE_TO_RECEIVE_DEATHLINKS = WRAM_START + 0xB585
CHAR_COUNT = WRAM_START + 0x98A4
OSS_FLAG = WRAM_START + 0x5D98
HINT_SCOUNT_IDS = ROM_START + 0x310250
SCOUTED_HINT_FLAGS = WRAM_START + 0xB621
MONEY_IN_BANK = WRAM_START + 0x9835
IS_ENERGYLINK_ENABLED = ROM_START + 0x04FD78
already_tried_to_connect = False
class EarthBoundClient(SNIClient):
game = "EarthBound"
patch_suffix = ".apeb"
most_recent_connect: str = ""
client_version: str = world_version
hint_list: list[int] = []
hinted_shop_locations: list[int] = []
async def deathlink_kill_player(self, ctx: "SNIContext") -> None:
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
battle_hp = {
1: WRAM_START + 0x9FBF,
2: WRAM_START + 0xA00D,
3: WRAM_START + 0xA05B,
4: WRAM_START + 0xA0A9,
}
active_hp = {
1: WRAM_START + 0x9A15,
2: WRAM_START + 0x9A74,
3: WRAM_START + 0x9AD3,
4: WRAM_START + 0x9B32,
}
scrolling_hp = {
1: WRAM_START + 0x9A13,
2: WRAM_START + 0x9A72,
3: WRAM_START + 0x9AD1,
4: WRAM_START + 0x9B30,
}
deathlink_mode = await snes_read(ctx, DEATHLINK_TYPE, 1)
oss_flag = await snes_read(ctx, OSS_FLAG, 1)
is_currently_dead = await snes_read(ctx, IS_CURRENTLY_DEAD, 1)
can_receive_deathlinks = await snes_read(ctx, IS_ABLE_TO_RECEIVE_DEATHLINKS, 1)
is_in_battle = await snes_read(ctx, IS_IN_BATTLE, 1)
char_count = await snes_read(ctx, CHAR_COUNT, 1)
snes_buffered_write(ctx, GOT_DEATH_FROM_SERVER, bytes([0x01]))
text_open = await snes_read(ctx, OPEN_WINDOW, 1)
if text_open is None: #Catch None reads from client jank????????
return
if is_currently_dead[0] != 0x00 or can_receive_deathlinks[0] == 0x00:
return
# If suppression is set and we're not in a battle dont do deathlinks
if oss_flag[0] != 0x00 and is_in_battle[0] == 0x00:
return
# Prevent overworld deaths while a menu is open
if not is_in_battle[0] and text_open[0] != 0xFF:
return
for i in range(char_count[0]):
w_cur_char = WRAM_START + 0x986F + i
current_char = await snes_read(ctx, w_cur_char, 1)
snes_buffered_write(ctx, active_hp[current_char[0]], bytes([0x00, 0x00]))
snes_buffered_write(ctx, battle_hp[i + 1], bytes([0x00, 0x00]))
if deathlink_mode[0] == 0 or is_in_battle[0] == 0:
# This should be the check for instant or mercy. Write the value, call it here
snes_buffered_write(ctx, scrolling_hp[current_char[0]], bytes([0x00, 0x00]))
await snes_flush_writes(ctx)
ctx.death_state = DeathState.dead
ctx.last_death_link = time.time()
def on_package(self, ctx, cmd: str, args: dict[str, typing.Any]) -> None:
super().on_package(ctx, cmd, args)
if cmd == "Connected":
self.slot_data = args.get("slot_data", None)
async def validate_rom(self, ctx: "SNIContext") -> bool:
from SNIClient import snes_read
rom_name = await snes_read(ctx, EB_ROMHASH_START, ROMHASH_SIZE)
apworld_version = await snes_read(ctx, WORLD_VERSION, 16)
item_handling = await snes_read(ctx, ITEM_MODE, 1)
if rom_name is None or rom_name[:6] != b"MOM2AP":
return False
apworld_version = apworld_version.decode("utf-8").strip("\x00")
if apworld_version != self.most_recent_connect and apworld_version != self.client_version:
ctx.gui_error("Bad Version", f"EarthBound APWorld version {self.client_version} does not match generated version {apworld_version}")
self.most_recent_connect = apworld_version
return False
ctx.game = self.game
if item_handling[0] == 0x00:
ctx.items_handling = 0b001
else:
ctx.items_handling = 0b111
ctx.rom = rom_name
death_link = await snes_read(ctx, DEATHLINK_ENABLED, 1)
if death_link:
await ctx.update_death_link(bool(death_link[0] & 0b1))
return True
async def game_watcher(self, ctx: "SNIContext") -> None:
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read, snes_write
giygas_clear = await snes_read(ctx, GIYGAS_CLEAR, 0x1)
game_clear = await snes_read(ctx, GAME_CLEAR, 0x1)
item_received = await snes_read(ctx, ITEM_RECEIVED, 0x1)
special_received = await snes_read(ctx, SPECIAL_RECEIVED, 0x1)
money_received = await snes_read(ctx, MONEY_RECIVED, 0x2)
save_num = await snes_read(ctx, SAVE_FILE, 0x1)
text_open = await snes_read(ctx, OPEN_WINDOW, 1)
melody_table = await snes_read(ctx, MELODY_TABLE, 2)
earth_power_absorbed = await snes_read(ctx, EARTH_POWER_FLAG, 1)
cur_script = await snes_read(ctx, CUR_SCENE, 1)
rom = await snes_read(ctx, EB_ROMHASH_START, ROMHASH_SIZE)
scouted_hint_flags = await snes_read(ctx, SCOUTED_HINT_FLAGS, 1)
gift_target = await snes_read(ctx, WRAM_START + 0xB5E7, 2)
outbound_gifts = await snes_read(ctx, WRAM_START + 0x31D0, 1)
shop_scout = await snes_read(ctx, WRAM_START + 0x0770, 1)
shop_scouts_enabled = await snes_read(ctx, ROM_START + 0x04FD77, 1)
outgoing_energy = await snes_read(ctx, MONEY_IN_BANK, 4)
if rom != ctx.rom:
ctx.rom = None
return
if giygas_clear[0] & 0x01 == 0x01: # Are we in the epilogue
return
if save_num[0] == 0x00: # If on the title screen
return
if ctx.slot is None:
return
if outgoing_energy is None: #None Catcher
return
if f"GiftBoxes;{ctx.team}" not in ctx.stored_data:
await ctx.send_msgs([{
"cmd": "SetNotify",
"keys": [f"GiftBoxes;{ctx.team}"]
}])
# GIFTING DATA
if f"GiftBox;{ctx.team};{ctx.slot}" not in ctx.stored_data:
local_giftbox = {
str(ctx.slot): {
"is_open": True,
"accepts_any_gift": False,
"desired_traits": wanted_traits,
"minimum_gift_data_version": 2,
"maximum_gift_data_version": 3}}
await ctx.send_msgs([{
"cmd": "Set",
"key": f"GiftBoxes;{ctx.team}",
"want_reply": False,
"default": {},
"operations": [{"operation": "update", "value": local_giftbox}]
}])
await ctx.send_msgs([{
"cmd": "Get",
"keys": [f"GiftBox;{ctx.team};{ctx.slot}"]
}])
await ctx.send_msgs([{
"cmd": "SetNotify",
"keys": [f"GiftBox;{ctx.team};{ctx.slot}", f"GiftBoxes;{ctx.team}"]
}])
inbox = ctx.stored_data.get(f"GiftBox;{ctx.team};{ctx.slot}")
motherbox = ctx.stored_data.get(f"GiftBoxes;{ctx.team}")
if inbox:
gift_item_name = "None"
key, gift = next(iter(inbox.items()))
if "item_name" in gift or "ItemName" in gift:
gift_item_name = gift.get("item_name", gift.get("ItemName"))
if gift_item_name in item_id_table and gift_item_name not in gift_exclusions:
# If the name matches an EB item, convert it to one (even if not coming from EB)
item = item_id_table[gift_item_name]
else:
item = trait_interpreter(gift)
inbox_queue = await snes_read(ctx, WRAM_START + 0x3200, 1)
# Pause if the receiver queue is full
if not inbox_queue[0]:
await snes_write(ctx, [(WRAM_START + 0x3200, bytes([item]))])
inbox.pop(key)
await ctx.send_msgs([{
"cmd": "Set",
"key": f"GiftBox;{ctx.team};{ctx.slot}",
"want_reply": False,
"default": {},
"operations": [{"operation": "pop", "value": key}]
}])
# We're in the Gift selection menu. This should write the selected player's name into RAM
# for parsing.
# TODO; CHECK A SETNOTIFY HERE
gift_target = int.from_bytes(gift_target, byteorder="little")
# Giftbox checking for the gift menu UI
if gift_target != 0x00 and motherbox is not None:
gift_recipient = str(gift_target)
recip_name = ctx.player_names[gift_target]
recip_name = get_alias(recip_name, ctx.slot_info[gift_target].name)
recip_name = text_encoder(recip_name, 20)
if gift_recipient in motherbox:
if "IsOpen" in motherbox[gift_recipient]:
motherbox[gift_recipient]["is_open"] = motherbox[gift_recipient].pop("IsOpen")
if gift_recipient in motherbox and motherbox[gift_recipient]["is_open"]:
recip_name.extend(text_encoder(" (Open)", 20))
else:
recip_name.extend(text_encoder(" (Closed)", 20))
recip_name.append(0x00)
await snes_write(ctx, [(WRAM_START + 0xFF80, recip_name)])
await snes_write(ctx, [(WRAM_START + 0xB5E7, bytes([0x00, 0x00]))])
await snes_write(ctx, [(WRAM_START + 0xB573, bytes([0x00, 0x00]))])
gift_flag_byte = await snes_read(ctx, WRAM_START + 0xB622, 1)
gift_flag_byte = gift_flag_byte[0] | 0x04
await snes_write(ctx, [(WRAM_START + 0xB622, bytes([gift_flag_byte]))])
if outbound_gifts[0] != 0x00 and motherbox is not None:
gift_buffer = await snes_read(ctx, WRAM_START + 0x31D1, 3)
gift_item_id = gift_buffer[0]
gift = gift_properties[gift_item_id]
recipient = struct.unpack("H", gift_buffer[-2:])
if str(recipient[0]) in motherbox:
# Check if the player's box is open, refund if not
if "IsOpen" in motherbox[str(recipient[0])]:
# Does the recipient 0 thing work if > 255? Will need some testing.
motherbox[str(recipient[0])]["is_open"] = motherbox[str(recipient[0])].pop("IsOpen")
if "AcceptsAnyGift" in motherbox[str(recipient[0])]:
motherbox[str(recipient[0])]["accepts_any_gift"] = motherbox[str(recipient[0])].pop("AcceptsAnyGift")
if "DesiredTraits" in motherbox[str(recipient[0])]:
motherbox[str(recipient[0])]["desired_traits"] = motherbox[str(recipient[0])].pop("DesiredTraits")
if "Trait" in motherbox[str(recipient[0])]["desired_traits"]:
motherbox[str(recipient[0])]["desired_traits"]["trait"] = motherbox[str(recipient[0])]["desired_traits"].pop("Trait")
if str(recipient[0]) in motherbox and motherbox[str(recipient[0])]["is_open"] and (any(
motherbox[str(recipient[0])]["accepts_any_gift"] or
trait["trait"] in motherbox[str(recipient[0])]["desired_traits"] for trait in gift.traits)):
was_refunded = False
recipient = recipient[0]
else:
was_refunded = True
recipient = ctx.slot
guid = str(uuid.uuid4())
outgoing_gift = {
guid: {
"id": guid,
"item_name": gift.name,
"amount": 1,
"item_value": gift.value,
"traits": gift.traits,
"sender_slot": ctx.slot,
"receiver_slot": recipient,
"sender_team": ctx.team,
"receiver_team": ctx.team, # ??? Should be Receive slot team?
"is_refund": was_refunded}}
await ctx.send_msgs([{
"cmd": "Set",
"key": f"GiftBox;{ctx.team};{recipient}", # Receiver team here too
"want_reply": True,
"default": {},
"operations": [{"operation": "update", "value": outgoing_gift}]
}])
gift_queue = await snes_read(ctx, WRAM_START + 0x31D4, 0x21)
# shuffle the entire queue down 3 bytes
outbox_full_byte = await snes_read(ctx, WRAM_START + 0xB622, 1)
await snes_write(ctx, [(WRAM_START + 0x31D1, gift_queue)])
await snes_write(ctx, [(WRAM_START + 0x31D0, bytes([outbound_gifts[0] - 1]))])
outbox_full_byte = outbox_full_byte[0] & ~0x08
await snes_write(ctx, [(WRAM_START + 0xB622, bytes([outbox_full_byte]))])
if (game_clear[0] & 0x01 == 0x01) and not ctx.finished_game: # Goal should ignore the item queue and textbox check
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
for i in range(6):
if scouted_hint_flags[0] & hint_bits[i]:
if i not in self.hint_list:
scoutable_hint = await snes_read(ctx, HINT_SCOUNT_IDS + (i * 3), 3)
if not scoutable_hint[2]:
scoutable_hint = (int.from_bytes(scoutable_hint[:2], byteorder="little") + 0xEB0000)
self.hint_list.append(i)
await ctx.send_msgs([{"cmd": "CreateHints", "locations": [scoutable_hint], "player": ctx.player}])
else:
hint = self.slot_data['hint_man_hints'][i]
await ctx.send_msgs([{"cmd": "CreateHints", "locations": [hint[0]], "player": hint[1]}])
self.hint_list.append(i)
if shop_scout[0] and shop_scouts_enabled[0]:
shop_slots = []
for i in range(7):
slot_id = (0xEB0FF9 + (shop_scout[0] * 7) + i)
if slot_id in ctx.server_locations and slot_id not in self.hinted_shop_locations:
shop_slots.append(slot_id)
if shop_slots:
if shop_scouts_enabled[0] == 2:
await ctx.send_msgs([{"cmd": "CreateHints", "locations": shop_slots, "player": ctx.slot}])
await snes_write(ctx, [(WRAM_START + 0x0770, bytes([0x00]))])
else:
prog_shops = []
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": shop_slots, "create_as_hint": 0}])
for location in shop_slots:
if location in ctx.locations_info:
self.hinted_shop_locations.append(location)
if ctx.locations_info[location].flags & 0x01:
prog_shops.append(location)
if prog_shops:
await ctx.send_msgs([{"cmd": "CreateHints", "locations": prog_shops, "player": ctx.slot}])
melody_data = f"{ctx.team}_{ctx.slot}_melody_status"
earth_power_data = f"{ctx.team}_{ctx.slot}_earthpower"
current_melodies = int.from_bytes(melody_table, "little")
earth_power_state = int.from_bytes(earth_power_absorbed, "little")
if melody_data not in ctx.stored_data or (ctx.stored_data[melody_data] != current_melodies) or (ctx.stored_data[earth_power_data] != earth_power_state):
await ctx.send_msgs([{
"cmd": "Set",
"key": melody_data,
"default": None,
"want_reply": True,
"operations": [{"operation": "replace", "value": int.from_bytes(melody_table, "little")}]},
{
"cmd": "Set",
"key": earth_power_data,
"default": None,
"want_reply": True,
"operations": [{"operation": "replace", "value": int.from_bytes(earth_power_absorbed, "little")}]
}])
# death link handling goes here
if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
send_deathlink = await snes_read(ctx, PLAYER_JUST_DIED_SEND_DEATHLINK, 1)
currently_dead = send_deathlink[0] != 0x00
if send_deathlink[0] != 0x00:
snes_buffered_write(ctx, PLAYER_JUST_DIED_SEND_DEATHLINK, bytes([0x00]))
await ctx.handle_deathlink_state(currently_dead)
new_checks = []
from .game_data.local_data import check_table
location_ram_data = await snes_read(ctx, WRAM_START + 0x9C00, 0x88)
shop_location_flags = await snes_read(ctx, WRAM_START + 0xB721, 0x41)
for loc_id, loc_data in check_table.items():
if loc_id not in ctx.locations_checked:
if loc_id >= 0xEB1000:
data = shop_location_flags[loc_data[0]]
else:
data = location_ram_data[loc_data[0]]
masked_data = data & (1 << loc_data[1])
bit_set = masked_data != 0
invert_bit = ((len(loc_data) >= 3) and loc_data[2])
if bit_set != invert_bit and loc_id in ctx.server_locations:
if text_open[0] == 0xFF or shop_scout[0]: # Don't check locations while in a textbox
new_checks.append(loc_id)
for new_check_id in new_checks:
ctx.locations_checked.add(new_check_id)
location = ctx.location_names.lookup_in_slot(new_check_id)
snes_logger.info(
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
await snes_write(ctx, [(WRAM_START + 0x0770, bytes([0]))])
if item_received[0] or special_received[0] != 0x00 or money_received[0] != 0x00: # If processing any item from the server
return
is_energylink_enabled = await snes_read(ctx, IS_ENERGYLINK_ENABLED, 1)
is_requesting_energy = await snes_read(ctx, WRAM_START + 0x0790, 1)
energy_withdrawal = await snes_read(ctx, WRAM_START + 0x0796, 4)
ctx.set_notify(f"EnergyLink{ctx.team}")
energy = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
exchange_rate = 1000000
if is_energylink_enabled[0]:
deposited_energy = int.from_bytes(outgoing_energy, byteorder="little")
if deposited_energy:
deposited_energy *= exchange_rate
await snes_write(ctx, [(MONEY_IN_BANK, (0x00).to_bytes(4, byteorder="little"))])
await ctx.send_msgs([{
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
[{"operation": "add", "value": deposited_energy},
{"operation": "max", "value": 0}]}])
if is_requesting_energy[0] and energy: # This is just to pull the current number for a display.
energy //= exchange_rate
if energy > 9999999:
energy = 9999999
cap_flag = await snes_read(ctx, WRAM_START + 0xB623, 1)
cap_flag = int.from_bytes(cap_flag)
cap_flag |= 0x20
await snes_write(ctx, [(WRAM_START + 0xB623, cap_flag.to_bytes(1, byteorder="little"))])
await snes_write(ctx, [(WRAM_START + 0x0792, int(energy).to_bytes(4, byteorder="little"))])
await snes_write(ctx, [(WRAM_START + 0x0790, (0x00).to_bytes(1, byteorder="little"))])
if any(energy_withdrawal) and energy:
withdrawal = int.from_bytes(energy_withdrawal, byteorder="little")
withdrawal *= exchange_rate
energy = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0) # Refresh the value
if withdrawal > energy:
energy_success = 2
withdrawal = energy
else:
energy_success = 1
await snes_write(ctx, [(WRAM_START + 0x97D0, (withdrawal // exchange_rate).to_bytes(4, byteorder="little"))])
await snes_write(ctx, [(WRAM_START + 0x0796, (0x00).to_bytes(4, byteorder="little"))])
await ctx.send_msgs([{
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
[{"operation": "add", "value": (withdrawal * -1)},
{"operation": "max", "value": 0}]}])
await snes_write(ctx, [(WRAM_START + 0x079A, energy_success.to_bytes(1, byteorder="little"))]) # Signal the game to continue
if cur_script[0]: # Stop items during cutscenes
return
recv_count = await snes_read(ctx, ITEMQUEUE_HIGH, 2)
recv_index = struct.unpack("H", recv_count)[0]
if recv_index < len(ctx.items_received):
item = ctx.items_received[recv_index]
item_id = (item.item - 0xEB0000)
recv_index += 1
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_names.lookup_in_slot(item.item), "red", "bold"),
color(ctx.player_names[item.player], 'yellow'),
ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
snes_buffered_write(ctx, ITEMQUEUE_HIGH, pack("H", recv_index))
if item_id <= 0xFD:
snes_buffered_write(ctx, WRAM_START + 0xB570, bytes([item_id]))
elif item_id in money_id_table:
snes_buffered_write(ctx, WRAM_START + 0xB5F1, bytes([list(money_id_table).index(item_id) + 1]))
else:
snes_buffered_write(ctx, WRAM_START + 0xB572, bytes([client_specials[item_id]]))
await snes_flush_writes(ctx)
def get_alias(alias: str, slot_name: str) -> str:
try:
index = alias.index(f" ({slot_name}")
except ValueError:
return alias
return alias[:index]

337
worlds/earthbound/Items.py Normal file
View File

@@ -0,0 +1,337 @@
from typing import Dict, Set, NamedTuple, Optional
from BaseClasses import ItemClassification
class ItemData(NamedTuple):
category: str
code: Optional[int]
classification: ItemClassification
amount: int = 1
item_table: Dict[str, ItemData] = {
"Franklin Badge": ItemData("Key Items", 0xEB0001, ItemClassification.progression),
"Teddy Bear": ItemData("Characters", 0xEB0002, ItemClassification.filler, 0),
"Super Plush Bear": ItemData("Characters", 0xEB0003, ItemClassification.useful, 0),
"Broken Machine": ItemData("Broken Items", 0xEB0004, ItemClassification.useful, 0),
"Broken Gadget": ItemData("Jeff Weapons", 0xEB0005, ItemClassification.useful, 0),
"Broken Air Gun": ItemData("Jeff Weapons", 0xEB0006, ItemClassification.filler, 0),
"Broken Spray Can": ItemData("Broken Items", 0xEB0007, ItemClassification.filler, 0),
"Broken Laser": ItemData("Jeff Weapons", 0xEB0008, ItemClassification.useful, 0),
"Broken Iron": ItemData("Broken Items", 0xEB0009, ItemClassification.filler, 0),
"Broken Pipe": ItemData("Broken Items", 0xEB000A, ItemClassification.useful, 0),
"Broken Cannon": ItemData("Jeff Weapons", 0xEB000B, ItemClassification.useful, 0),
"Broken Tube": ItemData("Broken Items", 0xEB000C, ItemClassification.useful, 0),
"Broken Bazooka": ItemData("Broken Items", 0xEB000D, ItemClassification.useful, 0),
"Broken Trumpet": ItemData("Broken Items", 0xEB000E, ItemClassification.filler, 0),
"Broken Harmonica": ItemData("Jeff Weapons", 0xEB000F, ItemClassification.useful, 0),
"Broken Antenna": ItemData("Jeff Weapons", 0xEB0010, ItemClassification.useful, 0),
"Cracked Bat": ItemData("Ness Weapons", 0xEB0011, ItemClassification.filler, 0),
"Tee Ball Bat": ItemData("Ness Weapons", 0xEB0012, ItemClassification.filler, 0),
"Sand Lot Bat": ItemData("Ness Weapons", 0xEB0013, ItemClassification.filler, 0),
"Minor League Bat": ItemData("Ness Weapons", 0xEB0014, ItemClassification.filler, 0),
"Mr. Baseball Bat": ItemData("Ness Weapons", 0xEB0015, ItemClassification.useful, 0),
"Big League Bat": ItemData("Ness Weapons", 0xEB00D5, ItemClassification.useful, 0),
"Hall of Fame Bat": ItemData("Ness Weapons", 0xEB0017, ItemClassification.useful, 0),
"Magicant Bat": ItemData("Ness Weapons", 0xEB0018, ItemClassification.useful),
"Legendary Bat": ItemData("Ness Weapons", 0xEB0019, ItemClassification.useful),
"Gutsy Bat": ItemData("Ness Weapons", 0xEB001A, ItemClassification.useful, 0),
"Casey Bat": ItemData("Ness Weapons", 0xEB001B, ItemClassification.filler, 0),
"Fry Pan": ItemData("Paula Weapons", 0xEB001C, ItemClassification.filler, 0),
"Thick Fry Pan": ItemData("Paula Weapons", 0xEB001D, ItemClassification.filler, 0),
"Deluxe Fry Pan": ItemData("Paula Weapons", 0xEB001E, ItemClassification.filler, 0),
"Chef's Fry Pan": ItemData("Paula Weapons", 0xEB001F, ItemClassification.useful, 0),
"French Fry Pan": ItemData("Paula Weapons", 0xEB0020, ItemClassification.useful, 0),
"Magic Fry Pan": ItemData("Paula Weapons", 0xEB0021, ItemClassification.useful, 0),
"Holy Fry Pan": ItemData("Paula Weapons", 0xEB0022, ItemClassification.useful, 0),
"Sword of Kings": ItemData("Poo Weapons", 0xEB0023, ItemClassification.useful, 0),
"Pop Gun": ItemData("Jeff Weapons", 0xEB0024, ItemClassification.filler),
"Stun Gun": ItemData("Jeff Weapons", 0xEB0025, ItemClassification.filler),
"Toy Air Gun": ItemData("Jeff Weapons", 0xEB0026, ItemClassification.filler, 0),
"Magnum Air Gun": ItemData("Jeff Weapons", 0xEB0027, ItemClassification.filler, 0),
"Zip Gun": ItemData("Jeff Weapons", 0xEB0028, ItemClassification.filler, 0),
"Laser Gun": ItemData("Jeff Weapons", 0xEB0029, ItemClassification.filler, 0),
"Hyper Beam": ItemData("Jeff Weapons", 0xEB002A, ItemClassification.useful, 0),
"Crusher Beam": ItemData("Jeff Weapons", 0xEB002B, ItemClassification.useful, 0),
"Spectrum Beam": ItemData("Jeff Weapons", 0xEB002C, ItemClassification.useful, 0),
"Death Ray": ItemData("Jeff Weapons", 0xEB002D, ItemClassification.useful),
"Baddest Beam": ItemData("Jeff Weapons", 0xEB002E, ItemClassification.useful, 0),
"Moon Beam Gun": ItemData("Jeff Weapons", 0xEB002F, ItemClassification.useful),
"Gaia Beam": ItemData("Jeff Weapons", 0xEB0030, ItemClassification.useful, 0),
"Yo-yo": ItemData("Alt Weapons", 0xEB0031, ItemClassification.filler, 0),
"Slingshot": ItemData("Alt Weapons", 0xEB0032, ItemClassification.filler, 0),
"Bionic Slingshot": ItemData("Alt Weapons", 0xEB0033, ItemClassification.filler, 0),
"Trick Yo-yo": ItemData("Alt Weapons", 0xEB0034, ItemClassification.filler, 0),
"Combat Yo-yo": ItemData("Alt Weapons", 0xEB0035, ItemClassification.filler, 0),
"Travel Charm": ItemData("Body Equipment", 0xEB0036, ItemClassification.filler),
"Great Charm": ItemData("Body Equipment", 0xEB0037, ItemClassification.filler),
"Crystal Charm": ItemData("Body Equipment", 0xEB0038, ItemClassification.filler, 0),
"Rabbit's Foot": ItemData("Body Equipment", 0xEB0039, ItemClassification.useful),
"Flame Pendant": ItemData("Body Equipment", 0xEB003A, ItemClassification.useful),
"Rain Pendant": ItemData("Body Equipment", 0xEB003B, ItemClassification.useful),
"Night Pendant": ItemData("Body Equipment", 0xEB003C, ItemClassification.useful),
"Sea Pendant": ItemData("Body Equipment", 0xEB003D, ItemClassification.useful),
"Star Pendant": ItemData("Body Equipment", 0xEB003E, ItemClassification.useful, 0),
"Cloak of Kings": ItemData("Poo Equipment", 0xEB003F, ItemClassification.useful),
"Cheap Bracelet": ItemData("Arm Equipment", 0xEB0040, ItemClassification.filler, 0),
"Copper Bracelet": ItemData("Arm Equipment", 0xEB0041, ItemClassification.filler, 0),
"Silver Bracelet": ItemData("Arm Equipment", 0xEB0042, ItemClassification.filler, 0),
"Gold Bracelet": ItemData("Arm Equipment", 0xEB0043, ItemClassification.filler, 0),
"Platinum Band": ItemData("Arm Equipment", 0xEB00D8, ItemClassification.useful),
"Diamond Band": ItemData("Arm Equipment", 0xEB00D9, ItemClassification.useful),
"Pixie's Bracelet": ItemData("Arm Equipment", 0xEB0046, ItemClassification.useful),
"Cherub's Band": ItemData("Arm Equipment", 0xEB0047, ItemClassification.useful),
"Goddess Band": ItemData("Arm Equipment", 0xEB0048, ItemClassification.useful),
"Bracer of Kings": ItemData("Poo Equipment", 0xEB0049, ItemClassification.useful),
"Baseball Cap": ItemData("Other Equipment", 0xEB004A, ItemClassification.filler, 0),
"Holmes Hat": ItemData("Other Equipment", 0xEB004B, ItemClassification.filler, 0),
"Mr. Baseball Cap": ItemData("Other Equipment", 0xEB004C, ItemClassification.filler, 0),
"Hard Hat": ItemData("Other Equipment", 0xEB004D, ItemClassification.filler, 0),
"Ribbon": ItemData("Ribbons", 0xEB004E, ItemClassification.filler, 0),
"Red Ribbon": ItemData("Ribbons", 0xEB004F, ItemClassification.filler, 0),
"Goddess Ribbon": ItemData("Ribbons", 0xEB0050, ItemClassification.useful, 0),
"Coin of Slumber": ItemData("Other Equipment", 0xEB0051, ItemClassification.useful),
"Coin of Defense": ItemData("Other Equipment", 0xEB0052, ItemClassification.useful, 0),
"Lucky Coin": ItemData("Other Equipment", 0xEB0053, ItemClassification.useful, 0),
"Talisman Coin": ItemData("Other Equipment", 0xEB0054, ItemClassification.useful, 0),
"Shiny Coin": ItemData("Other Equipment", 0xEB0055, ItemClassification.useful, 0),
"Souvenir Coin": ItemData("Other Equipment", 0xEB0056, ItemClassification.useful),
"Diadem of Kings": ItemData("Poo Equipment", 0xEB0057, ItemClassification.useful),
"Cookie": ItemData("Food", 0xEB0058, ItemClassification.filler, 0),
"Bag of Fries": ItemData("Food", 0xEB0059, ItemClassification.filler, 0),
"Hamburger": ItemData("Food", 0xEB005A, ItemClassification.filler, 0),
"Boiled Egg": ItemData("Food", 0xEB005B, ItemClassification.filler, 0),
"Fresh Egg": ItemData("Food", 0xEB005C, ItemClassification.filler, 0),
"Picnic Lunch": ItemData("Food", 0xEB005D, ItemClassification.filler, 0),
"Pasta di Summers": ItemData("Food", 0xEB005E, ItemClassification.filler, 0),
"Pizza": ItemData("Food", 0xEB005F, ItemClassification.filler, 0),
"Chef's Special": ItemData("Food", 0xEB0060, ItemClassification.filler, 0),
"Large Pizza": ItemData("Food", 0xEB0061, ItemClassification.filler, 0),
"PSI Caramel": ItemData("Food", 0xEB0062, ItemClassification.useful, 0),
"Magic Truffle": ItemData("Food", 0xEB0063, ItemClassification.useful, 0),
"Brain Food Lunch": ItemData("Food", 0xEB0064, ItemClassification.useful, 0),
"Rock Candy": ItemData("Food", 0xEB0065, ItemClassification.useful, 0),
"Croissant": ItemData("Food", 0xEB0066, ItemClassification.filler, 0),
"Bread Roll": ItemData("Food", 0xEB0067, ItemClassification.filler, 0),
"Pak of Bubble Gum": ItemData("Key Items", 0xEB0068, ItemClassification.progression),
"Jar of Fly Honey": ItemData("Key Items", 0xEB0069, ItemClassification.progression),
"Can of Fruit Juice": ItemData("Food", 0xEB006A, ItemClassification.filler, 0),
"Royal Iced Tea": ItemData("Food", 0xEB006B, ItemClassification.filler, 0),
"Protein Drink": ItemData("Food", 0xEB006C, ItemClassification.filler, 0),
"Kraken Soup": ItemData("Food", 0xEB006D, ItemClassification.filler, 0),
"Bottle of Water": ItemData("Food", 0xEB006E, ItemClassification.filler, 0),
"Cold Remedy": ItemData("Status Heal", 0xEB006F, ItemClassification.filler, 0),
"Vial of Serum": ItemData("Status Heal", 0xEB0070, ItemClassification.filler, 0),
"IQ Capsule": ItemData("Food", 0xEB0071, ItemClassification.useful, 0),
"Guts Capsule": ItemData("Food", 0xEB0072, ItemClassification.useful, 0),
"Speed Capsule": ItemData("Food", 0xEB0073, ItemClassification.useful, 0),
"Vital Capsule": ItemData("Food", 0xEB0074, ItemClassification.useful, 0),
"Luck Capsule": ItemData("Food", 0xEB0075, ItemClassification.useful, 0),
"Ketchup Packet": ItemData("Condiments", 0xEB0076, ItemClassification.filler, 0),
"Sugar Packet": ItemData("Condiments", 0xEB0077, ItemClassification.filler, 0),
"Tin of Cocoa": ItemData("Condiments", 0xEB0078, ItemClassification.filler, 0),
"Carton of Cream": ItemData("Condiments", 0xEB0079, ItemClassification.filler, 0),
"Sprig of Parsley": ItemData("Condiments", 0xEB007A, ItemClassification.filler, 0),
"Jar of Hot Sauce": ItemData("Condiments", 0xEB007B, ItemClassification.filler, 0),
"Salt Packet": ItemData("Condiments", 0xEB007C, ItemClassification.filler, 0),
"Tiny Key": ItemData("Key Items", 0xEB007D, ItemClassification.progression), # Progressive Gun
"Jar of Delisauce": ItemData("Condiments", 0xEB007E, ItemClassification.useful, 0),
"Wet Towel": ItemData("Status Heal", 0xEB007F, ItemClassification.filler, 0),
"Refreshing Herb": ItemData("Status Heal", 0xEB0080, ItemClassification.useful, 0),
"Secret Herb": ItemData("Status Heal", 0xEB0081, ItemClassification.useful, 0),
"Horn of Life": ItemData("Status Heal", 0xEB0082, ItemClassification.useful, 0),
"Counter-PSI Unit": ItemData("Jeff Items", 0xEB0083, ItemClassification.useful, 0),
"Shield Killer": ItemData("Jeff Items", 0xEB0084, ItemClassification.useful, 0),
"Bazooka": ItemData("Jeff Items", 0xEB0085, ItemClassification.useful, 0),
"Heavy Bazooka": ItemData("Jeff Items", 0xEB0086, ItemClassification.useful, 0),
"HP-Sucker": ItemData("Jeff Items", 0xEB0087, ItemClassification.useful),
"Hungry HP-Sucker": ItemData("Jeff Items", 0xEB0088, ItemClassification.useful, 0),
"Xterminator Spray": ItemData("Battle Items", 0xEB0089, ItemClassification.useful, 0),
"Slime Generator": ItemData("Jeff Items", 0xEB008A, ItemClassification.useful, 0),
"Yogurt Dispenser": ItemData("Key Items", 0xEB008B, ItemClassification.progression),
"Ruler": ItemData("Battle Items", 0xEB008C, ItemClassification.filler, 0),
"Snake Bag": ItemData("Battle Items", 0xEB008D, ItemClassification.filler, 0),
"Mummy Wrap": ItemData("Battle Items", 0xEB008E, ItemClassification.filler, 0),
"Protractor": ItemData("Battle Items", 0xEB008F, ItemClassification.filler, 0),
"Bottle Rocket": ItemData("Jeff Items", 0xEB0090, ItemClassification.filler, 0),
"Big Bottle Rocket": ItemData("Jeff Items", 0xEB0091, ItemClassification.useful, 0),
"Multi Bottle Rocket": ItemData("Jeff Items", 0xEB0092, ItemClassification.useful, 0),
"Bomb": ItemData("Battle Items", 0xEB0093, ItemClassification.filler, 0),
"Super Bomb": ItemData("Battle Items", 0xEB0094, ItemClassification.useful, 0),
"Insecticide Spray": ItemData("Battle Items", 0xEB0095, ItemClassification.filler, 0),
"Rust Promoter": ItemData("Battle Items", 0xEB0096, ItemClassification.filler, 0),
"Rust Promoter DX": ItemData("Battle Items", 0xEB0097, ItemClassification.useful, 0),
"Pair of Dirty Socks": ItemData("Battle Items", 0xEB0098, ItemClassification.filler, 0),
"Stag Beetle": ItemData("Battle Items", 0xEB0099, ItemClassification.filler, 0),
"Toothbrush": ItemData("Battle Items", 0xEB009A, ItemClassification.filler, 0),
"Handbag Strap": ItemData("Battle Items", 0xEB009B, ItemClassification.filler, 0),
"Pharaoh's Curse": ItemData("Battle Items", 0xEB009C, ItemClassification.filler, 0),
"Defense Shower": ItemData("Battle Items", 0xEB009D, ItemClassification.useful, 0),
"UFO Engine": ItemData("Key Items", 0xEB009E, ItemClassification.progression),
"Sudden Guts Pill": ItemData("Battle Items", 0xEB009F, ItemClassification.useful, 0),
"Bag of Dragonite": ItemData("Battle Items", 0xEB00A0, ItemClassification.useful, 0),
"Defense Spray": ItemData("Battle Items", 0xEB00A1, ItemClassification.filler, 0),
"Piggy Nose": ItemData("Key Items", 0xEB00A2, ItemClassification.progression),
"For Sale Sign": ItemData("Field Items", 0xEB00A3, ItemClassification.filler),
"Shyness Book": ItemData("Key Items", 0xEB00A4, ItemClassification.progression),
"Picture Postcard": ItemData("Field Items", 0xEB00A5, ItemClassification.filler, 0),
"King Banana": ItemData("Key Items", 0xEB00A6, ItemClassification.progression),
"Letter For Tony": ItemData("Key Items", 0xEB00A7, ItemClassification.progression),
"Chick": ItemData("Field Items", 0xEB00A8, ItemClassification.filler, 0),
"Chicken": ItemData("Field Items", 0xEB00A9, ItemClassification.filler, 0),
"Key to the Shack": ItemData("Key Items", 0xEB00AA, ItemClassification.progression),
"Key to the Cabin": ItemData("Key Items", 0xEB00AB, ItemClassification.progression),
"Bad Key Machine": ItemData("Key Items", 0xEB00AC, ItemClassification.progression),
# "Archipelago Item": ItemData("Key Items", 0xEB00AD, ItemClassification.progression, 0),
"Zombie Paper": ItemData("Key Items", 0xEB00AE, ItemClassification.progression),
"Hawk Eye": ItemData("Key Items", 0xEB00AF, ItemClassification.progression),
"Bicycle": ItemData("Key Items", 0xEB00B0, ItemClassification.useful),
"ATM Card": ItemData("Key Items", 0xEB00B1, ItemClassification.progression, 0),
"Show Ticket": ItemData("Key Items", 0xEB00B2, ItemClassification.filler, 0),
"Tenda Lavapants": ItemData("Key Items", 0xEB00B3, ItemClassification.progression), # Progressive Bat
"Wad of Bills": ItemData("Key Items", 0xEB00B4, ItemClassification.progression),
"Warp Pad": ItemData("Key Items", 0xEB00B5, ItemClassification.progression, 0),
"Diamond": ItemData("Key Items", 0xEB00B6, ItemClassification.progression),
"Signed Banana": ItemData("Key Items", 0xEB00B7, ItemClassification.progression),
"Pencil Eraser": ItemData("Key Items", 0xEB00B8, ItemClassification.progression),
"Hieroglyph Copy": ItemData("Key Items", 0xEB00B9, ItemClassification.progression),
"Meteotite": ItemData("Field Items", 0xEB00BA, ItemClassification.useful, 0),
"Contact Lens": ItemData("Key Items", 0xEB00BB, ItemClassification.progression),
"Hand-Aid": ItemData("Food", 0xEB00BC, ItemClassification.useful),
"Trout Yogurt": ItemData("Food", 0xEB00BD, ItemClassification.filler, 0),
"Banana": ItemData("Food", 0xEB00BE, ItemClassification.filler, 0),
"Calorie Stick": ItemData("Food", 0xEB00BF, ItemClassification.filler, 0),
"Key to the Tower": ItemData("Key Items", 0xEB00C0, ItemClassification.progression),
"Meteorite Piece": ItemData("Key Items", 0xEB00C1, ItemClassification.progression),
"Earth Pendant": ItemData("Body Equipment", 0xEB00C2, ItemClassification.useful, 0),
"Neutralizer": ItemData("Jeff Items", 0xEB00C3, ItemClassification.useful),
"Sound Stone": ItemData("Key Items", 0xEB00C4, ItemClassification.progression, 0),
"Exit Mouse": ItemData("Key Items", 0xEB00C5, ItemClassification.useful, 0),
"Gelato de Resort": ItemData("Food", 0xEB00C6, ItemClassification.filler, 0),
"Snake": ItemData("Battle Items", 0xEB00C7, ItemClassification.filler, 0),
"Viper": ItemData("Battle Items", 0xEB00C8, ItemClassification.filler, 0),
"Brain Stone": ItemData("Battle Items", 0xEB00C9, ItemClassification.filler),
"Police Badge": ItemData("Key Items", 0xEB00CA, ItemClassification.progression),
"Mining Permit": ItemData("Key Items", 0xEB00CB, ItemClassification.progression),
"Suporma": ItemData("Field Items", 0xEB00CC, ItemClassification.trap),
"Key to the Locker": ItemData("Key Items", 0xEB00CD, ItemClassification.progression),
"Insignificant Item": ItemData("Key Items", 0xEB00CE, ItemClassification.progression),
"Magic Tart": ItemData("Food", 0xEB00CF, ItemClassification.useful, 0),
"Tiny Ruby": ItemData("Key Items", 0xEB00D0, ItemClassification.progression),
"Monkey's Love": ItemData("Battle Items", 0xEB00D1, ItemClassification.useful),
"Eraser Eraser": ItemData("Key Items", 0xEB00D2, ItemClassification.progression),
"Tendakraut": ItemData("Key Items", 0xEB00D3, ItemClassification.progression),
"T-Rex's Bat": ItemData("Ness Weapons", 0xEB00D4, ItemClassification.useful, 0),
# "Big League Bat": ItemData("Ness Weapons", 0xEB0016, ItemClassification.useful, 0), Summers copy
"Ultimate Bat": ItemData("Ness Weapons", 0xEB00D6, ItemClassification.useful, 0),
"Double Beam": ItemData("Jeff Weapons", 0xEB00D7, ItemClassification.useful, 0),
# "Platinum Band": ItemData("Arm Equipment", 0xEB00D8, ItemClassification.useful, 0), Summers copy
# "Diamond Band": ItemData("Arm Equipment", 0xEB00D9, ItemClassification.useful, 0), Summers Copy
"Defense Ribbon": ItemData("Ribbons", 0xEB00DA, ItemClassification.useful, 0),
"Talisman Ribbon": ItemData("Ribbons", 0xEB00DB, ItemClassification.useful),
"Saturn Ribbon": ItemData("Ribbons", 0xEB00DC, ItemClassification.useful),
"Coin of Silence": ItemData("Other Equipment", 0xEB00DD, ItemClassification.useful, 0),
"Charm Coin": ItemData("Other Equipment", 0xEB00DE, ItemClassification.useful, 0),
"Cup of Noodles": ItemData("Food", 0xEB00DF, ItemClassification.filler, 0),
"Repel Sandwich": ItemData("Food", 0xEB00E0, ItemClassification.useful, 0),
"Repel Superwich": ItemData("Food", 0xEB00E1, ItemClassification.useful, 0),
"Lucky Sandwich": ItemData("Food", 0xEB00E2, ItemClassification.useful, 0),
"Progressive Bat": ItemData("Progressive Equipment", 0xEB00E3, ItemClassification.useful, 0),
"Progressive Fry Pan": ItemData("Progressive Equipment", 0xEB00E4, ItemClassification.useful, 0),
"Progressive Gun": ItemData("Progressive Equipment", 0xEB00E5, ItemClassification.useful, 0),
"Progressive Bracelet": ItemData("Progressive Equipment", 0xEB00E6, ItemClassification.useful, 0),
"Progressive Other": ItemData("Progressive Equipment", 0xEB00E7, ItemClassification.useful, 0),
"Cup of Coffee": ItemData("Food", 0xEB00E8, ItemClassification.filler, 0),
"Double Burger": ItemData("Food", 0xEB00E9, ItemClassification.filler, 0),
"Peanut Cheese Bar": ItemData("Food", 0xEB00EA, ItemClassification.filler, 0),
"Piggy Jelly": ItemData("Food", 0xEB00EB, ItemClassification.filler, 0),
"Bowl of Rice Gruel": ItemData("Food", 0xEB00EC, ItemClassification.filler, 0),
"Bean Croquette": ItemData("Food", 0xEB00ED, ItemClassification.filler, 0),
"Molokheiya Soup": ItemData("Food", 0xEB00EE, ItemClassification.filler, 0),
"Plain Roll": ItemData("Food", 0xEB00EF, ItemClassification.filler, 0),
"Kabob": ItemData("Food", 0xEB00F0, ItemClassification.filler, 0),
"Plain Yogurt": ItemData("Food", 0xEB00F1, ItemClassification.filler, 0),
"Beef Jerky": ItemData("Food", 0xEB00F2, ItemClassification.filler, 0),
"Mammoth Burger": ItemData("Food", 0xEB00F3, ItemClassification.filler, 0),
"Spicy Jerky": ItemData("Food", 0xEB00F4, ItemClassification.filler, 0),
"Luxury Jerky": ItemData("Food", 0xEB00F5, ItemClassification.filler, 0),
"Bottle of DXwater": ItemData("Food", 0xEB00F6, ItemClassification.useful, 0),
"Magic Pudding": ItemData("Food", 0xEB00F7, ItemClassification.useful, 0),
"Non-Stick Frypan": ItemData("Paula Weapons", 0xEB00F8, ItemClassification.useful, 0),
"Mr. Saturn Coin": ItemData("Other Equipment", 0xEB00F9, ItemClassification.useful),
"Meteornium": ItemData("Field Items", 0xEB00FA, ItemClassification.useful, 0),
"Popsicle": ItemData("Food", 0xEB00FB, ItemClassification.filler, 0),
"Cup of Lifenoodles": ItemData("Status Heal", 0xEB00FC, ItemClassification.useful, 0),
"Carrot Key": ItemData("Key Items", 0xEB00FD, ItemClassification.progression),
"Onett Teleport": ItemData("PSI", 0xEB00FE, ItemClassification.progression),
"Twoson Teleport": ItemData("PSI", 0xEB00FF, ItemClassification.progression),
"Happy-Happy Village Teleport": ItemData("PSI", 0xEB0100, ItemClassification.progression),
"Threed Teleport": ItemData("PSI", 0xEB0101, ItemClassification.progression),
"Saturn Valley Teleport": ItemData("PSI", 0xEB0102, ItemClassification.progression),
"Dusty Dunes Teleport": ItemData("PSI", 0xEB0103, ItemClassification.progression),
"Fourside Teleport": ItemData("PSI", 0xEB0104, ItemClassification.progression),
"Winters Teleport": ItemData("PSI", 0xEB0105, ItemClassification.progression),
"Summers Teleport": ItemData("PSI", 0xEB0106, ItemClassification.progression),
"Scaraba Teleport": ItemData("PSI", 0xEB0107, ItemClassification.progression),
"Dalaam Teleport": ItemData("PSI", 0xEB0108, ItemClassification.progression),
"Deep Darkness Teleport": ItemData("PSI", 0xEB0109, ItemClassification.progression),
"Tenda Village Teleport": ItemData("PSI", 0xEB010A, ItemClassification.progression),
"Lost Underworld Teleport": ItemData("PSI", 0xEB010B, ItemClassification.progression),
"Progressive Poo PSI": ItemData("PSI", 0xEB010C, ItemClassification.useful, 2),
"Magicant Teleport": ItemData("PSI", 0xEB010D, ItemClassification.progression),
"Paula": ItemData("Characters", 0xEB010E, ItemClassification.progression),
"Jeff": ItemData("Characters", 0xEB010F, ItemClassification.progression),
"Poo": ItemData("Characters", 0xEB0110, ItemClassification.progression),
"Flying Man": ItemData("Characters", 0xEB0111, ItemClassification.useful),
"Ness": ItemData("Characters", 0xEB0112, ItemClassification.progression),
"Photograph": ItemData("Photos", 0xEB0113, ItemClassification.trap, 0),
"$10": ItemData("Money", 0xEB0114, ItemClassification.filler, 0),
"$100": ItemData("Money", 0xEB0115, ItemClassification.filler, 0),
"$1000": ItemData("Money", 0xEB0116, ItemClassification.useful, 0),
'Threed Tunnels Clear': ItemData('Events', None, ItemClassification.progression, 0),
'Submarine to Deep Darkness': ItemData('Events', None, ItemClassification.progression, 0),
'Melody': ItemData('Events', None, ItemClassification.progression, 0),
'Saved Earth': ItemData('Events', None, ItemClassification.progression, 0),
"Power of the Earth": ItemData("Events", None, ItemClassification.progression, 0),
"Alternate Goal": ItemData("Events", None, ItemClassification.useful, 0),
"Valley Bridge Repair": ItemData("Events", None, ItemClassification.progression, 0),
"Magicant Unlock": ItemData("Events", None, ItemClassification.progression, 0),
"ATM Access": ItemData("Events", None, ItemClassification.progression, 0)
}
def get_item_names_per_category() -> Dict[str, Set[str]]:
categories: Dict[str, Set[str]] = {}
for name, data in item_table.items():
if data.category != "Events":
categories.setdefault(data.category, set()).add(name)
return categories

View File

@@ -0,0 +1,594 @@
from typing import List, Optional, NamedTuple, TYPE_CHECKING
from .Options import MagicantMode, ShopRandomizer
if TYPE_CHECKING:
from . import EarthBoundWorld
class LocationData(NamedTuple):
region: str
name: str
code: Optional[int]
def get_locations(world: "EarthBoundWorld") -> List[LocationData]:
location_table: List[LocationData] = [
LocationData("Northern Onett", "Onett - Tracy Gift", 0xEB0000),
LocationData("Northern Onett", "Onett - Tracy's Room Present", 0xEB0001),
LocationData("Northern Onett", "Onett - Hilltop Present", 0xEB0002),
LocationData("Northern Onett", "Onett - Meteor Item", 0xEB0003),
LocationData("Northern Onett", "Onett - Buzz Buzz", 0xEB0004),
LocationData("Northern Onett", "Onett - Mani Mani Statue", 0xEB0005),
LocationData("Onett", "Onett - Library Counter", 0xEB0006),
LocationData("Onett", "Onett - Library Bookshelf", 0xEB0007),
LocationData("Onett", "Onett - Burger Shop Trashcan", 0xEB0008),
LocationData("Onett", "Onett - Treehouse Guy", 0xEB0009),
LocationData("Onett", "Onett - South Road Present", 0xEB000A),
LocationData("Onett", "Onett - Hotel Trashcan", 0xEB000B),
LocationData("Onett", "Onett - Arcade Trashcan", 0xEB000C),
LocationData("Onett", "Onett - Mayor Pirkle", 0xEB000D),
LocationData("Onett", "Onett - Traveling Entertainer", 0xEB000E),
LocationData("Giant Step", "Giant Step - First Cave Present", 0xEB000F),
LocationData("Giant Step", "Giant Step - Floor 2 Cave Present", 0xEB0010),
LocationData("Giant Step", "Giant Step - Floor 3 Present", 0xEB0011),
LocationData("Twoson", "Twoson - Bike Shop Rental", 0xEB0012),
LocationData("Twoson", "Twoson - Antique Shop", 0xEB0013),
LocationData("Twoson", "Twoson - Paula's Room Present", 0xEB0014),
LocationData("Twoson", "Twoson - Apple Kid Trashcan", 0xEB0015),
LocationData("Twoson", "Twoson - South of Town Present", 0xEB0016),
LocationData("Twoson", "Twoson - Orange Kid Donation", 0xEB0017),
LocationData("Twoson", "Twoson - Apple Kid Invention", 0xEB0018),
LocationData("Twoson", "Twoson - Apple Kid's Mouse", 0xEB0019),
LocationData("Twoson", "Twoson - Paula's Mother", 0xEB001A),
LocationData("Everdred's House", "Twoson - Everdred Meeting", 0xEB001B),
LocationData("Twoson", "Twoson - Insignificant Location", 0xEB001C),
LocationData("Peaceful Rest Valley", "Peaceful Rest Valley - Split Hill Present", 0xEB001D),
LocationData("Peaceful Rest Valley", "Peaceful Rest Valley - Hill Nook Present", 0xEB001E),
LocationData("Peaceful Rest Valley", "Peaceful Rest Valley - South of Bridge Present", 0xEB001F),
LocationData("Peaceful Rest Valley", "Peaceful Rest Valley - Dead End Present", 0xEB0020),
LocationData("Peaceful Rest Valley", "Peaceful Rest Valley - River Overlook Present", 0xEB0021),
LocationData("Peaceful Rest Valley", "Peaceful Rest Valley - North Side Present", 0xEB0022),
LocationData("Happy-Happy Village", "Happy-Happy Village - Donation Lady", 0xEB0023),
LocationData("Happy-Happy HQ", "Happy-Happy Village - Right HQ Present", 0xEB0024),
LocationData("Happy-Happy HQ", "Happy-Happy Village - Left HQ Present", 0xEB0025),
LocationData("Happy-Happy Village", "Happy-Happy Village - Prisoner Item", 0xEB0026),
LocationData("Happy-Happy Village", "Happy-Happy Village - Prisoner", 0xEB0027),
LocationData("Happy-Happy HQ", "Happy-Happy Village - Defeat Carpainter", 0xEB0028),
LocationData("Lilliput Steps", "Lilliput Steps - Southwest Pool Present", 0xEB0029),
LocationData("Lilliput Steps", "Lilliput Steps - East Cliff Present", 0xEB002A),
LocationData("Lilliput Steps", "Lilliput Steps - North Stream Present", 0xEB002B),
LocationData("Boogey Tent", "Threed - Boogey Tent Trashcan", 0xEB002C),
LocationData("Threed", "Threed - Cemetery Trashcan", 0xEB002D),
LocationData("Threed", "Threed - Downtown Trashcan", 0xEB002E),
LocationData("Threed", "Threed - East Side Trashcan", 0xEB002F),
LocationData("Threed", "Threed - Northeast Shack Trashcan", 0xEB0030),
LocationData("Threed", "Threed - Hospital Drawer", 0xEB0031),
LocationData("Threed", "Threed - Zombie Prisoner", 0xEB0032),
LocationData("Threed Underground", "Threed Underground - Left Coffin", 0xEB0033),
LocationData("Threed Underground", "Threed Underground - Right Coffin", 0xEB0034),
LocationData("Grapefruit Falls", "Grapefruit Falls - South Present", 0xEB0035),
LocationData("Grapefruit Falls", "Grapefruit Falls - North Present", 0xEB0036),
LocationData("Grapefruit Falls", "Grapefruit Falls - Saturn Cave Present", 0xEB0037),
LocationData("Saturn Valley", "Saturn Valley - Ladder Present", 0xEB0038),
LocationData("Saturn Valley", "Saturn Valley - Trashcan #1", 0xEB0039),
LocationData("Saturn Valley", "Saturn Valley - Trashcan #2", 0xEB003A),
LocationData("Saturn Valley", "Saturn Valley - Trashcan #3", 0xEB003B),
LocationData("Upper Saturn Valley", "Saturn Valley - Saturn Coffee", 0xEB003C),
LocationData("Saturn Valley", "Saturn Valley - Post Belch Gift #1", 0xEB003D),
LocationData("Saturn Valley", "Saturn Valley - Post Belch Gift #2", 0xEB003E),
LocationData("Saturn Valley", "Saturn Valley - Post Belch Gift #3", 0xEB003F),
LocationData("Milky Well", "Milky Well - Cavern Present", 0xEB0040),
LocationData("Belch's Factory", "Belch's Factory - Top Right Room Trashcan", 0xEB0041),
LocationData("Belch's Factory", "Belch's Factory - Pit Room Trashcan #1", 0xEB0042),
LocationData("Belch's Factory", "Belch's Factory - Pit Room Trashcan #2", 0xEB0043),
LocationData("Belch's Factory", "Belch's Factory - Balcony Room Trashcan #1", 0xEB0044),
LocationData("Belch's Factory", "Belch's Factory - Balcony Room Trashcan #2", 0xEB0045),
LocationData("Belch's Factory", "Belch's Factory - Balcony Room Trashcan #3", 0xEB0046),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Northwest Corner Present", 0xEB0047),
LocationData("Dusty Dunes Desert", "Dusty Dunes - South Side Present", 0xEB0048),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Surrounding Rocks Present", 0xEB0049),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Black Sesame Present", 0xEB004A),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Oasis Present", 0xEB004B),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Northeast Corner Present", 0xEB004C),
LocationData("Dusty Dunes Desert", "Dusty Dunes - North Central Present", 0xEB004D),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Shining Spot", 0xEB004E),
LocationData("Dusty Dunes Desert", "Dusty Dunes - East Peninsula Present", 0xEB004F),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Reward", 0xEB0050),
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #1", 0xEB0051),
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #2", 0xEB0052),
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #3", 0xEB0053),
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #4", 0xEB0054),
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #5", 0xEB0055),
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #6", 0xEB0056),
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #7", 0xEB0057),
LocationData("Scaraba", "Scaraba - Snake Bag Salesman", 0xEB0058),
LocationData("Snow Wood Boarding School", "Snow Wood - Upper Right Locker", 0xEB0059),
LocationData("Snow Wood Boarding School", "Snow Wood - Upper Left Locker", 0xEB005A),
LocationData("Snow Wood Boarding School", "Snow Wood - Bottom Right Locker", 0xEB005B),
LocationData("Snow Wood Boarding School", "Snow Wood - Bottom Left Locker", 0xEB005C),
LocationData("Snow Wood Boarding School", "Snow Wood - Maxwell Item", 0xEB005D),
LocationData("Snow Wood Boarding School", "Snow Wood - Bedroom", 0xEB005E),
LocationData("Winters", "Winters - Drugstore Saleswoman", 0xEB005F),
LocationData("Brickroad Maze", "Brick Road Maze - Top Path Present", 0xEB0060),
LocationData("Brickroad Maze", "Brick Road Maze - Guarded Present", 0xEB0061),
LocationData("Brickroad Maze", "Brick Road Maze - Out of the Way Present", 0xEB0062),
LocationData("Brickroad Maze", "Brick Road Maze - Alcove Present", 0xEB0063),
LocationData("Brickroad Maze", "Brick Road Maze - Near Exit Present", 0xEB0064),
LocationData("Rainy Circle", "Rainy Circle - Isolated Present", 0xEB0065),
LocationData("Rainy Circle", "Rainy Circle - East Cliff Present", 0xEB0066),
LocationData("Rainy Circle", "Rainy Circle - Near Ropes Present", 0xEB0067),
LocationData("Andonuts Lab Area", "Andonuts Lab - Present", 0xEB0068),
LocationData("Andonuts Lab Area", "Andonuts Lab - Mouse", 0xEB0069),
LocationData("Stonehenge Base", "Stonehenge - Purple Maze Present", 0xEB006A),
LocationData("Stonehenge Base", "Stonehenge - Dead End Present", 0xEB006B),
LocationData("Stonehenge Base", "Stonehenge - Near End of the Maze Present", 0xEB006C),
LocationData("Stonehenge Base", "Stonehenge - Bridge Room East Balcony Present", 0xEB006D),
LocationData("Stonehenge Base", "Stonehenge - Bridge Room Lower Present", 0xEB006E),
LocationData("Stonehenge Base", "Stonehenge - Flashing Room Right Path Present", 0xEB006F),
LocationData("Stonehenge Base", "Stonehenge - Flashing Room Center Present", 0xEB0070),
LocationData("Stonehenge Base", "Stonehenge - Flashing Room Upper Present", 0xEB0071),
LocationData("Stonehenge Base", "Stonehenge - Kidnapped Mr. Saturn", 0xEB0072),
LocationData("Stonehenge Base", "Stonehenge - Tony Item", 0xEB0073),
LocationData("Gold Mine", "Gold Mine - Mouse Crossroad Present #1", 0xEB0074),
LocationData("Gold Mine", "Gold Mine - Mouse Crossroad Present #2", 0xEB0075),
LocationData("Gold Mine", "Gold Mine - B1F Lonely Mole Present", 0xEB0076),
LocationData("Gold Mine", "Gold Mine - South Hall Present", 0xEB0077),
LocationData("Gold Mine", "Gold Mine - South Corner Present", 0xEB0078),
LocationData("Gold Mine", "Gold Mine - South Mole Present #1", 0xEB0079),
LocationData("Gold Mine", "Gold Mine - South Mole Present #2", 0xEB007A),
LocationData("Gold Mine", "Gold Mine - North Crossroad Detour Present", 0xEB007B),
LocationData("Gold Mine", "Gold Mine - North Mole Present", 0xEB007C),
LocationData("Gold Mine", "Gold Mine - West Mole Present", 0xEB007D),
LocationData("Gold Mine", "Gold Mine - B1F Isolated Present", 0xEB007E),
LocationData("Gold Mine", "Gold Mine - West Crossroad Detour Present", 0xEB007F),
LocationData("Gold Mine", "Gold Mine - B1F Junction Present", 0xEB0080),
LocationData("Gold Mine", "Gold Mine - B1F Junction Mole Present", 0xEB0081),
LocationData("Monkey Caves", "Monkey Caves - 1F Right Chest", 0xEB00F1),
LocationData("Monkey Caves", "Monkey Caves - 1F Left Chest", 0xEB00F2),
LocationData("Monkey Caves", "Monkey Caves - West 2F Left Chest", 0xEB00F3),
LocationData("Monkey Caves", "Monkey Caves - West 2F Right Chest #1", 0xEB00F4),
LocationData("Monkey Caves", "Monkey Caves - West 2F Right Chest #2", 0xEB00F5),
LocationData("Monkey Caves", "Monkey Caves - East 2F Left Chest", 0xEB00F6),
LocationData("Monkey Caves", "Monkey Caves - East 2F Right Chest", 0xEB00F7),
LocationData("Monkey Caves", "Monkey Caves - East West 3F Right Chest #1", 0xEB00F8),
LocationData("Monkey Caves", "Monkey Caves - East West 3F Right Chest #2", 0xEB00F9),
LocationData("Monkey Caves", "Monkey Caves - West End Chest", 0xEB0082),
LocationData("Monkey Caves", "Monkey Caves - West End Trashcan", 0xEB0083),
LocationData("Monkey Caves", "Monkey Caves - East End Chest", 0xEB0084),
LocationData("Monkey Caves", "Monkey Caves - East End Trashcan", 0xEB0085),
LocationData("Monkey Caves", "Monkey Caves - Bow Monkey Gift", 0xEB0086),
LocationData("Monkey Caves", "Monkey Caves - Talah Rama Chest #1", 0xEB0087),
LocationData("Monkey Caves", "Monkey Caves - Talah Rama Chest #2", 0xEB0088),
LocationData("Monkey Caves", "Monkey Caves - Talah Rama Gift", 0xEB0089),
LocationData("Monkey Caves", "Monkey Caves - Monkey Power", 0xEB008A),
LocationData("Fourside", "Fourside - Venus Gift", 0xEB008B),
LocationData("Moonside", "Fourside - Post-Moonside Delivery", 0xEB008C),
LocationData("Fourside", "Fourside - Bakery 2F Gift", 0xEB008D),
LocationData("Moonside", "Moonside - Two Trees Present", 0xEB008E),
LocationData("Moonside", "Moonside - East Island Present", 0xEB008F),
LocationData("Moonside", "Moonside - Businessman Present", 0xEB0090),
LocationData("Moonside", "Moonside - West Island Present", 0xEB0091),
LocationData("Moonside", "Moonside - Hospital Present", 0xEB0092),
LocationData("Fourside Dept. Store", "Fourside - Department Store Blackout", 0xEB0093),
LocationData("Magnet Hill", "Magnet Hill - West Entrance Trashcan", 0xEB0094),
LocationData("Magnet Hill", "Magnet Hill - First Room Free Door Trashcan", 0xEB0095),
LocationData("Magnet Hill", "Magnet Hill - First Room Barrel Door Trashcan", 0xEB0096),
LocationData("Magnet Hill", "Magnet Hill - Second Room Dead End Trashcan", 0xEB0097),
LocationData("Magnet Hill", "Magnet Hill - Final Room Door Trashcan", 0xEB0098),
LocationData("Magnet Hill", "Fourside - Magnet Hill Chest", 0xEB0099),
LocationData("Monotoli Building", "Monotoli Building - One Table Present", 0xEB009A),
LocationData("Monotoli Building", "Monotoli Building - Two Table Present", 0xEB009B),
LocationData("Monotoli Building", "Monotoli Building - Electra Gift", 0xEB009C),
LocationData("Monotoli Building", "Monotoli Building - Monotoli Gift", 0xEB009D),
LocationData("Monotoli Building", "Monotoli Building - Monotoli Character", 0xEB009E),
LocationData("Summers Museum", "Summers - Museum Item", 0xEB009F),
LocationData("Summers", "Summers - Magic Cake", 0xEB00A0),
LocationData("Dalaam", "Dalaam - Throne Room Chest #1", 0xEB00A1),
LocationData("Dalaam", "Dalaam - Throne Room Chest #2", 0xEB00A2),
LocationData("Dalaam", "Dalaam - Throne Room Chest #3", 0xEB00A3),
LocationData("Dalaam", "Dalaam - Trial of Mu", 0xEB00A4),
LocationData("Dalaam", "Dalaam - Restaurant Chest #1", 0xEB00A5),
LocationData("Dalaam", "Dalaam - Restaurant Chest #2", 0xEB00A6),
LocationData("Dalaam", "Dalaam - Do Do Guy's House Chest", 0xEB00A7),
LocationData("Dalaam", "Dalaam - Upper House Chest", 0xEB00A8),
LocationData("Dalaam", "Dalaam - Throne Character", 0xEB00A9),
LocationData("Ness's Mind", "Poo - Starting Item", 0xEB00AA),
LocationData("Pink Cloud", "Pink Cloud - Three Holes Present", 0xEB00AB),
LocationData("Pink Cloud", "Pink Cloud - Left Hole Present", 0xEB00AC),
LocationData("Pink Cloud", "Pink Cloud - Ground Floor Present", 0xEB00AD),
LocationData("Pyramid", "Pyramid - Anteroom Sarcophagus", 0xEB00AE),
LocationData("Pyramid", "Pyramid - Northwest Door Sarcophagus", 0xEB00AF),
LocationData("Pyramid", "Pyramid - Hallway Sarcophagus #1", 0xEB00B0),
LocationData("Pyramid", "Pyramid - Hallway Sarcophagus #2", 0xEB00B1),
LocationData("Pyramid", "Pyramid - Switch Room Sarcophagus", 0xEB00B2),
LocationData("Pyramid", "Pyramid - Pedestal Item", 0xEB00B3),
LocationData("Pyramid", "Pyramid - Way Out Sarcophagus", 0xEB00B4),
LocationData("Southern Scaraba", "Scaraba - Star Master", 0xEB00B5),
LocationData("Southern Scaraba", "Scaraba - Key Holder", 0xEB00B6),
LocationData("Dungeon Man", "Dungeon Man - 1F Dead End Present", 0xEB00B7),
LocationData("Dungeon Man", "Dungeon Man - 1F Long Walk Present", 0xEB00B8),
LocationData("Dungeon Man", "Dungeon Man - 1F Disappointing Present", 0xEB00B9),
LocationData("Dungeon Man", "Dungeon Man - 1F Opinion Present", 0xEB00BA),
LocationData("Dungeon Man", "Dungeon Man - 1F No Sign Present", 0xEB00BB),
LocationData("Dungeon Man", "Dungeon Man - 2F Unnecessary Billboard Present", 0xEB00BC),
LocationData("Dungeon Man", "Dungeon Man - 2F Dungeon Exploration Present", 0xEB00BD),
LocationData("Dungeon Man", "Dungeon Man - 2F South Ledge Present", 0xEB00BE),
LocationData("Dungeon Man", "Dungeon Man - 2F North Alcove Present", 0xEB00BF),
LocationData("Dungeon Man", "Dungeon Man - 3F Present", 0xEB00C0),
LocationData("Dungeon Man", "Dungeon Man - 2F Hole Present", 0xEB00C1),
LocationData("Dungeon Man", "Dungeon Man - 1F Exit Ledge Present", 0xEB00C2),
LocationData("Deep Darkness", "Deep Darkness - Teleporting Monkey", 0xEB00C3),
LocationData("Deep Darkness", "Deep Darkness - Crest of Darkness Present", 0xEB00C4),
LocationData("Deep Darkness Darkness", "Deep Darkness - Helicopter Present", 0xEB00C5),
LocationData("Deep Darkness Darkness", "Deep Darkness - Yellow Bird Present", 0xEB00C6),
LocationData("Deep Darkness Darkness", "Deep Darkness - Swamp Present", 0xEB00C7),
LocationData("Deep Darkness Darkness", "Deep Darkness - Corner Present", 0xEB00C8),
LocationData("Deep Darkness Darkness", "Deep Darkness - Alcove Present", 0xEB00C9),
LocationData("Deep Darkness Darkness", "Deep Darkness - North Alcove Truffle", 0xEB00CA),
LocationData("Deep Darkness Darkness", "Deep Darkness - Near Land Truffle", 0xEB00CB),
LocationData("Deep Darkness Darkness", "Deep Darkness - Present Truffle", 0xEB00CC),
LocationData("Deep Darkness Darkness", "Deep Darkness - Village Truffle", 0xEB00CD),
LocationData("Deep Darkness Darkness", "Deep Darkness - Entrance Truffle", 0xEB00CE),
LocationData("Deep Darkness Darkness", "Deep Darkness - Barf Character", 0xEB00CF),
LocationData("Tenda Village", "Tenda Village - Trashcan", 0xEB00D0),
LocationData("Tenda Village", "Tenda Village - Tenda Tea", 0xEB00D1),
LocationData("Tenda Village", "Tenda Village - Tenda Gift", 0xEB00D2),
LocationData("Tenda Village", "Tenda Village - Tenda Gift #2", 0xEB00D3),
LocationData("Lumine Hall", "Lumine Hall - B1F Non-Talkative Rock Present", 0xEB00D4),
LocationData("Lumine Hall", "Lumine Hall - 1F North Path Present", 0xEB00D5),
LocationData("Lumine Hall", "Lumine Hall - B1F Thankful Rock Corner Present", 0xEB00D6),
LocationData("Lumine Hall", "Lumine Hall - B1F Thankful Rock Junction Present", 0xEB00D7),
LocationData("Lumine Hall", "Lumine Hall - 1F Above Belly Button Present", 0xEB00D8),
LocationData("Lumine Hall", "Lumine Hall - B1F Belly Button Present", 0xEB00D9),
LocationData("Lumine Hall", "Lumine Hall - 1F Near Exit Present", 0xEB00DA),
LocationData("Lumine Hall", "Lumine Hall - 1F Dead End Present", 0xEB00DB),
LocationData("Lumine Hall", "Lumine Hall - B1F West Alcove Present", 0xEB00DC),
LocationData("Lost Underworld", "Lost Underworld - Talking Rock", 0xEB00DD),
LocationData("Lost Underworld", "Lost Underworld - East Present", 0xEB00DE),
LocationData("Lost Underworld", "Lost Underworld - Northeast Present", 0xEB00DF),
LocationData("Lost Underworld", "Lost Underworld - Northeast of Tenda Tribe Present", 0xEB00E0),
LocationData("Lost Underworld", "Lost Underworld - Southwest of Tenda Tribe Present", 0xEB00E1),
LocationData("Lost Underworld", "Lost Underworld - Evacuation Present", 0xEB00E2),
LocationData("Fire Spring", "Fire Spring - 1st Cave Present", 0xEB00E3),
LocationData("Fire Spring", "Fire Spring - East Corner Present", 0xEB00E4),
LocationData("Fire Spring", "Fire Spring - Volcano Present", 0xEB00E5),
LocationData("Fire Spring", "Fire Spring - Lone Cave Present", 0xEB00E6),
LocationData("Fire Spring", "Fire Spring - Upper Volcano Present", 0xEB00E7),
LocationData("Cave of the Present", "Cave of the Present - Star Master", 0xEB00EE),
LocationData("Cave of the Present", "Cave of the Present - Broken Phase Distorter", 0xEB00EF),
LocationData("Happy-Happy HQ", "Carpainter Defeated", None),
LocationData("Belch's Factory", "Belch Defeated", None),
LocationData("Dungeon Man", "Dungeon Man Submarine", None),
LocationData("Giant Step", "Giant Step Sanctuary", None),
LocationData("Lilliput Steps", "Lilliput Steps Sanctuary", None),
LocationData("Milky Well", "Milky Well Sanctuary", None),
LocationData("Rainy Circle", "Rainy Circle Sanctuary", None),
LocationData("Magnet Hill", "Magnet Hill Sanctuary", None),
LocationData("Pink Cloud", "Pink Cloud Sanctuary", None),
LocationData("Lumine Hall", "Lumine Hall Sanctuary", None),
LocationData("Fire Spring", "Fire Spring Sanctuary", None),
LocationData("Ness's Mind", "Sanctuary Goal", None),
LocationData("Global ATM Access", "Any ATM", None)
]
if world.options.giygas_required:
location_table += [
LocationData("Cave of the Past", "Cave of the Past - Present", 0xEB00F0),
LocationData("Endgame", "Giygas", None),
]
if world.options.alternate_sanctuary_goal:
location_table += [
LocationData("Ness's Mind", "+2 Sanctuaries", None)
]
if world.options.magicant_mode in range(1, 3):
location_table += [
LocationData("Sea of Eden", "Magicant - Ness's Nightmare", None),
]
if not world.options.magicant_mode:
location_table += [
LocationData("Sea of Eden", "Magicant - Ness's Nightmare", 0xEB00ED),
]
if world.options.magicant_mode < MagicantMode.option_alternate_goal:
location_table += [
LocationData("Magicant", "Magicant - Ness's Gift", 0xEB00E8),
LocationData("Magicant", "Magicant - Present Near Ness", 0xEB00E9),
LocationData("Magicant", "Magicant - Lonely Present", 0xEB00EA),
LocationData("Magicant", "Magicant - North Present", 0xEB00EB),
LocationData("Magicant", "Magicant - Hills Present", 0xEB00EC),
LocationData("Magicant", "Magicant - Town Present", 0xEB00FA)
]
if world.options.magicant_mode == MagicantMode.option_alternate_goal:
location_table += [
LocationData("Ness's Mind", "+1 Sanctuary", None)
]
if world.options.shop_randomizer == ShopRandomizer.option_shopsanity:
location_table += [
LocationData("Onett", "Onett Drugstore - Right Counter Slot 1", 0xeb1000),
LocationData("Onett", "Onett Drugstore - Right Counter Slot 2", 0xeb1001),
LocationData("Onett", "Onett Drugstore - Right Counter Slot 3", 0xeb1002),
LocationData("Onett", "Onett Drugstore - Right Counter Slot 4", 0xeb1003),
LocationData("Onett", "Onett Drugstore - Right Counter Slot 5", 0xeb1004),
LocationData("Onett", "Onett Drugstore - Left Counter", 0xeb1007),
LocationData("Summers", "Summers - Beach Cart", 0xeb100e),
LocationData("Onett", "Onett Burger Shop - Slot 1", 0xeb1015),
LocationData("Onett", "Onett Burger Shop - Slot 2", 0xeb1016),
LocationData("Onett", "Onett Burger Shop - Slot 3", 0xeb1017),
LocationData("Onett", "Onett Burger Shop - Slot 4", 0xeb1018),
LocationData("Onett", "Onett Bakery - Slot 1", 0xeb101c),
LocationData("Onett", "Onett Bakery - Slot 2", 0xeb101d),
LocationData("Onett", "Onett Bakery - Slot 3", 0xeb101e),
LocationData("Onett", "Onett Bakery - Slot 4", 0xeb101f),
LocationData("Twoson", "Twoson Department Store Burger Shop - Slot 1", 0xeb1023),
LocationData("Twoson", "Twoson Department Store Burger Shop - Slot 2", 0xeb1024),
LocationData("Twoson", "Twoson Department Store Burger Shop - Slot 3", 0xeb1025),
LocationData("Twoson", "Twoson Department Store Burger Shop - Slot 4", 0xeb1026),
LocationData("Twoson", "Twoson Department Store Bakery - Slot 1", 0xeb102a),
LocationData("Twoson", "Twoson Department Store Bakery - Slot 2", 0xeb102b),
LocationData("Twoson", "Twoson Department Store Bakery - Slot 3", 0xeb102c),
LocationData("Twoson", "Twoson Department Store Bakery - Slot 4", 0xeb102d),
LocationData("Twoson", "Twoson Department Store Top Floor - Right Counter Slot 1", 0xeb1031),
LocationData("Twoson", "Twoson Department Store Top Floor - Right Counter Slot 2", 0xeb1032),
LocationData("Twoson", "Twoson Department Store Top Floor - Right Counter Slot 3", 0xeb1033),
LocationData("Twoson", "Twoson Department Store Top Floor - Right Counter Slot 4", 0xeb1034),
LocationData("Twoson", "Twoson Department Store Top Floor - Right Counter Slot 5", 0xeb1035),
LocationData("Twoson", "Twoson Department Store Top Floor - Right Counter Slot 6", 0xeb1036),
LocationData("Twoson", "Twoson Department Store Top Floor - Left Counter Slot 1", 0xeb1038),
LocationData("Twoson", "Twoson Department Store Top Floor - Left Counter Slot 2", 0xeb1039),
LocationData("Summers", "Summers - Magic Cake Cart Shop Slot", 0xeb103f),
LocationData("Twoson", "Burglin Park Junk Shop - Slot 1", 0xeb1046),
LocationData("Twoson", "Burglin Park Junk Shop - Slot 2", 0xeb1047),
LocationData("Twoson", "Burglin Park Junk Shop - Slot 3", 0xeb1048),
LocationData("Twoson", "Burglin Park Junk Shop - Slot 4", 0xeb1049),
LocationData("Twoson", "Burglin Park Junk Shop - Slot 5", 0xeb104a),
LocationData("Twoson", "Burglin Park Junk Shop - Slot 6", 0xeb104b),
LocationData("Twoson", "Burglin Park Bread Stand - Slot 1", 0xeb105b),
LocationData("Twoson", "Burglin Park Bread Stand - Slot 2", 0xeb105c),
LocationData("Twoson", "Burglin Park Bread Stand - Slot 3", 0xeb105d),
LocationData("Twoson", "Burglin Park Bread Stand - Slot 4", 0xeb105e),
LocationData("Twoson", "Burglin Park Bread Stand - Slot 5", 0xeb105f),
LocationData("Twoson", "Burglin Park Bread Stand - Slot 6", 0xeb1060),
LocationData("Twoson", "Burglin Park - Banana Stand", 0xeb1062),
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Right Counter Slot 1", 0xeb1069),
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Right Counter Slot 2", 0xeb106a),
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Right Counter Slot 3", 0xeb106b),
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Right Counter Slot 4", 0xeb106c),
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Right Counter Slot 5", 0xeb106d),
LocationData("Threed", "Threed Drugstore - Right Counter Slot 1", 0xeb1070),
LocationData("Threed", "Threed Drugstore - Right Counter Slot 2", 0xeb1071),
LocationData("Threed", "Threed Drugstore - Right Counter Slot 3", 0xeb1072),
LocationData("Threed", "Threed Drugstore - Right Counter Slot 4", 0xeb1073),
LocationData("Threed", "Threed Drugstore - Right Counter Slot 5", 0xeb1074),
LocationData("Threed", "Threed Drugstore - Left Counter Slot 1", 0xeb1077),
LocationData("Threed", "Threed Drugstore - Left Counter Slot 2", 0xeb1078),
LocationData("Threed", "Threed Drugstore - Left Counter Slot 3", 0xeb1079),
LocationData("Threed", "Threed Drugstore - Left Counter Slot 4", 0xeb107a),
LocationData("Threed", "Threed Drugstore - Left Counter Slot 5", 0xeb107b),
LocationData("Threed", "Threed - Arms Dealer Slot 1", 0xeb107e),
LocationData("Threed", "Threed - Arms Dealer Slot 2", 0xeb107f),
LocationData("Threed", "Threed - Arms Dealer Slot 3", 0xeb1080),
LocationData("Threed", "Threed - Arms Dealer Slot 4", 0xeb1081),
LocationData("Threed", "Threed Bakery - Slot 1", 0xeb1085),
LocationData("Threed", "Threed Bakery - Slot 2", 0xeb1086),
LocationData("Threed", "Threed Bakery - Slot 3", 0xeb1087),
LocationData("Threed", "Threed Bakery - Slot 4", 0xeb1088),
LocationData("Threed", "Threed Bakery - Slot 5", 0xeb1089),
LocationData("Threed", "Threed Bakery - Slot 6", 0xeb108a),
LocationData("Threed", "Threed Bakery - Slot 7", 0xeb108b),
LocationData("Scaraba", "Scaraba - Expensive Water Guy", 0xeb108c),
LocationData("Winters", "Winters Drugstore - Slot 1", 0xeb1093),
LocationData("Winters", "Winters Drugstore - Slot 2", 0xeb1094),
LocationData("Winters", "Winters Drugstore - Slot 3", 0xeb1095),
LocationData("Winters", "Winters Drugstore - Slot 4", 0xeb1096),
LocationData("Winters", "Winters Drugstore - Slot 5", 0xeb1097),
LocationData("Winters", "Winters Drugstore - Slot 6", 0xeb1098),
LocationData("Winters", "Winters Drugstore - Slot 7", 0xeb1099),
LocationData("Saturn Valley", "Saturn Valley Shop - Center Saturn Slot 1", 0xeb109a),
LocationData("Saturn Valley", "Saturn Valley Shop - Center Saturn Slot 2", 0xeb109b),
LocationData("Saturn Valley", "Saturn Valley Shop - Center Saturn Slot 3", 0xeb109c),
LocationData("Saturn Valley", "Saturn Valley Shop - Center Saturn Slot 4", 0xeb109d),
LocationData("Saturn Valley", "Saturn Valley Shop - Center Saturn Slot 5", 0xeb109e),
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Counter Slot 1", 0xeb10a1),
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Counter Slot 2", 0xeb10a2),
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Counter Slot 3", 0xeb10a3),
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Counter Slot 4", 0xeb10a4),
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Counter Slot 5", 0xeb10a5),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Arms Dealer Slot 1", 0xeb10a8),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Arms Dealer Slot 2", 0xeb10a9),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Arms Dealer Slot 3", 0xeb10aa),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Arms Dealer Slot 4", 0xeb10ab),
LocationData("Fourside", "Fourside Bakery - Slot 1", 0xeb10af),
LocationData("Fourside", "Fourside Bakery - Slot 2", 0xeb10b0),
LocationData("Fourside", "Fourside Bakery - Slot 3", 0xeb10b1),
LocationData("Fourside", "Fourside Bakery - Slot 4", 0xeb10b2),
LocationData("Fourside", "Fourside Bakery - Slot 5", 0xeb10b3),
LocationData("Fourside", "Fourside Bakery - Slot 6", 0xeb10b4),
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 1", 0xeb10b6),
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 2", 0xeb10b7),
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 3", 0xeb10b8),
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 4", 0xeb10b9),
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 5", 0xeb10ba),
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 6", 0xeb10bb),
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 7", 0xeb10bc),
LocationData("Fourside", "Fourside Department Store - Shop Shop Slot 1", 0xeb10bd),
LocationData("Fourside", "Fourside Department Store - Shop Shop Slot 2", 0xeb10be),
LocationData("Fourside", "Fourside Department Store - Shop Shop Slot 3", 0xeb10bf),
LocationData("Fourside", "Fourside Department Store - Shop Shop Slot 4", 0xeb10c0),
LocationData("Fourside", "Fourside Department Store - Food Shop Slot 1", 0xeb10c4),
LocationData("Fourside", "Fourside Department Store - Food Shop Slot 2", 0xeb10c5),
LocationData("Fourside", "Fourside Department Store - Food Shop Slot 3", 0xeb10c6),
LocationData("Fourside", "Fourside Department Store - Food Shop Slot 4", 0xeb10c7),
LocationData("Fourside", "Fourside Department Store - Food Shop Slot 5", 0xeb10c8),
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 1", 0xeb10cb),
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 2", 0xeb10cc),
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 3", 0xeb10cd),
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 4", 0xeb10ce),
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 5", 0xeb10cf),
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 6", 0xeb10d0),
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 7", 0xeb10d1),
LocationData("Fourside", "Fourside Department Store - Toys Shop Slot 1", 0xeb10d2),
LocationData("Fourside", "Fourside Department Store - Toys Shop Slot 2", 0xeb10d3),
LocationData("Fourside", "Fourside Department Store - Toys Shop Slot 3", 0xeb10d4),
LocationData("Fourside", "Fourside Department Store - Toys Shop Slot 4", 0xeb10d5),
LocationData("Fourside", "Fourside Department Store - Toys Shop Slot 5", 0xeb10d6),
LocationData("Fourside", "Fourside Department Store - Toys Shop Slot 6", 0xeb10d7),
LocationData("Fourside", "Fourside Department Store - Sports Shop Slot 1", 0xeb10d9),
LocationData("Fourside", "Fourside Department Store - Sports Shop Slot 2", 0xeb10da),
LocationData("Fourside", "Fourside Department Store - Sports Shop Slot 3", 0xeb10db),
LocationData("Fourside", "Fourside Department Store - Sports Shop Slot 4", 0xeb10dc),
LocationData("Fourside", "Fourside Department Store - Burger Shop Slot 1", 0xeb10e0),
LocationData("Fourside", "Fourside Department Store - Burger Shop Slot 2", 0xeb10e1),
LocationData("Fourside", "Fourside Department Store - Burger Shop Slot 3", 0xeb10e2),
LocationData("Fourside", "Fourside Department Store - Burger Shop Slot 4", 0xeb10e3),
LocationData("Fourside", "Fourside Department Store - Burger Shop Slot 5", 0xeb10e4),
LocationData("Fourside", "Fourside Department Store - Arms Dealer Slot 1", 0xeb10e7),
LocationData("Fourside", "Fourside Department Store - Arms Dealer Slot 2", 0xeb10e8),
LocationData("Fourside", "Fourside Department Store - Arms Dealer Slot 3", 0xeb10e9),
LocationData("Fourside", "Fourside Department Store - Arms Dealer Slot 4", 0xeb10ea),
LocationData("Fourside", "Fourside Department Store - Arms Dealer Slot 5", 0xeb10eb),
LocationData("Fourside", "Fourside - Northeast Alley Junk Shop Slot 1", 0xeb10ee),
LocationData("Fourside", "Fourside - Northeast Alley Junk Shop Slot 2", 0xeb10ef),
LocationData("Fourside", "Fourside - Northeast Alley Junk Shop Slot 3", 0xeb10f0),
LocationData("Fourside", "Fourside - Northeast Alley Junk Shop Slot 4", 0xeb10f1),
LocationData("Summers", "Summers - Scam Shop Slot 1", 0xeb1103),
LocationData("Summers", "Summers - Scam Shop Slot 2", 0xeb1104),
LocationData("Summers", "Summers - Scam Shop Slot 3", 0xeb1105),
LocationData("Summers", "Summers - Scam Shop Slot 4", 0xeb1106),
LocationData("Summers", "Summers - Scam Shop Slot 5", 0xeb1107),
LocationData("Summers", "Summers - Scam Shop Slot 6", 0xeb1108),
LocationData("Summers", "Summers - Scam Shop Slot 7", 0xeb1109),
LocationData("Summers", "Summers Harbor - Shop Slot 1", 0xeb110a),
LocationData("Summers", "Summers Harbor - Shop Slot 2", 0xeb110b),
LocationData("Summers", "Summers Harbor - Shop Slot 3", 0xeb110c),
LocationData("Summers", "Summers Harbor - Shop Slot 4", 0xeb110d),
LocationData("Summers", "Summers Harbor - Shop Slot 5", 0xeb110e),
LocationData("Summers", "Summers Harbor - Shop Slot 6", 0xeb110f),
LocationData("Summers", "Summers Harbor - Shop Slot 7", 0xeb1110),
LocationData("Summers", "Summers Restaurant - Slot 1", 0xeb1111),
LocationData("Summers", "Summers Restaurant - Slot 2", 0xeb1112),
LocationData("Summers", "Summers Restaurant - Slot 3", 0xeb1113),
LocationData("Summers", "Summers Restaurant - Slot 4", 0xeb1114),
LocationData("Summers", "Summers Restaurant - Slot 5", 0xeb1115),
LocationData("Summers", "Summers Restaurant - Slot 6", 0xeb1116),
LocationData("Scaraba", "Scaraba - Indoors Shop Slot 1", 0xeb1118),
LocationData("Scaraba", "Scaraba - Indoors Shop Slot 2", 0xeb1119),
LocationData("Scaraba", "Scaraba - Indoors Shop Slot 3", 0xeb111a),
LocationData("Scaraba", "Scaraba - Indoors Shop Slot 4", 0xeb111b),
LocationData("Scaraba", "Scaraba - Indoors Shop Slot 5", 0xeb111c),
LocationData("Scaraba", "Scaraba - Indoors Shop Slot 6", 0xeb111d),
LocationData("Scaraba", "Scaraba Bazaar - Red Snake Carpet Slot 1", 0xeb1126),
LocationData("Scaraba", "Scaraba Bazaar - Red Snake Carpet Slot 2", 0xeb1127),
LocationData("Scaraba", "Scaraba Bazaar - Red Snake Carpet Slot 3", 0xeb1128),
LocationData("Scaraba", "Scaraba Bazaar - Bottom Left Carpet Slot 1", 0xeb112d),
LocationData("Scaraba", "Scaraba Bazaar - Bottom Left Carpet Slot 2", 0xeb112e),
LocationData("Scaraba", "Scaraba Bazaar - Bottom Left Carpet Slot 3", 0xeb112f),
LocationData("Scaraba", "Scaraba Bazaar - Bottom Left Carpet Slot 4", 0xeb1130),
LocationData("Scaraba", "Scaraba Bazaar - Bottom Left Carpet Slot 5", 0xeb1131),
LocationData("Scaraba", "Scaraba Bazaar - Bottom Left Carpet Slot 6", 0xeb1132),
LocationData("Scaraba", "Scaraba Hotel - Arms Dealer Slot 1", 0xeb1134),
LocationData("Scaraba", "Scaraba Hotel - Arms Dealer Slot 2", 0xeb1135),
LocationData("Scaraba", "Scaraba Hotel - Arms Dealer Slot 3", 0xeb1136),
LocationData("Scaraba", "Scaraba Hotel - Arms Dealer Slot 4", 0xeb1137),
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 1", 0xeb113b),
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 2", 0xeb113c),
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 3", 0xeb113d),
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 4", 0xeb113e),
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 5", 0xeb113f),
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 6", 0xeb1140),
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 7", 0xeb1141),
LocationData("Saturn Valley", "Saturn Valley Shop - Post-Belch Saturn Slot 1", 0xeb1157),
LocationData("Saturn Valley", "Saturn Valley Shop - Post-Belch Saturn Slot 2", 0xeb1158),
LocationData("Saturn Valley", "Saturn Valley Shop - Post-Belch Saturn Slot 3", 0xeb1159),
LocationData("Saturn Valley", "Saturn Valley Shop - Post-Belch Saturn Slot 4", 0xeb115a),
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 1", 0xeb115e),
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 2", 0xeb115f),
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 3", 0xeb1160),
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 4", 0xeb1161),
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 5", 0xeb1162),
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 6", 0xeb1163),
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 7", 0xeb1164),
LocationData("Deep Darkness", "Deep Darkness - Arms Dealer Slot 1", 0xeb1165),
LocationData("Deep Darkness", "Deep Darkness - Arms Dealer Slot 2", 0xeb1166),
LocationData("Deep Darkness", "Deep Darkness - Arms Dealer Slot 3", 0xeb1167),
LocationData("Deep Darkness", "Deep Darkness - Arms Dealer Slot 4", 0xeb1168),
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 1", 0xeb116c),
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 2", 0xeb116d),
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 3", 0xeb116e),
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 4", 0xeb116f),
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 5", 0xeb1170),
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 6", 0xeb1171),
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 7", 0xeb1172),
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 1", 0xeb117a),
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 2", 0xeb117b),
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 3", 0xeb117c),
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 4", 0xeb117d),
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 5", 0xeb117e),
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 6", 0xeb117f),
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 7", 0xeb1180),
LocationData("Grapefruit Falls", "Grapefruit Falls - Hiker Shop Slot 1", 0xeb1181),
LocationData("Grapefruit Falls", "Grapefruit Falls - Hiker Shop Slot 2", 0xeb1182),
LocationData("Grapefruit Falls", "Grapefruit Falls - Hiker Shop Slot 3", 0xeb1183),
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 1", 0xeb1188),
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 2", 0xeb1189),
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 3", 0xeb118a),
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 4", 0xeb118b),
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 5", 0xeb118c),
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 6", 0xeb118d),
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 7", 0xeb118e),
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 1", 0xeb118f),
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 2", 0xeb1190),
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 3", 0xeb1191),
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 4", 0xeb1192),
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 5", 0xeb1193),
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 6", 0xeb1194),
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 7", 0xeb1195),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 1", 0xeb1196),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 2", 0xeb1197),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 3", 0xeb1198),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 4", 0xeb1199),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 5", 0xeb119a),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 6", 0xeb119b),
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 7", 0xeb119c),
LocationData("Moonside", "Moonside Hotel - Shop Slot 1", 0xeb119d),
LocationData("Moonside", "Moonside Hotel - Shop Slot 2", 0xeb119e),
LocationData("Moonside", "Moonside Hotel - Shop Slot 3", 0xeb119f),
LocationData("Moonside", "Moonside Hotel - Shop Slot 4", 0xeb11a0),
LocationData("Moonside", "Moonside Hotel - Shop Slot 5", 0xeb11a1),
LocationData("Dalaam", "Dalaam Restaurant - Slot 1", 0xeb11a4),
LocationData("Dalaam", "Dalaam Restaurant - Slot 2", 0xeb11a5),
LocationData("Dalaam", "Dalaam Restaurant - Slot 3", 0xeb11a6),
LocationData("Dalaam", "Dalaam Restaurant - Slot 4", 0xeb11a7),
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 1", 0xeb11ab),
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 2", 0xeb11ac),
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 3", 0xeb11ad),
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 4", 0xeb11ae),
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 5", 0xeb11af),
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 6", 0xeb11b0),
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 7", 0xeb11b1),
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 1", 0xeb11b2),
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 2", 0xeb11b3),
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 3", 0xeb11b4),
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 4", 0xeb11b5),
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 5", 0xeb11b6),
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 6", 0xeb11b7),
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 7", 0xeb11b8),
LocationData("Andonuts Lab Area", "Andonuts Lab - Caveman Shop Slot 1", 0xeb11c0),
LocationData("Andonuts Lab Area", "Andonuts Lab - Caveman Shop Slot 2", 0xeb11c1),
LocationData("Andonuts Lab Area", "Andonuts Lab - Caveman Shop Slot 3", 0xeb11c2),
LocationData("Andonuts Lab Area", "Andonuts Lab - Caveman Shop Slot 4", 0xeb11c3),
LocationData("Andonuts Lab Area", "Andonuts Lab - Caveman Shop Slot 5", 0xeb11c4)
]
if world.options.magicant_mode < MagicantMode.option_alternate_goal:
location_table += [
LocationData("Magicant", "Magicant - Shop Slot 1", 0xeb10f5),
LocationData("Magicant", "Magicant - Shop Slot 2", 0xeb10f6)
]
return location_table

View File

@@ -0,0 +1,625 @@
from dataclasses import dataclass
from Options import (Toggle, DefaultOnToggle, DeathLink, Choice, Range, PerGameCommonOptions, StartInventoryPool,
OptionGroup, FreeText, Visibility, PlandoBosses)
from .modules.boss_shuffle import boss_plando_keys
class GiygasRequired(DefaultOnToggle):
"""If enabled, your goal will be to defeat Giygas at the Cave of the Past.
If disabled, your goal will either complete automatically upon completing
enough Sanctuaries, or completing Magicant if it is required."""
display_name = "Giygas Required"
class SanctuariesRequired(Range):
"""How many of the eight "Your Sanctuary" locations are required to be cleared."""
display_name = "Required Sanctuaries"
range_start = 1
range_end = 8
default = 4
class SanctuaryAltGoal(Toggle):
"""If enabled, you will be able to win by completing 2 more Sanctuaries than are required.
Does nothing if 7 or more Sanctuaries are required, or if Magicant and Giygas are not required."""
display_name = "Sanctuary Alternate Goal"
class MagicantMode(Choice):
"""PSI Location: You will be able to find a Magicant teleport item. Ness's Nightmare contains a PSI location, and no stat boost.
Required: You will unlock the Magicant Teleport upon reaching your Sanctuary goal. If Giygas is required, beating Ness's Nightmare will unlock the Cave of the Past and grant a party-wide stat boost. Otherwise, Ness's Nightmare will finish your game.
Alternate Goal: You will unlock the Magicant Teleport upon reaching one more Sanctuary than required. Beating Ness's Nightmare will finish your game. Does nothing if Giygas is not required, or if 8 Sanctuaries are required. Magicant locations are removed from the multiworld, but contain random junk for yourself.
Optional Boost: You will be able to find a Magicant teleport item. Beating Ness's Nightmare will grant a party-wide stat boost. Magicant locations are removed from the multiworld, but contain random junk for yourself.
Removed: Magicant will be completely inacessible."""
display_name = "Magicant Mode"
option_psi_location = 0
option_required = 1
option_alternate_goal = 2
option_optional_boost = 3
option_removed = 4
default = 0
class MonkeyCavesMode(Choice):
"""Chests: Items required to finish the Monkey Caves will be forcibly placed on the chests that can be found in-between rooms of the monkey caves. The "reward" locations, usually found at the end of a branch, are still random. If you waste chest items, they will need to be replaced via the methods in hunt mode.
Hunt: Items required to finish the Monkey Caves will needsell you every minor item needed to be found outside. They can be obtained from the Dusty Dunes drugstore, the Fourside department store, and the pizza shop in either Twoson or Threed.
Shop: The monkey outside the Monkey Caves will sell every item needed to complete the caves and is not affected by shop randomization.
Solved: The Monkey Caves monkeys will already be moved out of the way and not require any items."""
display_name = "Monkey Caves Mode"
option_chests = 0
option_hunt = 1
option_shop = 2
option_solved = 3
default = 1
class ShortenPrayers(DefaultOnToggle):
"""If enabled, the Prayer cutscenes while fighting Giygas will be skipped, excluding the final one."""
display_name = "Skip Prayer Sequences"
class RandomStartLocation(Toggle):
"""If disabled, you will always start at Ness's house with no teleports unlocked.
If enabled, you will start at a random teleport destination with one teleport unlocked.
Additionally, you will need to fight Captain Strong to access the north part of Onett if this is enabled."""
display_name = "Random Starting Location"
class LocalTeleports(Toggle):
"""Forces all teleports and Poo PSI to be placed locally in your world."""
display_name = "Local Teleports"
class CharacterShuffle(Choice):
"""Shuffled: Characters will be shuffled amongst Character Locations. Extra locations will have Flying Man, a Teddy Bear, or a Super Plush Bear.
Anywhere: Characters can be found anywhere in the multiworld, and character locations will have regular checks.
See the Game Page for more information on Character Locations."""
display_name = "Character Shuffle"
option_shuffled = 0
option_anywhere = 1
default = 0
class PSIShuffle(Choice):
"""None: Characters will learn their normal PSI skills.
Basic: Offensive and Assist PSI will be shuffled. Recovery PSI is not modified. Ness's Favorite Thing will be named Wave in other slots.
Extended: Basic shuffle, but includes Jeff gadgets and some combat items.
See the Game Page for more information."""
display_name = "PSI Shuffle"
option_none = 0
option_basic = 1
option_extended = 2
class BossShuffle(PlandoBosses):
"""Shuffles boss encounters amongst each other."""
display_name = "Boss Shuffle"
option_false = 0
option_true = 1
default = 0
bosses = boss_plando_keys
locations = boss_plando_keys
duplicate_bosses = False
@classmethod
def can_place_boss(cls, boss: str, location: str) -> bool:
return True
class DecoupleDiamondDog(Toggle):
"""Shuffles Diamond Dog as a boss separate from Carbon Dog. Carbon Dog will transform into a random boss.
Does nothing if Boss Shuffle is disabled."""
display_name = "Decouple Diamond Dog"
class ShuffleGiygas(Toggle):
"""Adds the standalone Giygas fight to the shuffled boss pool.
This only applies to the second phase Giygas. The prayer fight is not affected.
Does nothing if Boss Shuffle is disabled."""
display_name = "Add Giygas to Boss Pool"
class BanFlashFavorite(Toggle):
"""If enabled, allows PSI Flash to be shuffled onto the Favorite Thing PSI slot. Can be quite annoying early-game.
Does nothing if PSI Shuffle is set to None."""
display_name = "Flash as Favorite"
class PreFixItems(Toggle):
"""If enabled, broken items in the multiworld pool will be replaced with their fixed versions.
This does not affect any items that are not placed by the multiworld."""
display_name = "Prefixed Items"
class AutoscaleParty(Toggle):
"""If enabled, joining party members will be scaled to roughly the level of the sphere they were obtained in."""
display_name = "Autoscale Party Members"
class ProgressiveWeapons(Toggle):
"""If enabled, Bats, Fry Pans, and Guns will be progressive. Does not apply to items dropped by enemies or found in shops."""
display_name = "Progressive Weapons"
class ProgressiveArmor(Toggle):
"""If enabled, Bracelets and items for the Other slot besides Ribbons will be progressive. Does not apply to items dropped by enemies or found in shops."""
display_name = "Progressive Armor"
class PresentSprites(DefaultOnToggle):
"""If enabled, Presents, Trash cans, and chests will have their appearance modified to be indicative of the item they contain."""
display_name = "Match Present Sprites"
class NoAPPresents(Toggle):
"""If enabled, present that contain items for other players will appear as EarthBound presents (trashcan, present, and chest) instead of Archipelago boxes.
Does nothing if Presents Match Contents is disabled."""
class ShuffleDrops(Toggle):
"""If enabled, enemies will drop random filler items. This does not put checks on enemy drops.
Drop rates are unchanged."""
display_name = "Shuffle Drops"
class RandomFranklinBadge(Toggle):
"""If enabled, the Franklin Badge will reflect a randomly selected attack type. The type can be determined from the item's name, as well as the help
text for it. The badge's function outside of battle will not change, and neither will its name outside of the game itself."""
display_name = "Franklin Badge Protection"
class CommonWeight(Range):
"""Weight for placing a common filler item."""
display_name = "Common Filler Weight"
range_start = 1
range_end = 100
default = 80
class UncommonWeight(Range):
"""Weight for placing an uncommon filler item."""
display_name = "Uncommon Filler Weight"
range_start = 1
range_end = 100
default = 30
class RareWeight(Range):
"""Weight for placing a rare filler item."""
display_name = "Rare Filler Weight"
range_start = 0
range_end = 100
default = 5
class MoneyWeight(Range):
"""Weight for placing money in the item pool."""
display_name = "Money Weight"
range_start = 0
range_end = 100
default = 0
class ExperienceModifier(Range):
"""Percentage of EXP enemies give you. 100 is vanilla, after scaling, and 300 is x3."""
display_name = "Experience Percentage"
range_start = 100
range_end = 300
default = 150
class StartingMoney(Range):
"""How much money you start with."""
display_name = "Starting Money"
range_start = 0
range_end = 99999
default = 20
class EasyDeaths(DefaultOnToggle):
"""Fully revives and heals all party members after death. If off, only Ness will be healed with 0 PP."""
display_name = "Easy Deaths"
class RandomFlavors(DefaultOnToggle):
"""Randomizes the non-plain window color options."""
display_name = "Random Flavors"
class DeathLinkMode(Choice):
"""Controls how receiving a Deathlink functions in battle.
Instant: The player will be instantly defeated.
Mortal: All characters will receieve mortal damage. The player will not be able to heal until the battle is finished.
Mortal Mercy: All characters will receieve mortal damage, but the player will be able to heal it before they die.
Regardless of this setting, receiving a deathlink outside of battle will always instantly defeat the player."""
display_name = "Death Link Mode"
option_instant = 0
option_mortal = 1
option_mortal_mercy = 2
default = 1
class RandomBattleBG(Toggle):
"""Generates random battle backgrounds."""
display_name = "Randomize Battle Backgrounds"
class RandomSwirlColors(Toggle):
"""Generates random colors for pre-battle swirls."""
display_name = "Randomize Swirl Colors"
class RemoteItems(Toggle):
"""If enabled, you will receive your own items from the server upon collecting them, rather than locally.
This allows co-op within the same game, and protects against loss of save data.
However, you will not be able to play offline if this is enabled."""
display_name = "Remote Items"
class PlandoLumineHallText(FreeText):
"""Set text to be displayed at Lumine Hall. If nothing is entered, random community-submitted text will be selected instead."""
display_name = "Lumine Hall Text Plando"
visibility = Visibility.none
class Armorizer(Choice):
"""All equippable armor will have randomly generated attributes. This includes who can equip it, elemental resistance (and how strong that resistance is),
defense, and the secondary stat it increases (Either Luck or Speed, depending on armor slot.) Choosing "Help!" from the Goods menu will give you exact details
on that piece of equipment.
Keep Type: Equipment will keep its original equipment slot. If Progressive Armor is enabled, you will get armor with progressively higher defense.
Chaos: Equipment will have a randomly selected slot. It will try to respect the defense progressively, but the type may not match the type received."""
display_name = "Armorizer"
option_off = 0
option_keep_type = 1
option_chaos = 2
default = 0
class Weaponizer(Choice):
"""All weapons will have randomly generated attributes. This includes offense, guts boost, and miss rate.
Keep Type: Equipment will keep the character that was originally able to use it. If Progressive Weapons is enabled, you will get weapons with progressively higher offense.
Chaos: Equipment will be able to be equipped by a randomly selected character. It will try to respect the offense progresively, but the type may not match the type recieved.
The Tee Ball Bat will always be a weapon for Ness."""
display_name = "Weaponizer"
option_off = 0
option_keep_type = 1
option_chaos = 2
default = 0
class ElementChance(Range):
"""Percent chance for any given Body/Other equipment to have elemental protection.
Affects Armorizer only."""
display_name = "Elemental Resistance Chance"
range_start = 1
range_end = 50
default = 15
class NoFreeSancs(Toggle):
"""If enabled, the entrance to Lilliput Steps and Fire Spring will be locked and require extra key items to access.
These items are the Tiny Key and Tenda Lavapants, respectively."""
display_name = "No Free Sanctuaries"
class RandomizeFanfares(Choice):
"""Randomizes fanfares."""
display_name = "Randomize Fanfares"
option_off = 0
option_on = 1
option_on_no_sound_stone_fanfares = 2
default = 0
class RandomizeBattleMusic(Toggle):
"""Randomizes in-battle songs."""
display_name = "Randomize Battle Music"
class RandomizeOverworldMusic(Choice):
"""Randomizes music on the overworld. Some sound effects might sound weird.
Normal: Does not randomize music.
Match Type: Music will be randomized with similar song categories (Town, dungeon, etc.)
Full: Overworld music will be randomized disregarding categories."""
display_name = "Overworld Music Randomizer"
option_normal = 0
option_match_type = 1
option_full = 2
default = 0
class RandomizePSIPalettes(Choice):
"""Randomizes the colors of PSI spells.
Normal: Doesn't randomize PSI colors.
Shuffled: PSI spell palettes are swapped around with each other.
Randomized: PSI spells use completely random colors."""
display_name = "Random PSI Palettes"
option_normal = 0
option_shuffled = 1
option_randomized = 2
default = 0
class ShopRandomizer(Choice):
"""Randomizes items in shops.
Local Filler: Shops contain only random items for yourself and are not checks.
Shopsanity. Every shop slot in the game contains a Multiworld location. ONLY ENABLE SHOPSANITY IF YOU KNOW WHAT YOU ARE DOING."""
display_name = "Shop Randomizer"
option_off = 0
option_local_filler = 1
option_shopsanity = 2
default = 0
class ScoutShopChecks(Choice):
"""Scouts Shop checks when you open a shop. Only affects shops in Shopsanity mode."""
display_name = "Scout Shop Checks"
option_off = 0
option_progression_only = 1
option_all = 2
default = 1
class StartingCharacter(Choice):
"""Sets which character you start as. Each character will always start with the ability to teleport,
and the ATM card. Ness will not be required to fight Sanctuary bosses."""
display_name = "Starting Character"
option_Ness = 0
option_Paula = 1
option_Jeff = 2
option_Poo = 3
default = 0
class EquipamizerStatCap(DefaultOnToggle):
"""If enabled, the highest value that Equipamizer can roll for a piece of equipment's
main stat will be capped. 80 for armor, 125 for weapons.
If disabled, the main stat can potentially roll up to 128."""
display_name = "Equipamizer Stat Cap"
class MoneyDropMultiplier(Range):
"""Multiplies money dropped by enemies by the chosen value."""
display_name = "Money Drop Multiplier"
range_start = 1
range_end = 100
default = 1
class EnemyShuffle(Toggle):
"""Shuffles Non-boss enemies amongst each other."""
display_name = "Enemy Shuffle"
class SkipEpilogue(Toggle):
"""If enabled, the choice to play the epilogue after beating Giygas will be removed, and you will
go directly to the credits. This option is mainly for no-release seeds where checks could be
potentially spoiled in the open-access epilogue."""
display_name = "Skip Epilogue"
visibility = Visibility.template
class EnergyLink(Toggle):
"""If enabled, the money in the ATM will be linked across the Archipelago Server.
This requires a server connection to be used, but won't break offline play."""
display_name = "Energy Link"
class DungeonShuffle(Toggle):
"""Shuffles Dungeon entrances amongst each other."""
display_name = "Dungeon Shuffle"
class PhotoCount(Range):
"""How many Photograph traps are placed in the item pool."""
display_name = "Photos in pool"
range_start = 0
range_end = 32
default = 20
class EasyCombat(Toggle):
"""Automatically halves all scaled enemy levels."""
display_name = "Easy Combat"
class EnemizerStats(Toggle):
"""Randomizes base stats and level of non-boss enemies."""
display_name = "Randomize Enemy Stats"
class EnemizerAttacks(Toggle):
"""Randomizes attacks of non-boss enemies."""
display_name = "Randomize Enemy Attacks"
class EnemizerAttributes(Toggle):
"""Randomizes most attributes of non-boss enemies."""
display_name = "Randomize Enemy Attributes"
class RandomMapColors(Choice):
"""Randomizes map colors.
Normal: Uses normal colors
Nice: Uses generally good looking palettes for maps with little artifacting.
Ugly: Allows map palettes with artifacting or colors that may not look good.
Nonsense: Allows really bad palettes or heavy artifacting."""
display_name = "Shuffle Map Palettes"
option_normal = 0
option_nice = 1
option_ugly = 2
option_nonsense = 3
default = 0
class SafeFinalBoss(DefaultOnToggle):
"""Prevents specific difficult bosses from being randomized onto Heavily Armed Pokey's boss slot.
Only affects Boss Shuffle, and does not affect Phase 2 Giygas if Boss Shuffle Add Giygas is enabled."""
display_name = "Safe Final Boss"
@dataclass
class EBOptions(PerGameCommonOptions):
giygas_required: GiygasRequired
sanctuaries_required: SanctuariesRequired
skip_prayer_sequences: ShortenPrayers
random_start_location: RandomStartLocation
alternate_sanctuary_goal: SanctuaryAltGoal
magicant_mode: MagicantMode
monkey_caves_mode: MonkeyCavesMode
local_teleports: LocalTeleports
character_shuffle: CharacterShuffle
starting_character: StartingCharacter
psi_shuffle: PSIShuffle
allow_flash_as_favorite_thing: BanFlashFavorite
enemy_shuffle: EnemyShuffle
boss_shuffle: BossShuffle
decouple_diamond_dog: DecoupleDiamondDog
boss_shuffle_add_giygas: ShuffleGiygas
safe_final_boss: SafeFinalBoss
randomize_enemy_attributes: EnemizerAttributes
randomize_enemy_stats: EnemizerStats
randomize_enemy_attacks: EnemizerAttacks
experience_modifier: ExperienceModifier
money_drop_multiplier: MoneyDropMultiplier
starting_money: StartingMoney
easy_deaths: EasyDeaths
easy_combat: EasyCombat
progressive_weapons: ProgressiveWeapons
progressive_armor: ProgressiveArmor
armorizer: Armorizer
weaponizer: Weaponizer
armorizer_resistance_chance: ElementChance
equipamizer_cap_stats: EquipamizerStatCap
auto_scale_party_members: AutoscaleParty
remote_items: RemoteItems
random_flavors: RandomFlavors
random_battle_backgrounds: RandomBattleBG
random_swirl_colors: RandomSwirlColors
presents_match_contents: PresentSprites
nonlocal_items_use_local_presents: NoAPPresents
prefixed_items: PreFixItems
total_photos: PhotoCount
randomize_franklinbadge_protection: RandomFranklinBadge
shuffle_enemy_drops: ShuffleDrops
common_filler_weight: CommonWeight
uncommon_filler_weight: UncommonWeight
rare_filler_weight: RareWeight
money_weight: MoneyWeight
plando_lumine_hall_text: PlandoLumineHallText
no_free_sanctuaries: NoFreeSancs
randomize_overworld_music: RandomizeOverworldMusic
randomize_battle_music: RandomizeBattleMusic
randomize_fanfares: RandomizeFanfares
randomize_psi_palettes: RandomizePSIPalettes
map_palette_shuffle: RandomMapColors
shop_randomizer: ShopRandomizer
scout_shop_checks: ScoutShopChecks
dungeon_shuffle: DungeonShuffle
skip_epilogue: SkipEpilogue
start_inventory_from_pool: StartInventoryPool
death_link: DeathLink
death_link_mode: DeathLinkMode
energy_link: EnergyLink
eb_option_groups = [
OptionGroup("Goal Settings", [
GiygasRequired,
SanctuariesRequired,
SanctuaryAltGoal
]),
OptionGroup("Item Settings", [
LocalTeleports,
CharacterShuffle,
ProgressiveWeapons,
ProgressiveArmor,
RandomFranklinBadge,
CommonWeight,
UncommonWeight,
RareWeight,
MoneyWeight,
PreFixItems,
PhotoCount
]),
OptionGroup("Equipamizer", [
Armorizer,
Weaponizer,
ElementChance,
EquipamizerStatCap
]),
OptionGroup("World Modes", [
RandomStartLocation,
MagicantMode,
MonkeyCavesMode,
NoFreeSancs,
StartingCharacter
]),
OptionGroup("PSI Randomization", [
PSIShuffle,
BanFlashFavorite
]),
OptionGroup("Enemy Randomization", [
EnemyShuffle,
BossShuffle,
SafeFinalBoss,
DecoupleDiamondDog,
ShuffleGiygas,
ExperienceModifier,
ShuffleDrops,
MoneyDropMultiplier
]),
OptionGroup("Enemizer", [
EnemizerAttributes,
EnemizerAttacks,
EnemizerStats
]),
OptionGroup("Shop Randomization", [
ShopRandomizer,
ScoutShopChecks
]),
OptionGroup("Entrance Randomization", [
DungeonShuffle
]),
OptionGroup("Convenience Settings", [
ShortenPrayers,
EasyDeaths,
StartingMoney,
RemoteItems,
AutoscaleParty,
SkipEpilogue,
EasyCombat
]),
OptionGroup("Aesthetic Settings", [
RandomFlavors,
RandomSwirlColors,
RandomBattleBG,
RandomMapColors,
PresentSprites,
NoAPPresents,
RandomizePSIPalettes,
PlandoLumineHallText
]),
OptionGroup("Music Randomizer", [
RandomizeOverworldMusic,
RandomizeBattleMusic,
RandomizeFanfares
]),
OptionGroup("Multiplayer Features", [
DeathLink,
DeathLinkMode,
EnergyLink
])
]

View File

@@ -0,0 +1,279 @@
from typing import List, Dict, TYPE_CHECKING, Optional
from BaseClasses import Region, Location
from .Locations import LocationData
from .Options import MagicantMode
if TYPE_CHECKING:
from . import EarthBoundWorld
class EBLocation(Location):
game: str = "EarthBound"
def init_areas(world: "EarthBoundWorld", locations: list[LocationData]) -> None:
multiworld = world.multiworld
player = world.player
locations_per_region = get_locations_per_region(locations)
regions = [
create_region(world, player, locations_per_region, "Menu"),
create_region(world, player, locations_per_region, "Ness's Mind"),
create_region(world, player, locations_per_region, "Global ATM Access"),
create_region(world, player, locations_per_region, "Northern Onett"),
create_region(world, player, locations_per_region, "Onett"),
create_region(world, player, locations_per_region, "Arcade"),
create_region(world, player, locations_per_region, "Giant Step"),
create_region(world, player, locations_per_region, "Twoson"),
create_region(world, player, locations_per_region, "Common Condiment Shop"),
create_region(world, player, locations_per_region, "Everdred's House"),
create_region(world, player, locations_per_region, "Peaceful Rest Valley"),
create_region(world, player, locations_per_region, "Happy-Happy Village"),
create_region(world, player, locations_per_region, "Happy-Happy HQ"),
create_region(world, player, locations_per_region, "Lilliput Steps"),
create_region(world, player, locations_per_region, "Threed"),
create_region(world, player, locations_per_region, "Threed Underground"),
create_region(world, player, locations_per_region, "Boogey Tent"),
create_region(world, player, locations_per_region, "Grapefruit Falls"),
create_region(world, player, locations_per_region, "Belch's Factory"),
create_region(world, player, locations_per_region, "Saturn Valley"),
create_region(world, player, locations_per_region, "Upper Saturn Valley"),
create_region(world, player, locations_per_region, "Milky Well"),
create_region(world, player, locations_per_region, "Dusty Dunes Desert"),
create_region(world, player, locations_per_region, "Gold Mine"),
create_region(world, player, locations_per_region, "Monkey Caves"),
create_region(world, player, locations_per_region, "Fourside"),
create_region(world, player, locations_per_region, "Moonside"),
create_region(world, player, locations_per_region, "Fourside Dept. Store"),
create_region(world, player, locations_per_region, "Magnet Hill"),
create_region(world, player, locations_per_region, "Monotoli Building"),
create_region(world, player, locations_per_region, "Winters"),
create_region(world, player, locations_per_region, "Snow Wood Boarding School"),
create_region(world, player, locations_per_region, "Southern Winters"),
create_region(world, player, locations_per_region, "Brickroad Maze"),
create_region(world, player, locations_per_region, "Rainy Circle"),
create_region(world, player, locations_per_region, "Andonuts Lab Area"),
create_region(world, player, locations_per_region, "Stonehenge Base"),
create_region(world, player, locations_per_region, "Summers"),
create_region(world, player, locations_per_region, "Summers Museum"),
create_region(world, player, locations_per_region, "Dalaam"),
create_region(world, player, locations_per_region, "Pink Cloud"),
create_region(world, player, locations_per_region, "Scaraba"),
create_region(world, player, locations_per_region, "Pyramid"),
create_region(world, player, locations_per_region, "Southern Scaraba"),
create_region(world, player, locations_per_region, "Dungeon Man"),
create_region(world, player, locations_per_region, "Deep Darkness"),
create_region(world, player, locations_per_region, "Deep Darkness Darkness"),
create_region(world, player, locations_per_region, "Tenda Village"),
create_region(world, player, locations_per_region, "Lumine Hall"),
create_region(world, player, locations_per_region, "Lost Underworld"),
create_region(world, player, locations_per_region, "Fire Spring"),
create_region(world, player, locations_per_region, "Magicant"),
create_region(world, player, locations_per_region, "Sea of Eden"),
create_region(world, player, locations_per_region, "Cave of the Present")
]
if world.options.giygas_required:
regions.extend([
create_region(world, player, locations_per_region, "Cave of the Past"),
create_region(world, player, locations_per_region, "Endgame")
])
multiworld.regions += regions
def connect_area_exits(world: "EarthBoundWorld"):
multiworld = world.multiworld
player = world.player
connect_menu_region(world)
arcade_connection = world.dungeon_connections["Arcade"]
giant_step_connection = world.dungeon_connections["Giant Step"]
lilliput_steps_connection = world.dungeon_connections["Lilliput Steps"]
happy_happy_hq_connection = world.dungeon_connections["Happy-Happy HQ"]
belch_factory_connection = world.dungeon_connections["Belch's Factory"]
milky_well_connection = world.dungeon_connections["Milky Well"]
gold_mine_connection = world.dungeon_connections["Gold Mine"]
moonside_connection = world.dungeon_connections["Moonside"]
monotoli_building_connection = world.dungeon_connections["Monotoli Building"]
magnet_hill_connection = world.dungeon_connections["Magnet Hill"]
pink_cloud_connection = world.dungeon_connections["Pink Cloud"]
pyramid_connection = world.dungeon_connections["Pyramid"]
dungeon_man_connection = world.dungeon_connections["Dungeon Man"]
rainy_circle_connection = world.dungeon_connections["Rainy Circle"]
stonehenge_connection = world.dungeon_connections["Stonehenge Base"]
lumine_hall_connection = world.dungeon_connections["Lumine Hall"]
fire_spring_connection = world.dungeon_connections["Fire Spring"]
sea_of_eden_connection = world.dungeon_connections["Sea of Eden"]
brickroad_maze_connection = world.dungeon_connections["Brickroad Maze"]
multiworld.get_region("Ness's Mind", player).add_exits(["Onett", "Twoson", "Happy-Happy Village", "Threed", "Saturn Valley", "Dusty Dunes Desert", "Fourside", "Winters", "Summers", "Dalaam", "Scaraba", "Deep Darkness", "Tenda Village", "Lost Underworld", "Magicant"],
{"Onett": lambda state: state.has("Onett Teleport", player),
"Twoson": lambda state: state.has("Twoson Teleport", player),
"Happy-Happy Village": lambda state: state.has("Happy-Happy Village Teleport", player),
"Threed": lambda state: state.has("Threed Teleport", player),
"Saturn Valley": lambda state: state.has("Saturn Valley Teleport", player),
"Dusty Dunes Desert": lambda state: state.has("Dusty Dunes Teleport", player),
"Fourside": lambda state: state.has("Fourside Teleport", player),
"Winters": lambda state: state.has("Winters Teleport", player),
"Summers": lambda state: state.has("Summers Teleport", player),
"Dalaam": lambda state: state.has("Dalaam Teleport", player),
"Scaraba": lambda state: state.has("Scaraba Teleport", player),
"Deep Darkness": lambda state: state.has("Deep Darkness Teleport", player),
"Tenda Village": lambda state: state.has("Tenda Village Teleport", player),
"Lost Underworld": lambda state: state.has("Lost Underworld Teleport", player),
"Magicant": lambda state: state.has_any({"Magicant Teleport", "Magicant Unlock"}, player)})
multiworld.get_region("Northern Onett", player).add_exits(["Onett"])
multiworld.get_region("Onett", player).add_exits([giant_step_connection, "Twoson", "Northern Onett", "Global ATM Access", arcade_connection],
{giant_step_connection: lambda state: state.has("Key to the Shack", player),
"Twoson": lambda state: state.has("Police Badge", player),
"Northern Onett": lambda state: state.has("Police Badge", player)})
multiworld.get_region("Twoson", player).add_exits(["Onett", "Peaceful Rest Valley", "Threed", "Everdred's House", "Global ATM Access", "Common Condiment Shop"],
{"Onett": lambda state: state.has("Police Badge", player),
"Peaceful Rest Valley": lambda state: state.has_any({"Pencil Eraser", "Valley Bridge Repair"}, player),
"Threed": lambda state: state.has_any({"Threed Tunnels Clear", "Wad of Bills"}, player),
"Everdred's House": lambda state: state.has("Paula", player)})
multiworld.get_region("Peaceful Rest Valley", player).add_exits(["Twoson", "Happy-Happy Village"],
{"Twoson": lambda state: state.has_any({"Pencil Eraser", "Valley Bridge Repair"}, player)})
multiworld.get_region("Happy-Happy Village", player).add_exits(["Peaceful Rest Valley", lilliput_steps_connection, "Global ATM Access", happy_happy_hq_connection])
multiworld.get_region("Threed", player).add_exits(["Twoson", "Dusty Dunes Desert", "Andonuts Lab Area", "Threed Underground", "Boogey Tent", "Global ATM Access"],
{"Twoson": lambda state: state.has("Threed Tunnels Clear", player),
"Dusty Dunes Desert": lambda state: state.has("Threed Tunnels Clear", player),
"Andonuts Lab Area": lambda state: state.has_all({"UFO Engine", "Bad Key Machine"}, player),
"Threed Underground": lambda state: state.has("Zombie Paper", player),
"Boogey Tent": lambda state: state.has("Jeff", player)})
multiworld.get_region("Threed Underground", player).add_exits(["Grapefruit Falls"])
multiworld.get_region("Grapefruit Falls", player).add_exits([belch_factory_connection, "Saturn Valley", "Threed Underground"],
{belch_factory_connection: lambda state: state.has("Jar of Fly Honey", player)})
multiworld.get_region(belch_factory_connection, player).add_exits(["Upper Saturn Valley"],
{"Upper Saturn Valley": lambda state: state.has("Threed Tunnels Clear", player)})
multiworld.get_region("Saturn Valley", player).add_exits(["Grapefruit Falls", "Cave of the Present", "Global ATM Access"],
{"Cave of the Present": lambda state: state.has("Meteorite Piece", player)})
multiworld.get_region("Upper Saturn Valley", player).add_exits([milky_well_connection, "Saturn Valley"])
multiworld.get_region("Dusty Dunes Desert", player).add_exits(["Threed", "Monkey Caves", gold_mine_connection, "Fourside", "Global ATM Access"],
{"Threed": lambda state: state.has("Threed Tunnels Clear", player),
"Monkey Caves": lambda state: state.has("King Banana", player),
gold_mine_connection: lambda state: state.has("Mining Permit", player)})
multiworld.get_region("Fourside", player).add_exits(["Dusty Dunes Desert", monotoli_building_connection, magnet_hill_connection, "Threed", "Fourside Dept. Store", "Global ATM Access", moonside_connection],
{monotoli_building_connection: lambda state: state.has("Yogurt Dispenser", player),
magnet_hill_connection: lambda state: state.has("Signed Banana", player),
"Threed": lambda state: state.has("Diamond", player),
"Fourside Dept. Store": lambda state: state.has("Jeff", player)})
multiworld.get_region("Moonside", player).add_exits(["Global ATM Access"])
multiworld.get_region("Summers", player).add_exits(["Scaraba", "Summers Museum", "Global ATM Access"],
{"Summers Museum": lambda state: state.has("Tiny Ruby", player)})
multiworld.get_region("Winters", player).add_exits(["Snow Wood Boarding School", "Southern Winters", "Global ATM Access"],
{"Snow Wood Boarding School": lambda state: state.has("Letter For Tony", player),
"Southern Winters": lambda state: state.has("Pak of Bubble Gum", player)})
multiworld.get_region("Southern Winters", player).add_exits([brickroad_maze_connection])
multiworld.get_region(brickroad_maze_connection, player).add_exits(["Southern Winters", rainy_circle_connection])
multiworld.get_region(rainy_circle_connection, player).add_exits([brickroad_maze_connection, "Andonuts Lab Area"])
multiworld.get_region("Andonuts Lab Area", player).add_exits([stonehenge_connection, "Winters", rainy_circle_connection],
{stonehenge_connection: lambda state: state.has("Eraser Eraser", player)})
multiworld.get_region("Dalaam", player).add_exits([pink_cloud_connection],
{pink_cloud_connection: lambda state: state.has("Carrot Key", player)})
multiworld.get_region("Scaraba", player).add_exits([pyramid_connection, "Global ATM Access", "Common Condiment Shop"],
{pyramid_connection: lambda state: state.has("Hieroglyph Copy", player)})
multiworld.get_region(pyramid_connection, player).add_exits(["Southern Scaraba"])
multiworld.get_region("Southern Scaraba", player).add_exits([dungeon_man_connection],
{dungeon_man_connection: lambda state: state.has_any({"Key to the Tower"}, player)})
multiworld.get_region("Dungeon Man", player).add_exits(["Deep Darkness"],
{"Deep Darkness": lambda state: state.has("Submarine to Deep Darkness", player)})
multiworld.get_region("Deep Darkness", player).add_exits(["Deep Darkness Darkness"],
{"Deep Darkness Darkness": lambda state: state.has("Hawk Eye", player)})
multiworld.get_region("Deep Darkness Darkness", player).add_exits(["Tenda Village", "Deep Darkness"])
multiworld.get_region("Tenda Village", player).add_exits([lumine_hall_connection, "Deep Darkness Darkness"],
{lumine_hall_connection: lambda state: state.has("Shyness Book", player),
"Deep Darkness Darkness": lambda state: state.has_all({"Shyness Book", "Hawk Eye"}, player)})
multiworld.get_region("Lumine Hall", player).add_exits(["Lost Underworld"])
multiworld.get_region("Lost Underworld", player).add_exits([fire_spring_connection])
if world.options.giygas_required:
multiworld.get_region("Cave of the Present", player).add_exits(["Cave of the Past"],
{"Cave of the Past": lambda state: state.has("Power of the Earth", player)})
multiworld.get_region("Cave of the Past", player).add_exits(["Endgame"],
{"Endgame": lambda state: state.has("Paula", player)})
if world.options.magicant_mode < MagicantMode.option_optional_boost: # 3
multiworld.get_region("Magicant", player).add_exits(["Global ATM Access", sea_of_eden_connection],
{sea_of_eden_connection: lambda state: state.has("Ness", player)})
def create_location(player: int, location_data: LocationData, region: Region) -> Location:
location = EBLocation(player, location_data.name, location_data.code, region)
return location
def create_region(world: "EarthBoundWorld", player: int, locations_per_region: Dict[str, List[LocationData]], name: str) -> Region:
region = Region(name, player, world.multiworld)
if name in locations_per_region:
for location_data in locations_per_region[name]:
location = create_location(player, location_data, region)
region.locations.append(location)
return region
def get_locations_per_region(locations: List[LocationData]) -> Dict[str, List[LocationData]]:
per_region: Dict[str, List[LocationData]] = {}
for location in locations:
per_region.setdefault(location.region, []).append(location)
return per_region
def connect_menu_region(world: "EarthBoundWorld") -> None:
starting_region_list = {
0: "Northern Onett",
1: "Onett",
2: "Twoson",
3: "Happy-Happy Village",
4: "Threed",
5: "Saturn Valley",
6: "Fourside",
7: "Winters",
8: "Summers",
9: "Dalaam",
10: "Scaraba",
11: "Deep Darkness",
12: "Tenda Village",
13: "Lost Underworld",
14: "Magicant"
}
world.starting_region = starting_region_list[world.start_location]
world.multiworld.get_region("Menu", world.player).add_exits([world.starting_region, "Ness's Mind"],
{"Ness's Mind": lambda state: state.has_any({"Ness", "Paula", "Jeff", "Poo"}, world.player),
world.starting_region: lambda state: state.has_any({"Ness", "Paula", "Jeff", "Poo"}, world.player)})

848
worlds/earthbound/Rom.py Normal file
View File

@@ -0,0 +1,848 @@
import hashlib
import os
import Utils
import typing
import struct
import settings
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension
from .game_data import local_data
from .game_data.battle_bg_data import battle_bg_bpp
from .modules.psi_shuffle import write_psi
from .game_data.text_data import barf_text, text_encoder
from .modules.flavor_data import flavor_data, vanilla_flavor_pointers
from .modules.hint_data import parse_hint_data
from .modules.enemy_data import scale_enemies
from .modules.area_scaling import calculate_scaling
from .modules.boss_shuffle import write_bosses
from .modules.equipamizer import randomize_armor, randomize_weapons
from .modules.music_rando import music_randomizer
from .modules.palette_shuffle import randomize_psi_palettes, map_palette_shuffle
from .modules.shopsanity import write_shop_checks
from .modules.enemy_shuffler import apply_enemy_shuffle
from .modules.dungeon_er import write_dungeon_entrances
# from .modules.foodamizer import randomize_food
from .modules.enemizer.randomize_enemy_attributes import randomize_enemy_attributes
from .modules.enemizer.randomize_enemy_stats import randomize_enemy_stats
from .modules.enemizer.randomize_enemy_attacks import randomize_enemy_attacks
from .game_data.static_location_data import location_groups
from BaseClasses import ItemClassification
from typing import TYPE_CHECKING, Sequence
from logging import warning
# from .local_data import local_locations
if TYPE_CHECKING:
from . import EarthBoundWorld
item_id_table = local_data.item_id_table
location_dialogue = local_data.location_dialogue
present_locations = local_data.present_locations
psi_locations = local_data.psi_locations
npc_locations = local_data.npc_locations
character_locations = local_data.character_locations
special_name_table = local_data.special_name_table
item_space_checks = local_data.item_space_checks
local_present_types = local_data.local_present_types
present_text_pointers = local_data.present_text_pointers
psi_item_table = local_data.psi_item_table
character_item_table = local_data.character_item_table
party_id_nums = local_data.party_id_nums
starting_psi_table = local_data.starting_psi_table
badge_names = local_data.badge_names
world_version = local_data.world_version
protection_checks = local_data.protection_checks
protection_text = local_data.protection_text
nonlocal_present_types = local_data.nonlocal_present_types
ap_text_pntrs = local_data.ap_text_pntrs
money_item_table = local_data.money_item_table
valid_hashes = ["a864b2e5c141d2dec1c4cbed75a42a85", # Cartridge
"6d71ccc8e2afda15d011348291afdf4f"] # VC
class LocalRom(object):
def __init__(self, file: bytes, name: str | None = None) -> None:
self.file = bytearray(file)
self.name = name
def read_byte(self, offset: int) -> int:
return self.file[offset]
def read_bytes(self, offset: int, length: int) -> bytes:
return self.file[offset:offset + length]
def write_bytes(self, offset: int, values: Sequence[int]) -> None:
self.file[offset:offset + len(values)] = values
def get_bytes(self) -> bytes:
return bytes(self.file)
def patch_rom(world: "EarthBoundWorld", rom: LocalRom, player: int) -> None:
rom.copy_bytes(0x1578DD, 0x3E, 0x34A060) # Threed/Saturn teleport move
rom.copy_bytes(0x15791B, 0xF8, 0x157959)
rom.copy_bytes(0x34A000, 0x1F, 0x1578DD)
rom.copy_bytes(0x34A020, 0x1F, 0x15793A)
rom.copy_bytes(0x34A040, 0x1F, 0x157A51)
rom.copy_bytes(0x34A060, 0x3E, 0x1578FC)
rom.copy_bytes(0x15ED4B, 0x06, 0x15F1FB)
rom.copy_bytes(0x1A7FA7, 0xBF, 0x389900)
starting_area_coordinates = {
0: [0x50, 0x04, 0xB5, 0x1F], # North Onett
1: [0x52, 0x06, 0x4C, 0x1F], # Onett
2: [0xEF, 0x22, 0x41, 0x1F], # Twoson
3: [0x53, 0x06, 0x85, 0x1D], # Happy Happy
4: [0x55, 0x24, 0x69, 0x1D], # Threed
5: [0x60, 0x1D, 0x30, 0x01], # Saturn Valley
6: [0xAB, 0x10, 0xF3, 0x09], # Fourside
7: [0xE3, 0x09, 0xA3, 0x1D], # Winters
8: [0xCB, 0x24, 0x7B, 0x1E], # Summers
9: [0xD0, 0x1E, 0x31, 0x1D], # Dalaam
10: [0xC7, 0x1F, 0x37, 0x19], # Scaraba
11: [0xDD, 0x1B, 0xB7, 0x17], # Deep Darkness
12: [0xD0, 0x25, 0x47, 0x18], # Tenda Village
13: [0x9C, 0x00, 0x84, 0x17], # Lost Underworld
14: [0x4B, 0x11, 0xAD, 0x18] # Magicant
}
starting_levels = {
"Ness": 0x15F5FB,
"Paula": 0x15F60F,
"Jeff": 0x15F623,
"Poo": 0x15F637
}
atm_card_slots = {
"Ness": 0x15F5FF,
"Paula": 0x15F613,
"Jeff": 0x15F629,
"Poo": 0x15F63B
}
starting_weapon = {
"Ness": [0x15F600, 0x12],
"Paula": [0x15F615, 0x1C],
"Jeff": [0x15F62A, 0x24]
}
teleport_learnlevel = {
"Ness": [0x158D53, 0x158D62],
"Paula": [0x158D54, 0x158D63],
"Poo": [0x158D55, 0x158D64]
}
world.start_items = []
world.handled_locations = []
for item in world.multiworld.precollected_items[world.player]:
world.start_items.append(item.name)
if world.options.random_start_location:
rom.write_bytes(0x0F96C2, bytearray([0x69, 0x00]))
rom.write_bytes(0x0F9618, bytearray([0x69, 0x00]))
rom.write_bytes(0x0F9629, bytearray([0x69, 0x00])) # Block Northern Onett
else:
rom.write_bytes(0x00B66A, bytearray([0x06])) # Fix starting direction
rom.write_bytes(0x01FE9B, bytearray(starting_area_coordinates[world.start_location][0:2]))
rom.write_bytes(0x01FE9E, bytearray(starting_area_coordinates[world.start_location][2:4])) # Start position
rom.write_bytes(0x01FE91, bytearray(starting_area_coordinates[world.start_location][0:2]))
rom.write_bytes(0x01FE8B, bytearray(starting_area_coordinates[world.start_location][2:4])) # Respawn position
if world.options.skip_epilogue:
rom.write_bytes(0x09C4D4, struct.pack("I", 0xEEA437))
if world.starting_character == "Poo":
rom.write_bytes(starting_levels[world.starting_character], bytearray([0x06]))
else:
rom.write_bytes(starting_levels[world.starting_character], bytearray([0x03]))
rom.write_bytes(atm_card_slots[world.starting_character], bytearray([0xB1]))
if world.starting_character != "Ness":
rom.write_bytes(atm_card_slots["Ness"], bytearray([0x58]))
if world.starting_character != "Poo":
rom.write_bytes(starting_weapon[world.starting_character][0], bytearray([starting_weapon[world.starting_character][1]]))
if world.starting_character != "Jeff":
for i in range(2):
rom.write_bytes(teleport_learnlevel[world.starting_character][i - 1], bytearray([0x01]))
else:
rom.write_bytes(0x15F62B, bytearray([0xB5]))
if world.options.alternate_sanctuary_goal:
rom.write_bytes(0x04FD72, bytearray([world.options.sanctuaries_required.value + 2]))
else:
rom.write_bytes(0x04FD72, bytearray([0xFF]))
if not world.options.giygas_required:
rom.write_bytes(0x2E9C29, bytearray([0x10, 0xA5]))
if world.options.magicant_mode == 2:
rom.write_bytes(0x04FD71, bytearray([world.options.sanctuaries_required.value + 1]))
rom.write_bytes(0x2EA26A, bytearray([0x0A, 0x10, 0xA5, 0xEE])) # Alt goal magicant sets the credits
elif world.options.magicant_mode == 1:
rom.write_bytes(0x2E9C29, bytearray([0x00, 0xA5]))
if world.options.giygas_required:
rom.write_bytes(0x2EA26A, bytearray([0x08, 0xD9, 0x9B, 0xEE])) # Give stat boost if magicant + giygas required
else:
rom.write_bytes(0x2EA26A, bytearray([0x0A, 0x10, 0xA5, 0xEE])) # If no giygas, set credits
elif world.options.magicant_mode == 3:
rom.write_bytes(0x2EA26A, bytearray([0x08, 0x0F, 0x9C, 0xEE])) # Give only stat boost if set to boost
rom.write_bytes(0x04FD74, bytearray([world.options.death_link.value]))
rom.write_bytes(0x04FD75, bytearray([world.options.death_link_mode.value]))
rom.write_bytes(0x04FD76, bytearray([world.options.remote_items.value]))
rom.write_bytes(0x04FD78, bytearray([world.options.energy_link.value]))
if world.options.death_link_mode != 1:
rom.write_bytes(0x2FFDFE, bytearray([0x80])) # Mercy healing
rom.write_bytes(0x2FFE30, bytearray([0x80])) # Mercy text
rom.write_bytes(0x2FFE59, bytearray([0x80])) # Mercy revive
# IF YOU ADD ASM, CHANGE THESE OR THE GAME WILL CRASH
if world.options.monkey_caves_mode == 2:
rom.write_bytes(0x062B87, bytearray([0x0A, 0x28, 0xCA, 0xEE]))
elif world.options.monkey_caves_mode == 3:
rom.write_bytes(0x0F1388, bytearray([0x03, 0xCA, 0xEE]))
if world.options.no_free_sanctuaries:
rom.write_bytes(0x0F09F2, bytearray([0x15, 0x84])) # Lock Lilliput steps with flag $0415
rom.write_bytes(0x0F09EE, struct.pack("I", 0xEEF790)) # Lilliput door script
rom.write_bytes(0x0F23D2, bytearray([0x16, 0x84])) # Lock Fire Spring with flag $0146
rom.write_bytes(0x0F23CE, struct.pack("I", 0xEEF946)) # Fire Spring door script
rom.write_bytes(0x04FD70, bytearray([world.options.sanctuaries_required.value]))
shop_checks = []
for location in world.multiworld.get_locations(player):
if location.address:
receiver_name = world.multiworld.get_player_name(location.item.player)
name = location.name
if world.options.remote_items:
item = "Remote Item"
else:
item = location.item.name
item_name_loc = (((location.address - 0xEB0000) * 128) + 0x3F0000)
# todo; replace with the encoder function
item_text = text_encoder(location.item.name, 128)
item_text.extend([0x00])
player_name_loc = (((location.address - 0xEB0000) * 48) + 0x3F8000)
player_text = text_encoder(receiver_name, 48)
# Locations over this address are Shopsanity locations and handled in the shopsanity module
if location.address < 0xEB1000:
rom.write_bytes(item_name_loc, bytearray(item_text))
rom.write_bytes(player_name_loc, bytearray(player_text))
if item not in item_id_table or location.item.player != location.player:
item_id = 0xAD
else:
item_id = item_id_table[item]
if name in location_dialogue:
for i in range(len(location_dialogue[name])):
if location.item.player != location.player or item == "Remote Item":
rom.write_bytes(location_dialogue[name][i] - 1, bytearray([0x17, location.address - 0xEB0000]))
elif item in item_id_table:
rom.write_bytes(location_dialogue[name][i], bytearray([item_id]))
elif item in psi_item_table or item in character_item_table:
rom.write_bytes(location_dialogue[name][i] - 1, bytearray([0x16, special_name_table[item][0]]))
elif item in money_item_table:
rom.write_bytes(location_dialogue[name][i] - 1, bytearray([0x16, (0x16 + list(money_item_table).index(item))]))
if name in present_locations:
world.handled_locations.append(name)
if item == "Nothing": # I can change this to "In nothing_table" later todo: make it so nonlocal items do not follow this table
rom.write_bytes(present_locations[name], bytearray([0x00, 0x00, 0x01]))
elif location.item.player != location.player or item == "Remote Item":
rom.write_bytes(present_locations[name], bytearray([item_id, 0x00, 0x00, (location.address - 0xEB0000)]))
elif item in item_id_table:
rom.write_bytes(present_locations[name], bytearray([item_id, 0x00]))
elif item in psi_item_table:
rom.write_bytes(present_locations[name], bytearray([psi_item_table[item], 0x00, 0x02]))
elif item in character_item_table:
rom.write_bytes(present_locations[name], bytearray([character_item_table[item][0], 0x00, 0x03]))
elif item in money_item_table:
rom.write_bytes(present_locations[name], struct.pack("H", money_item_table[item]))
rom.write_bytes(present_locations[name] + 2, bytearray([0x01]))
if name in npc_locations:
world.handled_locations.append(name)
for i in range(len(npc_locations[name])):
if item in item_id_table or location.item.player != location.player or item == "Remote Item":
rom.write_bytes(npc_locations[name][i], bytearray([item_id]))
elif item in psi_item_table or item in character_item_table:
rom.write_bytes(npc_locations[name][i] - 3, bytearray([0x0E, 0x00, 0x0E, (special_name_table[item][0] + 1)]))
rom.write_bytes(npc_locations[name][i] + 2, bytearray([0xA5, 0xAA, 0xEE]))
elif item in money_item_table:
rom.write_bytes(npc_locations[name][i] - 3, bytearray([0x1D, 0x25]))
rom.write_bytes(npc_locations[name][i] - 1, struct.pack("H", money_item_table[item]))
rom.write_bytes(npc_locations[name][i] + 2, bytearray([0x00, 0xF0, 0xF3]))
if name in psi_locations:
world.handled_locations.append(name)
if item in special_name_table and location.item.player == location.player and item != "Remote Item":
rom.write_bytes(psi_locations[name][0], special_name_table[item][1].to_bytes(3, byteorder="little"))
rom.write_bytes(psi_locations[name][0] + 4, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]))
elif item in money_item_table and location.item.player == location.player:
rom.write_bytes(psi_locations[name][0] - 1, bytearray([0x1D, 0x25]))
rom.write_bytes(psi_locations[name][0] + 1, struct.pack("H", money_item_table[item]))
rom.write_bytes(psi_locations[name][0] + 3, bytearray([0x08, 0x26, 0xF0, 0xF3, 0x00, 0x03, 0x00]))
else:
rom.write_bytes(psi_locations[name][0], bytearray(psi_locations[name][1:4]))
rom.write_bytes(psi_locations[name][4], bytearray([item_id]))
if name in character_locations:
world.handled_locations.append(name)
if item in character_item_table and location.item.player == location.player and item != "Remote Item":
rom.write_bytes(character_locations[name][0], special_name_table[item][1].to_bytes(3, byteorder="little"))
if name == "Snow Wood - Bedroom": # Use lying down sprites for the bedroom check
rom.write_bytes(character_locations[name][1], struct.pack("H", character_item_table[item][2]))
rom.write_bytes(0x0FB0D8, bytearray([0x06]))
else:
rom.write_bytes(character_locations[name][1], struct.pack("H", character_item_table[item][1]))
elif item in psi_item_table and location.item.player == location.player:
rom.write_bytes(character_locations[name][0], (special_name_table[item][1] + 1).to_bytes(3, byteorder="little"))
rom.write_bytes(character_locations[name][1], bytearray([0x62]))
rom.write_bytes(character_locations[name][2], bytearray([0x70, 0xF9, 0xD5]))
elif item in money_item_table and location.item.player == location.player:
rom.write_bytes(character_locations[name][2] - 1, bytearray([0x1D, 0x25]))
rom.write_bytes(character_locations[name][2] + 1, struct.pack("H", money_item_table[item]))
rom.write_bytes(character_locations[name][0] - 2, bytearray([0x01]))
rom.write_bytes(character_locations[name][0], bytearray([0x4B, 0xF0, 0xF3]))
rom.write_bytes(character_locations[name][1], bytearray([0x97]))
else:
rom.write_bytes(character_locations[name][0], bytearray(character_locations[name][4:7]))
if location.item.name in ["Ness", "Paula", "Jeff", "Poo"]:
rom.write_bytes(character_locations[name][1], bytearray([character_item_table[location.item.name][1]]))
else:
rom.write_bytes(character_locations[name][1], bytearray([0x97]))
rom.write_bytes(character_locations[name][2], bytearray([0x18, 0xF9, 0xD5]))
rom.write_bytes(character_locations[name][3], bytearray([item_id]))
if name == "Deep Darkness - Barf Character":
if item in character_item_table:
rom.write_bytes(0x2EA0E2, bytearray(barf_text[item][0:3]))
rom.write_bytes(0x2EA0E8, bytearray(barf_text[item][3:6]))
elif item in psi_item_table and location.item.player == location.player:
rom.write_bytes(0x2EA0E2, bytearray([0x98, 0xC3, 0xEE]))
rom.write_bytes(0x2EA0E8, bytearray([0xF7, 0xC4, 0xEE]))
else:
rom.write_bytes(0x2EA0E2, bytearray([0x6A, 0xC3, 0xEE]))
rom.write_bytes(0x2EA0E8, bytearray([0xB4, 0xC4, 0xEE]))
if name == "Poo - Starting Item":
world.handled_locations.append(name)
if item in item_id_table and location.item.player == location.player and item != "Remote Item":
rom.write_bytes(0x15F63C, bytearray([item_id]))
else:
rom.write_bytes(0x15F63C, bytearray([0x00])) # Don't give anything if the item doesn't have a tangible ID
if item in special_name_table and location.item.player == location.player: # Apply a special script if teleport or character
rom.write_bytes(0x15F765, special_name_table[item][1].to_bytes(3, byteorder="little")) # This might be offset, check if it is
rom.write_bytes(0x2EC618, bytearray([(special_name_table[item][0] + 1)]))
rom.write_bytes(0x2EC61A, bytearray([0xA5, 0xAA, 0xEE]))
rom.write_bytes(0x2EC613, bytearray([0x03, 0x01]))
if item in money_item_table and location.item.player == location.player:
rom.write_bytes(0x15F764, bytearray([0x1D, 0x08]))
rom.write_bytes(0x15F766, struct.pack("H", money_item_table[item]))
rom.write_bytes(0x15F768, bytearray([0x01]))
if location.address >= 0xEB1000:
world.handled_locations.append(name)
shop_checks.append(location)
if name not in world.handled_locations:
warning(f"{name} not placed in {world.multiworld.get_player_name(world.player)}'s EarthBound world. Something went wrong here.")
if name in item_space_checks:
if item not in item_id_table or location.item.player != location.player:
if len(item_space_checks[name]) == 4:
rom.write_bytes(item_space_checks[name][0], bytearray(item_space_checks[name][1:4]))
else:
rom.write_bytes(item_space_checks[name][0], bytearray(item_space_checks[name][1:4]))
rom.write_bytes(item_space_checks[name][4], bytearray(item_space_checks[name][5:8]))
if name in present_locations and "Lost Underworld" not in name and world.options.presents_match_contents:
if ItemClassification.trap in location.item.classification:
world.present_type = "trap"
elif ItemClassification.progression in location.item.classification:
world.present_type = "progression"
elif ItemClassification.useful in location.item.classification:
world.present_type = "useful"
else:
world.present_type = "filler"
if location.item.player == world.player or world.options.nonlocal_items_use_local_presents:
rom.write_bytes(present_locations[name] - 12, bytearray(local_present_types[world.present_type]))
if name != "Threed - Boogey Tent Trashcan":
rom.write_bytes(present_locations[name] - 4, bytearray(present_text_pointers[world.present_type]))
else:
rom.write_bytes(present_locations[name] - 12, bytearray(nonlocal_present_types[world.present_type]))
if name != "Threed - Boogey Tent Trashcan":
if world.present_type == "progression":
rom.write_bytes(present_locations[name] - 4, struct.pack("I", world.random.choice(ap_text_pntrs)))
elif world.present_type == "trap":
rom.write_bytes(present_locations[name] - 4, bytearray([0x8D, 0xce, 0xee]))
else:
rom.write_bytes(present_locations[name] - 4, bytearray([0xc1, 0xcd, 0xee]))
location = world.multiworld.get_location("Twoson - Bike Shop Rental", world.player)
if location.item.name in item_id_table:
item_id = item_id_table[location.item.name]
else:
item_id = 0xAD
rom.write_bytes(0x0800C4, bytearray([item_id])) # Bike shop
rom.write_bytes(0x0802EA, bytearray([item_id]))
rom.write_bytes(0x2EA05C, bytearray([item_id_table[world.slime_pile_wanted_item]]))
rom.write_bytes(0x2F61F6, bytearray([item_id_table[world.slime_pile_wanted_item]]))
hintable_locations = [
location for location in world.multiworld.get_locations()
if location.player == world.player or location.item.player == world.player
]
world.hint_pointer = 0x0000
world.hint_number = 0
for index, hint in enumerate(world.in_game_hint_types):
if hint == "item_at_location":
for location in hintable_locations:
if location.name == world.hinted_locations[index] and location.player == world.player:
parse_hint_data(world, location, rom, hint, index)
elif hint == "region_progression_check":
world.progression_count = 0
for location in hintable_locations:
if location.name in location_groups[world.hinted_regions[index]] and location.player == world.player:
if ItemClassification.progression in location.item.classification:
world.progression_count += 1
world.hinted_area = world.hinted_regions[index] # im doing a little sneaky
parse_hint_data(world, location, rom, hint, index)
elif hint == "hint_for_good_item" or hint == "prog_item_at_region":
hintable_locations_2 = []
for location in hintable_locations:
if location.item.name == world.hinted_items[index] and location.item.player == world.player:
hintable_locations_2.append(location)
if hintable_locations_2 == []:
# This is just failsafe behavior
warning(f"Warning: Unable to create local hint for {world.hinted_items[index]} for "
+ f"{world.multiworld.get_player_name(world.player)}'s EarthBound world."
+ " Please report this.")
location = world.random.choice(hintable_locations)
else:
location = world.random.choice(hintable_locations_2)
parse_hint_data(world, location, rom, hint, index)
elif hint == "item_in_local_region":
for location in hintable_locations:
if location.name == world.hinted_locations[index] and location.player == world.player:
parse_hint_data(world, location, rom, hint, index)
else:
location = "null"
parse_hint_data(world, location, rom, hint, index)
for location in hintable_locations:
if location.item.name == "Paula":
world.paula_region = location.parent_region
if location.item.name == "Jeff":
world.jeff_region = location.parent_region
if location.item.name == "Poo":
world.poo_region = location.parent_region
if world.options.skip_prayer_sequences:
rom.write_bytes(0x07BC96, bytearray([0x02]))
rom.write_bytes(0x07BA2C, bytearray([0x02]))
rom.write_bytes(0x07BAC7, bytearray([0x02]))
rom.write_bytes(0x07BB38, bytearray([0x02]))
rom.write_bytes(0x07BBF3, bytearray([0x02]))
rom.write_bytes(0x07BC56, bytearray([0x02]))
rom.write_bytes(0x07B9A1, bytearray([0x1f, 0xeb, 0xff, 0x02, 0x1f, 0x1f, 0xca, 0x01, 0x06, 0x1f, 0x1f, 0x72, 0x01, 0x06, 0x02])) # Clean up overworld stuff
if world.options.easy_deaths:
rom.write_bytes(0x2EBFF9, bytearray([0x0A]))
rom.write_bytes(0x04C7CE, bytearray([0x5C, 0x8A, 0xFB, 0xEF])) # Jump to code that restores the party
rom.write_bytes(0x04C7D4, bytearray([0xEA, 0xEA, 0xEA]))
# rom.write_bytes(0x04C7DA, bytearray([0xEA, 0xEA]))#Stop the game from zeroing stuff
rom.write_bytes(0x0912F2, bytearray([0x0A, 0xFE, 0xBF, 0xEE]))
rom.write_bytes(0x2EBFFE, bytearray([0x00, 0x1B, 0x04, 0x15, 0x38, 0x1F, 0x81, 0xFF, 0xFF, 0x1B, 0x04, 0x0A, 0xF7, 0x12, 0xC9])) # Hospitals = 0$
rom.write_bytes(0x04C822, bytearray([0xEA, 0xEA, 0xEA, 0xEA]))
rom.write_bytes(0x04C7F7, bytearray([0x00, 0x00])) # Stop "rounding up" the money
if world.options.magicant_mode >= 2:
rom.write_bytes(0x077629, bytearray([item_id_table[world.magicant_junk[0]]]))
rom.write_bytes(0x077614, bytearray([item_id_table[world.magicant_junk[0]]]))
rom.write_bytes(0x0FF25C, bytearray([item_id_table[world.magicant_junk[1]]]))
rom.write_bytes(0x0FF27E, bytearray([item_id_table[world.magicant_junk[2]]]))
rom.write_bytes(0x0FF28F, bytearray([item_id_table[world.magicant_junk[3]]]))
rom.write_bytes(0x0FF2A0, bytearray([item_id_table[world.magicant_junk[4]]]))
rom.write_bytes(0x0FF26D, bytearray([item_id_table[world.magicant_junk[5]]]))
rom.write_bytes(0x02EC1AA, bytearray([world.options.sanctuaries_required.value]))
if world.options.alternate_sanctuary_goal and world.options.giygas_required:
rom.write_bytes(0x02EC1E2, bytearray([0xFD, 0xC1, 0xEE]))
if world.options.magicant_mode == 1 and world.options.giygas_required: # Apple kid text
rom.write_bytes(0x2EC1D8, bytearray([0x33, 0xC2, 0xEE]))
elif world.options.magicant_mode == 2:
rom.write_bytes(0x2EC1D8, bytearray([0x6A, 0xC2, 0xEE]))
if not world.options.giygas_required:
rom.write_bytes(0x2EC164, bytearray([0xE8, 0xF0, 0xEE]))
rom.write_bytes(0x02EC1E2, bytearray([0x40, 0xC1, 0xEE]))
rom.write_bytes(0x02EC1E2, bytearray([0x40, 0xC1, 0xEE]))
flavor_address = 0x3FAF10
for i in range(4):
rom.copy_bytes(world.flavor_pointer[i], 2, 0x34B110 + (2 * i))
rom.copy_bytes(0x202008, 0x100, 0x34B000)
for i in range(4):
if world.available_flavors[i] not in ["Mint flavor", "Strawberry flavor", "Banana flavor", "Peanut flavor"]:
rom.write_bytes(flavor_address, bytearray(world.flavor_text[i]))
flavor_addr = flavor_address - 0x3F0000
flavor_addr = struct.pack("H", flavor_addr)
rom.write_bytes(world.flavor_pointer[i], flavor_addr)
rom.write_bytes(world.flavor_pointer[i] + 5, bytearray([0xFF]))
flavor_address += len(world.flavor_text[i])
rom.write_bytes(0x202008 + (0x40 * i), bytearray(flavor_data[world.available_flavors[i]]))
else:
rom.copy_bytes(vanilla_flavor_pointers[world.available_flavors[i]][1], 0x40, 0x202008 + (0x40 * i))
rom.copy_bytes(vanilla_flavor_pointers[world.available_flavors[i]][2], 2, world.flavor_pointer[i])
rom.write_bytes(0x048037, bytearray(world.lumine_text))
starting_item_address = 0
starting_psi = 0
starting_char = 0
starting_psi_types = []
starting_character_count = []
starting_inventory_pointers = {
"Ness": 0x99F3,
"Paula": 0x9A53,
"Jeff": 0x9AB4,
"Poo": 0x9B0F
}
starting_inv_amounts = {
"Ness": 0x0B,
"Paula": 0x0A,
"Jeff": 0x08,
"Poo": 0x0C
}
location = world.multiworld.get_location("Poo - Starting Item", world.player)
if world.starting_character == "Poo" and location.item.name in item_id_table and location.item.player == world.player:
starting_inventory_pointers["Poo"] = 0x9B10
starting_inv_amounts["Poo"] = 0x0B
rom.write_bytes(0x16FB66, struct.pack("H", starting_inventory_pointers[world.starting_character]))
rom.write_bytes(0x16FB68, struct.pack("H", starting_inv_amounts[world.starting_character]))
for item in world.multiworld.precollected_items[player]:
if item.name == world.starting_character: # Write the starting character
rom.write_bytes(0x00B672, bytearray([world.options.starting_character.value + 1]))
if world.options.remote_items:
continue
if item.name == "Photograph":
rom.write_bytes(0x17FEA8, bytearray([0x01]))
if item.name == "Poo" and world.multiworld.get_location("Poo - Starting Item", world.player).item.name in special_name_table and item.player == world.player:
world.multiworld.push_precollected(world.multiworld.get_location("Poo - Starting Item", world.player).item)
elif item.name == "Poo" and world.multiworld.get_location("Poo - Starting Item", world.player).item.name in money_item_table:
world.starting_money += money_item_table[world.multiworld.get_location("Poo - Starting Item", world.player).item.name]
# if item.name == "Poo" and world.multiworld.get_location("Poo - Starting Item", world.player).item.name == "Photograph":
# rom.write_bytes(0x17FEA9, bytearray([0x01]))
if item.name in ["Progressive Bat", "Progressive Fry Pan", "Progressive Gun", "Progressive Bracelet",
"Progressive Other"]:
old_item_name = item.name
item.name = world.progressive_item_groups[item.name][world.start_prog_counts[item.name]]
if world.start_prog_counts[old_item_name] != len(world.progressive_item_groups[old_item_name]) - 1:
world.start_prog_counts[old_item_name] += 1
if item.name in item_id_table:
rom.write_bytes(0x375000 + starting_item_address, bytearray([item_id_table[item.name]]))
starting_item_address += 1
elif item.name in psi_item_table:
if item.name != "Progressive Poo PSI":
if item.name not in starting_psi_types:
rom.write_bytes(0x17FC7C + starting_psi, bytearray([starting_psi_table[item.name]]))
starting_psi_types.append(item.name)
starting_psi += 1
else:
if starting_psi_types.count(item.name) < 2:
rom.write_bytes(0x17FC7C + starting_psi, bytearray([starting_psi_table[item.name]]))
starting_psi_types.append(item.name)
starting_psi += 1
elif item.name in character_item_table and item.name != "Photograph":
if item.name not in starting_character_count:
rom.write_bytes(0x17FC8D + starting_char, bytearray([party_id_nums[item.name]]))
starting_character_count.append(item.name)
starting_char += 1
elif item.name in money_item_table:
world.starting_money += money_item_table[item]
if world.options.random_battle_backgrounds:
bpp2_bgs = [bg_id for bg_id, bpp in battle_bg_bpp.items() if bpp == 2]
bpp4_bgs = [bg_id for bg_id, bpp in battle_bg_bpp.items() if bpp == 4]
for i in range(483):
world.flipped_bg = world.random.randint(0, 100)
if i == 480:
drawn_background = struct.pack("H", 0x00E3)
else:
drawn_background = struct.pack("H", world.random.randint(0x01, 0x0146))
if battle_bg_bpp[struct.unpack("H", drawn_background)[0]] == 4:
drawn_background_2 = struct.pack("H", 0x0000)
else:
drawn_background_2 = struct.pack("H", world.random.choice(bpp2_bgs))
#print(f"ello mate we are doing background {i} at {hex(0xCBD89A + (i * 4))}, the background is {drawn_background[0]}.")
if world.flipped_bg > 33 or drawn_background not in bpp2_bgs:
rom.write_bytes(0x0BD89A + (i * 4), drawn_background)
rom.write_bytes(0x0BD89C + (i * 4), drawn_background_2)
else:
rom.write_bytes(0x0BD89A + (i * 4), drawn_background_2)
rom.write_bytes(0x0BD89C + (i * 4), drawn_background)
rom.write_bytes(0x00B5F1, struct.pack("H", world.random.choice(bpp4_bgs)))
if world.options.random_swirl_colors:
if world.random.random() < 0.5:
rom.write_bytes(0x02E98A, bytearray([0x7F])) # Color math mode
rom.write_bytes(0x02E996, bytearray([0x3F]))
rom.write_bytes(0x300240, bytearray([world.random.randint(0x00, 0x1F)])) # Normal swirls
rom.write_bytes(0x300245, bytearray([world.random.randint(0x00, 0x1F)]))
rom.write_bytes(0x30024A, bytearray([world.random.randint(0x00, 0x1F)]))
rom.write_bytes(0x300253, bytearray([world.random.randint(0x00, 0x1F)])) # Green swirls
rom.write_bytes(0x300258, bytearray([world.random.randint(0x00, 0x1F)]))
rom.write_bytes(0x30025D, bytearray([world.random.randint(0x00, 0x1F)]))
rom.write_bytes(0x300269, bytearray([world.random.randint(0x00, 0x1F)])) # Red swirls
rom.write_bytes(0x30026E, bytearray([world.random.randint(0x00, 0x1F)]))
rom.write_bytes(0x300273, bytearray([world.random.randint(0x00, 0x1F)]))
if not world.options.prefixed_items:
rom.write_bytes(0x15F9DC, bytearray([0x06]))
rom.write_bytes(0x15F9DE, bytearray([0x08]))
rom.write_bytes(0x15F9E0, bytearray([0x05]))
rom.write_bytes(0x15F9E2, bytearray([0x0B]))
rom.write_bytes(0x15F9E4, bytearray([0x0F]))
rom.write_bytes(0x15F9E6, bytearray([0x10]))
# change if necessary
if world.options.psi_shuffle:
write_psi(world, rom)
world.description_pointer = 0x1000
if world.options.armorizer:
randomize_armor(world, rom)
if world.options.weaponizer:
randomize_weapons(world, rom)
music_randomizer(world, rom)
if world.options.map_palette_shuffle:
map_palette_shuffle(world, rom)
if world.options.randomize_psi_palettes:
randomize_psi_palettes(world, rom)
if world.options.randomize_enemy_attributes:
randomize_enemy_attributes(world, rom)
if world.options.randomize_enemy_stats:
randomize_enemy_stats(world, rom)
if world.options.randomize_enemy_attacks:
randomize_enemy_attacks(world, rom)
apply_enemy_shuffle(world, rom)
# randomize_food(world,rom)
write_bosses(world, rom)
if world.options.dungeon_shuffle:
write_dungeon_entrances(world, rom)
world.get_all_spheres.wait()
calculate_scaling(world)
if world.options.shop_randomizer:
write_shop_checks(world, rom, shop_checks)
scale_enemies(world, rom)
world.badge_name = badge_names[world.franklin_protection]
world.badge_name = text_encoder(world.badge_name, 23)
world.badge_name.extend([0x00])
world.starting_money = min(world.starting_money, 99999)
world.starting_money = struct.pack('<I', world.starting_money)
rom.write_bytes(0x17FCD0, world.starting_money)
rom.write_bytes(0x17FCE0, world.prayer_player)
rom.write_bytes(0x17FD00, world.credits_player)
rom.write_bytes(0x155027, world.badge_name)
rom.write_bytes(0x17FD50, struct.pack("H", world.multiworld.players))
rom.write_bytes(0x3FF0A0, world.world_version.encode("ascii"))
display_version = text_encoder(world_version, 15)
display_version.extend([0x02])
rom.write_bytes(0x3CFFBF, display_version)
for element in world.franklinbadge_elements:
for address in protection_checks[element]:
if element == world.franklin_protection:
rom.write_bytes(address, [0xF0])
else:
rom.write_bytes(address, [0x80])
# THIS WILL CRASH IF ADDRESS IS WRONG.
rom.write_bytes(0x2EC909, struct.pack("I", protection_text[world.franklin_protection][0])) # help text
rom.write_bytes(0x2EC957, struct.pack("I", protection_text[world.franklin_protection][1])) # battle text
from Utils import __version__
rom.name = bytearray(f'MOM2AP{__version__.replace(".", "")[0:3]}_{player}_{world.multiworld.seed:11}\0', "utf8")[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x00FFC0, rom.name)
rom.write_file("token_patch.bin", rom.get_token_binary())
class EBProcPatch(APProcedurePatch, APTokenMixin):
hash = valid_hashes
game = "EarthBound"
patch_file_ending = ".apeb"
result_file_ending = ".sfc"
name: bytearray
procedure = [
("apply_bsdiff4", ["earthbound_basepatch.bsdiff4"]),
("apply_tokens", ["token_patch.bin"]),
("repoint_vanilla_tables", [])
]
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes()
def write_bytes(self, offset: int, value: typing.Iterable[int]) -> None:
self.write_token(APTokenTypes.WRITE, offset, bytes(value))
def copy_bytes(self, source: int, amount: int, destination: int) -> None:
self.write_token(APTokenTypes.COPY, destination, (amount, source))
class EBPatchExtensions(APPatchExtension):
game = "EarthBound"
@staticmethod
def repoint_vanilla_tables(caller: APProcedurePatch, rom: bytes) -> bytes:
rom = LocalRom(rom)
version_check = rom.read_bytes(0x3FF0A0, 16)
version_check = version_check.split(b'\x00', 1)[0]
version_check_str = version_check.decode("ascii")
client_version = world_version
if client_version != version_check_str and version_check_str != "":
raise Exception(f"Error! Patch generated on EarthBound APWorld version {version_check_str} doesn't match client version {client_version}! " +
f"Please use EarthBound APWorld version {version_check_str} for patching.")
elif version_check_str == "":
raise Exception(f"Error! Patch generated on old EarthBound APWorld version, doesn't match client version {client_version}! " +
f"Please verify you are using the same APWorld as the generator.")
for action_number in range(0x013F):
current_action = rom.read_bytes(0x157B68 + (12 * action_number), 12)
rom.write_bytes(0x3FAFB0 + (12 * action_number), current_action)
for psi_number in range(0x35):
current_action = rom.read_bytes(0x158A50 + (15 * psi_number), 15)
rom.write_bytes(0x350000 + (15 * psi_number), current_action)
psi_text_table = rom.read_bytes(0x158D7A, (25 * 17))
rom.write_bytes(0x3B0500, psi_text_table)
psi_anim_config = rom.read_bytes(0x0CF04D, 0x0198)
rom.write_bytes(0x360000, psi_anim_config)
psi_anim_pointers = rom.read_bytes(0x0CF58F, 0x088)
rom.write_bytes(0x360400, psi_anim_pointers)
psi_anim_palettes = rom.read_bytes(0x0CF47F, 0x0110)
rom.write_bytes(0x360600, psi_anim_palettes)
for psi_number in range(0x32):
psi_anim = rom.read_bytes(0x2F8583 + (0x04 * psi_number), 4)
rom.write_bytes(0x3B0003 + (4 * psi_number), psi_anim)
rom.write_bytes(0x3B0003, bytearray([0x4C]))
# rom.write_bytes(0x3B0002, bytearray([0x45]))
main_font_data = rom.read_bytes(0x210C7A, 96)
main_font_gfx = rom.read_bytes(0x210CDA, 0x0C00)
saturn_font_data = rom.read_bytes(0x201359, 96)
saturn_font_gfx = rom.read_bytes(0x2013B9, 0x0C00)
letter_n = rom.read_bytes(0x21169F, 6)
letter_a = rom.read_bytes(0x2114FF, 6)
letter_e = rom.read_bytes(0x21157F, 6)
saturn_a = rom.read_bytes(0x2017DD, 9)
saturn_n = rom.read_bytes(0x20197E, 8)
saturn_e = rom.read_bytes(0x20185C, 24)
accent_tilde = rom.read_bytes(0x2118A1, 2)
saturn_tilde = rom.read_bytes(0x201F7F, 2)
rom.write_bytes(0x3A0000, main_font_data)
rom.write_bytes(0x3C0000, main_font_gfx)
rom.write_bytes(0x3A0100, saturn_font_data)
rom.write_bytes(0x3C1000, saturn_font_gfx)
rom.write_bytes(0x3C0D25, letter_n) # Setup n
rom.write_bytes(0x3C0D45, letter_a) # Setup a
rom.write_bytes(0x3C0D65, letter_e)
rom.write_bytes(0x3C0D22, accent_tilde)
rom.write_bytes(0x3C1D25, saturn_n) # Setup n
rom.write_bytes(0x3C1D45, saturn_a)
rom.write_bytes(0x3C1D63, saturn_e)
rom.write_bytes(0x3C1D22, saturn_tilde)
# ---------------------------------------
ness_level = rom.read_bytes(0x15F5FB, 1)
paula_level = rom.read_bytes(0x15f60f, 1)
jeff_level = rom.read_bytes(0x15f623, 1)
poo_level = rom.read_bytes(0x15f637, 1)
ness_start_exp = rom.read_bytes(0x158F49 + (ness_level[0] * 4), 4)
paula_start_exp = rom.read_bytes(0x1590D9 + (paula_level[0] * 4), 4)
jeff_start_exp = rom.read_bytes(0x159269 + (jeff_level[0] * 4), 4)
poo_start_exp = rom.read_bytes(0x1593F9 + (poo_level[0] * 4), 4)
rom.write_bytes(0x17FD40, ness_start_exp)
rom.write_bytes(0x17FD44, paula_start_exp)
rom.write_bytes(0x17FD48, jeff_start_exp)
rom.write_bytes(0x17FD4C, poo_start_exp)
return rom.get_bytes()
def get_base_rom_bytes(file_name: str = "") -> bytes:
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
file_name = get_base_rom_path(file_name)
base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb")))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if basemd5.hexdigest() not in valid_hashes:
raise Exception('Supplied Base Rom does not match known MD5 for US(1.0) release. '
'Get the correct game and version, then dump it')
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
return base_rom_bytes
def get_base_rom_path(file_name: str = "") -> str:
options: settings.Settings = settings.get_settings()
if not file_name:
file_name = options["earthbound_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.user_path(file_name)
return file_name
# Fix hint text, I have a special idea where I can give it info on a random region

128
worlds/earthbound/Rules.py Normal file
View File

@@ -0,0 +1,128 @@
from worlds.generic.Rules import set_rule, forbid_items_for_player, add_rule
from typing import TYPE_CHECKING
from .Options import ShopRandomizer, MonkeyCavesMode
if TYPE_CHECKING:
from . import EarthBoundWorld
def set_location_rules(world: "EarthBoundWorld") -> None:
player = world.player
twoson_paula_room_present = world.get_location("Twoson - Paula's Room Present")
can_buy_pizza = world.get_location("Threed - Downtown Trashcan")
set_rule(world.multiworld.get_location("Onett - Traveling Entertainer", player), lambda state: state.has("Key to the Shack", player))
set_rule(world.multiworld.get_location("Onett - South Road Present", player), lambda state: state.has("Police Badge", player))
set_rule(world.multiworld.get_location("Onett - Tracy Gift", player), lambda state: state.has("Ness", player))
set_rule(world.multiworld.get_location("Twoson - Paula's Mother", player), lambda state: state.has("Paula", player))
set_rule(world.multiworld.get_location("Twoson - Everdred Meeting", player), lambda state: state.has("Paula", player))
set_rule(world.multiworld.get_location("Twoson - Insignificant Location", player), lambda state: state.has("Insignificant Item", player))
set_rule(world.multiworld.get_location("Happy-Happy Village - Defeat Carpainter", player), lambda state: state.has("Franklin Badge", player))
set_rule(world.multiworld.get_location("Carpainter Defeated", player), lambda state: state.has("Franklin Badge", player))
set_rule(world.multiworld.get_location("Happy-Happy Village - Prisoner", player), lambda state: state.has("Key to the Cabin", player))
set_rule(world.multiworld.get_location("Threed - Boogey Tent Trashcan", player), lambda state: state.has("Jeff", player))
set_rule(world.multiworld.get_location("Threed - Zombie Prisoner", player), lambda state: state.has("Bad Key Machine", player))
set_rule(world.multiworld.get_location("Saturn Valley - Post Belch Gift #1", player), lambda state: state.has("Threed Tunnels Clear", player))
set_rule(world.multiworld.get_location("Saturn Valley - Post Belch Gift #2", player), lambda state: state.has("Threed Tunnels Clear", player))
set_rule(world.multiworld.get_location("Saturn Valley - Post Belch Gift #3", player), lambda state: state.has("Threed Tunnels Clear", player))
set_rule(world.multiworld.get_location("Saturn Valley - Saturn Coffee", player), lambda state: state.has("Threed Tunnels Clear", player))
set_rule(world.multiworld.get_location("Monkey Caves - Talah Rama Chest #1", player), lambda state: state.has("Pencil Eraser", player))
set_rule(world.multiworld.get_location("Monkey Caves - Talah Rama Chest #2", player), lambda state: state.has("Pencil Eraser", player))
set_rule(world.multiworld.get_location("Monkey Caves - Talah Rama Gift", player), lambda state: state.has("Pencil Eraser", player))
set_rule(world.multiworld.get_location("Monkey Caves - Monkey Power", player), lambda state: state.has("Pencil Eraser", player))
set_rule(world.multiworld.get_location("Dusty Dunes - Mine Reward", player), lambda state: state.can_reach_region("Gold Mine", player))
set_rule(world.multiworld.get_location("Snow Wood - Upper Right Locker", player), lambda state: state.has("Key to the Locker", player))
set_rule(world.multiworld.get_location("Snow Wood - Upper Left Locker", player), lambda state: state.has("Key to the Locker", player))
set_rule(world.multiworld.get_location("Snow Wood - Bottom Right Locker", player), lambda state: state.has("Key to the Locker", player))
set_rule(world.multiworld.get_location("Snow Wood - Bottom Left Locker", player), lambda state: state.has("Key to the Locker", player))
set_rule(world.multiworld.get_location("Fourside - Bakery 2F Gift", player), lambda state: state.has("Contact Lens", player))
set_rule(world.multiworld.get_location("Fourside - Department Store Blackout", player), lambda state: state.has("Jeff", player))
set_rule(world.multiworld.get_location("Fourside - Venus Gift", player), lambda state: state.has("Diamond", player))
set_rule(world.multiworld.get_location("Summers - Museum Item", player), lambda state: state.has("Tiny Ruby", player))
set_rule(world.multiworld.get_location("Dalaam - Trial of Mu", player), lambda state: state.has("Poo", player))
set_rule(world.multiworld.get_location("Poo - Starting Item", player), lambda state: state.has("Poo", player))
set_rule(world.multiworld.get_location("Deep Darkness - North Alcove Truffle", player), lambda state: state.has("Piggy Nose", player))
set_rule(world.multiworld.get_location("Deep Darkness - Near Land Truffle", player), lambda state: state.has("Piggy Nose", player))
set_rule(world.multiworld.get_location("Deep Darkness - Present Truffle", player), lambda state: state.has("Piggy Nose", player))
set_rule(world.multiworld.get_location("Deep Darkness - Village Truffle", player), lambda state: state.has("Piggy Nose", player))
set_rule(world.multiworld.get_location("Deep Darkness - Entrance Truffle", player), lambda state: state.has("Piggy Nose", player))
set_rule(world.multiworld.get_location("Tenda Village - Tenda Tea", player), lambda state: state.has("Shyness Book", player))
set_rule(world.multiworld.get_location("Tenda Village - Tenda Gift", player), lambda state: state.has("Shyness Book", player))
set_rule(world.multiworld.get_location("Tenda Village - Tenda Gift #2", player), lambda state: state.has("Shyness Book", player))
set_rule(world.multiworld.get_location("Lost Underworld - Talking Rock", player), lambda state: state.has("Tendakraut", player))
set_rule(world.multiworld.get_location("Sanctuary Goal", player), lambda state: state.has("Melody", player, world.options.sanctuaries_required.value))
forbid_items_for_player(world.multiworld.get_location("Poo - Starting Item", player), {"Poo"}, player)
forbid_items_for_player(world.multiworld.get_location("Poo - Starting Item", player), {"Progressive Bat"}, player)
forbid_items_for_player(world.multiworld.get_location("Poo - Starting Item", player), {"Progressive Gun"}, player)
forbid_items_for_player(world.multiworld.get_location("Poo - Starting Item", player), {"Progressive Fry Pan"}, player)
forbid_items_for_player(world.multiworld.get_location("Poo - Starting Item", player), {"Progressive Bracelet"}, player)
forbid_items_for_player(world.multiworld.get_location("Poo - Starting Item", player), {"Progressive Other"}, player)
if world.options.giygas_required:
set_rule(world.multiworld.get_location("Giygas", player), lambda state: state.has("Paula", player))
if world.options.monkey_caves_mode < MonkeyCavesMode.option_shop: # 2
set_rule(world.multiworld.get_location("Monkey Caves - West 2F Left Chest", player), lambda state: (twoson_paula_room_present.can_reach(state) or can_buy_pizza.can_reach(state)))
set_rule(world.multiworld.get_location("Monkey Caves - East 2F Left Chest", player), lambda state: (twoson_paula_room_present.can_reach(state) or can_buy_pizza.can_reach(state)))
set_rule(world.multiworld.get_location("Monkey Caves - East End Chest", player), lambda state: (twoson_paula_room_present.can_reach(state) or can_buy_pizza.can_reach(state)))
set_rule(world.multiworld.get_location("Monkey Caves - East End Trashcan", player), lambda state: (twoson_paula_room_present.can_reach(state) or can_buy_pizza.can_reach(state)))
set_rule(world.multiworld.get_location("Monkey Caves - East West 3F Right Chest #1", player), lambda state: (twoson_paula_room_present.can_reach(state) or can_buy_pizza.can_reach(state)))
set_rule(world.multiworld.get_location("Monkey Caves - East West 3F Right Chest #2", player), lambda state: (twoson_paula_room_present.can_reach(state) or can_buy_pizza.can_reach(state)))
add_rule(world.multiworld.get_location("Monkey Caves - Talah Rama Chest #1", player), lambda state: (twoson_paula_room_present.can_reach(state) or can_buy_pizza.can_reach(state)))
add_rule(world.multiworld.get_location("Monkey Caves - Talah Rama Chest #2", player), lambda state: (twoson_paula_room_present.can_reach(state) or can_buy_pizza.can_reach(state)))
add_rule(world.multiworld.get_location("Monkey Caves - Talah Rama Gift", player), lambda state: (twoson_paula_room_present.can_reach(state) or can_buy_pizza.can_reach(state)))
add_rule(world.multiworld.get_location("Monkey Caves - Monkey Power", player), lambda state: (twoson_paula_room_present.can_reach(state) or can_buy_pizza.can_reach(state)))
if world.options.no_free_sanctuaries:
lilliput_steps = world.multiworld.get_entrance(f"Happy-Happy Village -> {world.dungeon_connections['Lilliput Steps']}", player)
fire_spring = world.multiworld.get_entrance(f"Lost Underworld -> {world.dungeon_connections['Fire Spring']}", player)
add_rule(fire_spring, lambda state: state.has("Tenda Lavapants", player))
add_rule(lilliput_steps, lambda state: state.has("Tiny Key", player))
if world.options.shop_randomizer == ShopRandomizer.option_shopsanity: # 2
set_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 1", player), lambda state: state.has("Tendakraut", player))
set_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 2", player), lambda state: state.has("Tendakraut", player))
set_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 3", player), lambda state: state.has("Tendakraut", player))
set_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 4", player), lambda state: state.has("Tendakraut", player))
set_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 5", player), lambda state: state.has("Tendakraut", player))
set_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 6", player), lambda state: state.has("Tendakraut", player))
set_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 7", player), lambda state: state.has("Tendakraut", player))
set_rule(world.multiworld.get_location("Dusty Dunes - Mine Food Cart Slot 1", player), lambda state: state.has("Mining Permit", player))
set_rule(world.multiworld.get_location("Dusty Dunes - Mine Food Cart Slot 2", player), lambda state: state.has("Mining Permit", player))
set_rule(world.multiworld.get_location("Dusty Dunes - Mine Food Cart Slot 3", player), lambda state: state.has("Mining Permit", player))
set_rule(world.multiworld.get_location("Dusty Dunes - Mine Food Cart Slot 4", player), lambda state: state.has("Mining Permit", player))
set_rule(world.multiworld.get_location("Dusty Dunes - Mine Food Cart Slot 5", player), lambda state: state.has("Mining Permit", player))
set_rule(world.multiworld.get_location("Dusty Dunes - Mine Food Cart Slot 6", player), lambda state: state.has("Mining Permit", player))
set_rule(world.multiworld.get_location("Dusty Dunes - Mine Food Cart Slot 7", player), lambda state: state.has("Mining Permit", player))
set_rule(world.multiworld.get_location("Saturn Valley Shop - Post-Belch Saturn Slot 1", player), lambda state: state.has("Threed Tunnels Clear", player))
set_rule(world.multiworld.get_location("Saturn Valley Shop - Post-Belch Saturn Slot 2", player), lambda state: state.has("Threed Tunnels Clear", player))
set_rule(world.multiworld.get_location("Saturn Valley Shop - Post-Belch Saturn Slot 3", player), lambda state: state.has("Threed Tunnels Clear", player))
set_rule(world.multiworld.get_location("Saturn Valley Shop - Post-Belch Saturn Slot 4", player), lambda state: state.has("Threed Tunnels Clear", player))
set_rule(world.multiworld.get_location("Deep Darkness - Arms Dealer Slot 1", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Deep Darkness - Arms Dealer Slot 2", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Deep Darkness - Arms Dealer Slot 3", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Deep Darkness - Arms Dealer Slot 4", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Deep Darkness - Businessman Slot 1", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Deep Darkness - Businessman Slot 2", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Deep Darkness - Businessman Slot 3", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Deep Darkness - Businessman Slot 4", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Deep Darkness - Businessman Slot 5", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Deep Darkness - Businessman Slot 6", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Deep Darkness - Businessman Slot 7", player), lambda state: state.has("ATM Access", player))
add_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 1", player), lambda state: state.has("ATM Access", player))
add_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 2", player), lambda state: state.has("ATM Access", player))
add_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 3", player), lambda state: state.has("ATM Access", player))
add_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 4", player), lambda state: state.has("ATM Access", player))
add_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 5", player), lambda state: state.has("ATM Access", player))
add_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 6", player), lambda state: state.has("ATM Access", player))
add_rule(world.multiworld.get_location("Lost Underworld - Tenda Camp Shop Slot 7", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Dalaam Restaurant - Slot 1", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Dalaam Restaurant - Slot 2", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Dalaam Restaurant - Slot 3", player), lambda state: state.has("ATM Access", player))
set_rule(world.multiworld.get_location("Dalaam Restaurant - Slot 4", player), lambda state: state.has("ATM Access", player))

View File

@@ -0,0 +1,609 @@
import os
import typing
import threading
import pkgutil
from typing import List, Set, Dict, TextIO, Tuple
from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification
from Fill import fill_restrictive
from worlds.AutoWorld import World, WebWorld
import itertools
import settings
from .Items import get_item_names_per_category, item_table
from .Locations import get_locations
from .Regions import init_areas, connect_area_exits
from .Options import EBOptions, eb_option_groups
from .setup_game import setup_gamevars, place_static_items
from .modules.enemy_data import initialize_enemies
from .modules.flavor_data import create_flavors
from .game_data.local_data import item_id_table, world_version
from .modules.hint_data import setup_hints
from .game_data.text_data import spoiler_psi, spoiler_starts, spoiler_badges
from .Client import EarthBoundClient
from .Rules import set_location_rules
from .Rom import patch_rom, EBProcPatch, valid_hashes
from .game_data.static_location_data import location_ids, location_groups
from .modules.equipamizer import EBArmor, EBWeapon
from .modules.boss_shuffle import BossData, SlotInfo
from worlds.generic.Rules import add_item_rule
from Options import OptionError
class EBSettings(settings.Group):
class RomFile(settings.SNESRomPath):
"""File name of the EarthBound US ROM"""
description = "EarthBound ROM File"
copy_to = "EarthBound.sfc"
md5s = valid_hashes
rom_file: RomFile = RomFile(RomFile.copy_to)
class EBWeb(WebWorld):
theme = "ocean"
setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the EarthBound randomizer"
"and connecting to an Archipelago server.",
"English",
"setup_en.md",
"setup/en",
["Pink Switch"]
)
tutorials = [setup_en]
option_groups = eb_option_groups
# option_presets = eb_option_presets
class EBItem(Item):
game: str = "EarthBound"
class EarthBoundWorld(World):
"""EarthBound is a contemporary-themed JRPG. Take four psychically-endowed children
across the world in search of 8 Melodies to defeat Giygas, the cosmic evil."""
game = "EarthBound"
option_definitions = EBOptions
data_version = 1
required_client_version = (0, 5, 0)
item_name_to_id = {item: data.code for item, data in item_table.items() if data.code}
location_name_to_id = location_ids
item_name_groups = get_item_names_per_category()
location_name_groups = location_groups
web = EBWeb()
settings: typing.ClassVar[EBSettings]
# topology_present = True
options_dataclass = EBOptions
options: EBOptions
locked_locations: List[str]
location_cache: List[Location]
def __init__(self, multiworld: MultiWorld, player: int):
self.rom_name_available_event = threading.Event()
super().__init__(multiworld, player)
self.locked_locations = []
self.location_cache = []
self.event_count = 8
self.progressive_filler_bats: int = 0
self.progressive_filler_pans: int = 0
self.progressive_filler_guns: int = 0
self.progressive_filler_bracelets: int = 0
self.progressive_filler_other: int = 0
self.world_version: str = world_version
self.armor_list = Dict[str, EBArmor]
self.weapon_list = Dict[str, EBWeapon]
self.boss_slots = Dict[str, SlotInfo]
self.boss_info = Dict[str, BossData]
self.starting_character: str | None = None
self.locals = []
self.rom_name = None
self.starting_area_teleport = None
self.common_gear = []
self.uncommon_gear = []
self.rare_gear = []
self.get_all_spheres = threading.Event()
self.boss_list: List[str] = []
self.starting_region = str
self.start_location = int
self.dungeon_connections: dict[str, str] = {}
self.has_generated_output: bool = False
self.hint_man_hints: list[tuple[int | str, player]] = []
self.common_items = [
"Cookie",
"Bag of Fries",
"Teddy Bear",
"Hamburger",
"Boiled Egg",
"Fresh Egg",
"Picnic Lunch",
"Croissant",
"Bread Roll",
"Can of Fruit Juice",
"Royal Iced Tea",
"Protein Drink",
"Bottle of Water",
"Cold Remedy",
"Vial of Serum",
"Ketchup Packet",
"Sugar Packet",
"Tin of Cocoa",
"Carton of Cream",
"Sprig of Parsley",
"Jar of Hot Sauce",
"Salt Packet",
"Wet Towel",
"Refreshing Herb",
"Ruler",
"Protractor",
"Insecticide Spray",
"Rust Promoter",
"Stag Beetle",
"Toothbrush",
"Handbag Strap",
"Chick",
"Chicken",
"Trout Yogurt",
"Banana",
"Calorie Stick",
"Gelato de Resort",
"Snake",
"Cup of Noodles",
"Cup of Coffee",
"Double Burger",
"Bean Croquette",
"Molokheiya Soup",
"Plain Roll",
"Magic Tart",
"PSI Caramel",
"Popsicle",
"Bottle Rocket"
]
self.common_gear = [
"Yo-yo",
"Slingshot",
"Travel Charm",
"Great Charm",
"Ribbon",
"Red Ribbon"
]
self.uncommon_items = [
"Pasta di Summers",
"Pizza",
"Chef's Special",
"Super Plush Bear",
"Jar of Delisauce",
"Secret Herb",
"Xterminator Spray",
"Snake Bag",
"Bomb",
"Rust Promoter DX",
"Pair of Dirty Socks",
"Mummy Wrap",
"Pharaoh's Curse",
"Sudden Guts Pill",
"Picture Postcard",
"Viper",
"Repel Sandwich",
"Lucky Sandwich",
"Peanut Cheese Bar",
"Bowl of Rice Gruel",
"Kabob",
"Plain Yogurt",
"Beef Jerky",
"Mammoth Burger",
"Bottle of DXwater",
"Magic Pudding",
"Big Bottle Rocket",
"Bazooka",
"Meteornium"
]
self.uncommon_gear = [
"Trick Yo-yo",
"Bionic Slingshot",
"Crystal Charm",
"Defense Ribbon",
"Earth Pendant",
"Flame Pendant",
"Rain Pendant",
"Night Pendant"
]
self.rare_items = [
"Large Pizza",
"Magic Truffle",
"Brain Food Lunch",
"Rock Candy",
"Kraken Soup",
"IQ Capsule",
"Guts Capsule",
"Speed Capsule",
"Vital Capsule",
"Luck Capsule",
"Horn of Life",
"Multi Bottle Rocket",
"Super Bomb",
"Bag of Dragonite",
"Meteotite",
"Repel Superwich",
"Piggy Jelly",
"Spicy Jerky",
"Luxury Jerky",
"Cup of Lifenoodles"
]
self.rare_gear = [
"Combat Yo-yo",
"Sword of Kings",
"Sea Pendant",
"Star Pendant",
"Goddess Ribbon"
]
self.money = [
"$10",
"$100",
"$1000"
]
def generate_early(self) -> None: # Todo: place locked items in generate_early
self.starting_character = self.options.starting_character.current_key.capitalize()
self.locals = []
local_space_count = 0
max_counts = {
"Ness": 12,
"Paula": 11,
"Jeff": 9,
"Poo": 12
}
max_count = max_counts[self.starting_character]
for item_name, amount in itertools.chain(self.options.start_inventory.items(), self.options.start_inventory_from_pool.items()):
if item_name in item_id_table:
local_space_count += amount
if local_space_count > max_count and not self.options.remote_items:
player = self.multiworld.get_player_name(self.player)
raise OptionError(f"{player}: starting inventory cannot place more than {max_count} items into 'Goods' for {self.starting_character}. Attempted to place {local_space_count} Goods items.")
setup_gamevars(self)
create_flavors(self)
initialize_enemies(self)
if not self.options.character_shuffle:
self.options.local_items.value.update(["Paula", "Jeff", "Poo", "Flying Man"])
self.event_count += 6
if self.options.local_teleports:
self.options.local_items.value |= self.item_name_groups["PSI"]
def create_regions(self) -> None:
init_areas(self, get_locations(self))
connect_area_exits(self)
place_static_items(self)
def create_items(self) -> None:
pool = self.get_item_pool(self.get_excluded_items())
self.fill_item_pool(pool)
self.multiworld.itempool += pool
def set_rules(self) -> None:
set_location_rules(self)
self.multiworld.completion_condition[self.player] = lambda state: state.has('Saved Earth', self.player)
def pre_fill(self) -> None:
prefill_locations = []
prefill_items = []
if not self.options.character_shuffle:
main_characters = ["Ness", "Paula", "Jeff", "Poo"]
for character in main_characters:
if character != self.starting_character:
prefill_items.append(self.create_item(character))
prefill_items.extend([
self.create_item("Flying Man"),
self.create_item("Teddy Bear"),
self.create_item("Super Plush Bear")
])
prefill_locations.extend([
self.multiworld.get_location("Happy-Happy Village - Prisoner", self.player),
self.multiworld.get_location("Threed - Zombie Prisoner", self.player),
self.multiworld.get_location("Snow Wood - Bedroom", self.player),
self.multiworld.get_location("Monotoli Building - Monotoli Character", self.player),
self.multiworld.get_location("Dalaam - Throne Character", self.player),
self.multiworld.get_location("Deep Darkness - Barf Character", self.player),
])
self.random.shuffle(prefill_locations)
add_item_rule(self.multiworld.get_location("Happy-Happy Village - Prisoner", self.player), lambda item: item.name in self.item_name_groups["Characters"])
add_item_rule(self.multiworld.get_location("Threed - Zombie Prisoner", self.player), lambda item: item.name in self.item_name_groups["Characters"])
add_item_rule(self.multiworld.get_location("Snow Wood - Bedroom", self.player), lambda item: item.name in self.item_name_groups["Characters"])
add_item_rule(self.multiworld.get_location("Monotoli Building - Monotoli Character", self.player), lambda item: item.name in self.item_name_groups["Characters"])
add_item_rule(self.multiworld.get_location("Dalaam - Throne Character", self.player), lambda item: item.name in self.item_name_groups["Characters"])
add_item_rule(self.multiworld.get_location("Deep Darkness - Barf Character", self.player), lambda item: item.name in self.item_name_groups["Characters"])
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False, collect_pre_fill_items=False), prefill_locations, prefill_items, True, True)
setup_hints(self)
def get_pre_fill_items(self) -> list[Item]:
characters = ["Ness", "Paula", "Jeff", "Poo"]
prefill_items = []
for character in characters:
if character != self.starting_character:
prefill_items.append(self.create_item(f"{character}"))
return prefill_items
@classmethod
def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str) -> None:
try:
multiworld.earthbound_locations_by_sphere = list(multiworld.get_spheres())
except Exception:
raise
finally:
for world in multiworld.get_game_worlds("EarthBound"):
world.get_all_spheres.set()
def generate_output(self, output_directory: str) -> None:
self.has_generated_output = True # Make sure data defined in generate output doesn't get added to spoiler only mode
try:
patch = EBProcPatch(player=self.player, player_name=self.multiworld.player_name[self.player])
patch.write_file("earthbound_basepatch.bsdiff4", pkgutil.get_data(__name__, "src/earthbound_basepatch.bsdiff4"))
patch_rom(self, patch, self.player)
self.rom_name = patch.name
patch.write(os.path.join(output_directory,
f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}"))
except Exception:
raise
finally:
self.rom_name_available_event.set() # make sure threading continues and errors are collected
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
if self.options.dungeon_shuffle:
dungeon_entrances = {}
dungeon_mapping = {}
for dungeon in self.dungeon_connections:
dungeon_entrances[self.dungeon_connections[dungeon]] = dungeon
for dungeon in dungeon_entrances:
for location in self.get_region(dungeon).locations:
if location.address:
dungeon_mapping[location.address] = dungeon_entrances[dungeon]
hint_data[self.player] = dungeon_mapping
def fill_slot_data(self) -> Dict[str, typing.Any]:
return {
"starting_area": self.start_location,
"pizza_logic": self.options.monkey_caves_mode.value,
"free_sancs": self.options.no_free_sanctuaries.value,
"shopsanity": self.options.shop_randomizer.value,
"hint_man_hints": self.hint_man_hints
}
def modify_multidata(self, multidata: dict) -> None:
import base64
# wait for self.rom_name to be available.
self.rom_name_available_event.wait()
rom_name = getattr(self, "rom_name", None)
if rom_name:
new_name = base64.b64encode(bytes(self.rom_name)).decode()
multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
spoiler_handle.write(f"\nStarting Location: {spoiler_starts[self.start_location]}\n")
spoiler_handle.write(f"Franklin Badge Protection: {spoiler_badges[self.franklin_protection]}\n")
if self.options.psi_shuffle:
spoiler_handle.write("\nPSI Shuffle:\n")
spoiler_handle.write(f" Favorite Thing PSI Slot: {spoiler_psi[self.offensive_psi_slots[0]]}\n")
spoiler_handle.write(f" Ness Offensive PSI Middle Slot: {spoiler_psi[self.offensive_psi_slots[1]]}\n")
spoiler_handle.write(f" Paula Offensive PSI Top Slot: {spoiler_psi[self.offensive_psi_slots[2]]}\n")
spoiler_handle.write(f" Paula/Poo Offensive PSI Middle Slot: {spoiler_psi[self.offensive_psi_slots[3]]}\n")
spoiler_handle.write(f" Paula/Poo Offensive PSI Bottom Slot: {spoiler_psi[self.offensive_psi_slots[4]]}\n")
spoiler_handle.write(f" Poo Progressive PSI Slot: {spoiler_psi[self.offensive_psi_slots[5]]}\n")
spoiler_handle.write(f" Ness/Poo Shield Slot: {spoiler_psi[self.shield_slots[0]]}\n")
spoiler_handle.write(f" Paula Shield Slot: {spoiler_psi[self.shield_slots[1]]}\n")
spoiler_handle.write(f" Ness Assist PSI Middle Slot: {spoiler_psi[self.assist_psi_slots[0]]}\n")
spoiler_handle.write(f" Ness Assist PSI Bottom Slot: {spoiler_psi[self.assist_psi_slots[1]]}\n")
spoiler_handle.write(f" Paula Assist PSI Middle Slot: {spoiler_psi[self.assist_psi_slots[2]]}\n")
spoiler_handle.write(f" Paula Assist PSI Bottom Slot: {spoiler_psi[self.assist_psi_slots[3]]}\n")
spoiler_handle.write(f" Poo Assist PSI Slot: {spoiler_psi[self.assist_psi_slots[4]]}\n")
if self.options.psi_shuffle == 2:
spoiler_handle.write(f" Bomb/Bazooka Slot: {spoiler_psi[self.jeff_offense_items[0]]}\n")
spoiler_handle.write(f" Bottle Rocket Slot: {spoiler_psi[self.jeff_offense_items[1]]}\n")
spoiler_handle.write(f" Spray Can Slot: {spoiler_psi[self.jeff_assist_items[0]]}\n")
spoiler_handle.write(f" Multi-Level Gadget Slot 1: {spoiler_psi[self.jeff_assist_items[1]]}\n")
spoiler_handle.write(f" Single-Level Gadget Slot 1: {spoiler_psi[self.jeff_assist_items[2]]}\n")
spoiler_handle.write(f" Single-Level Gadget Slot 2: {spoiler_psi[self.jeff_assist_items[3]]}\n")
spoiler_handle.write(f" Multi-Level Gadget Slot 2: {spoiler_psi[self.jeff_assist_items[4]]}\n")
if self.options.boss_shuffle:
spoiler_handle.write("\nBoss Randomization:\n" +
f" Frank => {self.boss_list[0]}\n" +
f" Frankystein Mark II => {self.boss_list[1]}\n" +
f" Titanic Ant => {self.boss_list[2]}\n" +
f" Captain Strong => {self.boss_list[3]}\n" +
f" Everdred => {self.boss_list[4]}\n" +
f" Mr. Carpainter => {self.boss_list[5]}\n" +
f" Mondo Mole => {self.boss_list[6]}\n" +
f" Boogey Tent => {self.boss_list[7]}\n" +
f" Mini Barf => {self.boss_list[8]}\n" +
f" Master Belch => {self.boss_list[9]}\n" +
f" Trillionage Sprout => {self.boss_list[10]}\n" +
f" Guardian Digger => {self.boss_list[11]}\n" +
f" Dept. Store Spook => {self.boss_list[12]}\n" +
f" Evil Mani-Mani => {self.boss_list[13]}\n" +
f" Clumsy Robot => {self.boss_list[14]}\n" +
f" Shrooom! => {self.boss_list[15]}\n" +
f" Plague Rat of Doom => {self.boss_list[16]}\n" +
f" Thunder and Storm => {self.boss_list[17]}\n" +
f" Kraken => {self.boss_list[18]}\n" +
f" Guardian General => {self.boss_list[19]}\n" +
f" Master Barf => {self.boss_list[20]}\n" +
f" Starman Deluxe => {self.boss_list[21]}\n" +
f" Electro Specter => {self.boss_list[22]}\n" +
f" Carbon Dog => {self.boss_list[23]}\n" +
f" Ness's Nightmare => {self.boss_list[24]}\n" +
f" Heavily Armed Pokey => {self.boss_list[25]}\n" +
f" Starman Junior => {self.boss_list[26]}\n" +
f" Diamond Dog => {self.boss_list[27]}\n" +
f" Giygas (Phase 2) => {self.boss_list[28]}\n")
if self.options.dungeon_shuffle:
spoiler_handle.write("\nDungeon Entrances:\n")
for dungeon in self.dungeon_connections:
spoiler_handle.write(
f" {dungeon} => {self.dungeon_connections[dungeon]}\n"
)
if self.has_generated_output:
spoiler_handle.write("\nArea Levels:\n")
spoiler_excluded_areas = ["Ness's Mind", "Global ATM Access", "Common Condiment Shop"]
for area in self.area_levels:
if area not in spoiler_excluded_areas:
spoiler_handle.write(f" {area}: Level {self.area_levels[area]}\n")
def create_item(self, name: str) -> EBItem:
data = item_table[name]
return EBItem(name, data.classification, data.code, self.player)
def get_filler_item_name(self) -> str: # Todo: make this suck less
weights = {"rare": self.options.rare_filler_weight.value, "uncommon": self.options.uncommon_filler_weight.value, "common": self.options.common_filler_weight.value,
"rare_gear": int(self.options.rare_filler_weight.value * 0.5), "uncommon_gear": int(self.options.uncommon_filler_weight.value * 0.5),
"common_gear": int(self.options.common_filler_weight.value * 0.5), "money": self.options.money_weight.value}
filler_type = self.random.choices(list(weights), weights=list(weights.values()), k=1)[0]
weight_table = {
"common": self.common_items,
"common_gear": self.common_gear,
"uncommon": self.uncommon_items,
"uncommon_gear": self.uncommon_gear,
"rare": self.rare_items,
"rare_gear": self.rare_gear,
"money": self.money
}
return self.random.choice(weight_table[filler_type])
def get_excluded_items(self) -> Set[str]:
excluded_items: Set[str] = set()
excluded_items.add(self.starting_character)
starting_area_to_teleport = ["Onett Teleport", "Onett Teleport", "Twoson Teleport", "Happy-Happy Village Teleport",
"Threed Teleport", "Saturn Valley Teleport", "Fourside Teleport", "Winters Teleport",
"Summers Teleport", "Dalaam Teleport", "Scaraba Teleport", "Deep Darkness Teleport",
"Tenda Village Teleport", "Lost Underworld Teleport", "Magicant Teleport"]
self.starting_area_teleport = starting_area_to_teleport[self.start_location]
excluded_items.add(self.starting_area_teleport)
if self.options.random_start_location:
excluded_items.add(self.starting_teleport)
if self.options.magicant_mode not in [0, 3]:
excluded_items.add("Magicant Teleport")
if not self.options.character_shuffle:
excluded_items.add("Ness")
excluded_items.add("Paula")
excluded_items.add("Jeff")
excluded_items.add("Poo")
excluded_items.add("Flying Man")
if self.options.progressive_weapons:
excluded_items.add("Magicant Bat")
excluded_items.add("Legendary Bat")
excluded_items.add("Pop Gun")
excluded_items.add("Stun Gun")
excluded_items.add("Death Ray")
excluded_items.add("Moon Beam Gun")
if self.options.progressive_armor:
excluded_items.add("Platinum Band")
excluded_items.add("Diamond Band")
excluded_items.add("Pixie's Bracelet")
excluded_items.add("Cherub's Band")
excluded_items.add("Goddess Band")
excluded_items.add("Coin of Slumber")
excluded_items.add("Souvenir Coin")
excluded_items.add("Mr. Saturn Coin")
if not self.options.no_free_sanctuaries:
excluded_items.add("Tiny Key")
excluded_items.add("Tenda Lavapants")
return excluded_items
def set_classifications(self, name: str) -> Item:
data = item_table[name]
item = Item(name, data.classification, data.code, self.player)
if name == "Magicant Teleport" and self.options.magicant_mode == 3:
item.classification = ItemClassification.useful
return item
def fill_item_pool(self, pool: List[Item]) -> None:
item_to_counts = {
"Progressive Bat": self.progressive_filler_bats,
"Progressive Fry Pan": self.progressive_filler_pans,
"Progressive Gun": self.progressive_filler_guns,
"Progressive Bracelet": self.progressive_filler_bracelets,
"Progressive Other": self.progressive_filler_other
}
max_filler_counts = {
"Progressive Bat": 8,
"Progressive Fry Pan": 9,
"Progressive Gun": 6,
"Progressive Bracelet": 6,
"Progressive Other": 10
}
for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(pool) - self.event_count): # Change to fix event count
item = self.set_classifications(self.get_filler_item_name())
if item.name in ["Progressive Bat", "Progressive Fry Pan", "Progressive Other",
"Progressive Gun", "Progressive Bracelet"]:
item_to_counts[item.name] += 1
if item_to_counts[item.name] >= max_filler_counts[item.name]:
self.common_gear = [x for x in self.common_gear if x != item.name]
self.uncommon_gear = [x for x in self.uncommon_gear if x != item.name]
self.rare_gear = [x for x in self.rare_gear if x != item.name]
pool.append(item)
def get_item_pool(self, excluded_items: Set[str]) -> List[Item]:
pool: List[Item] = []
for name, data in item_table.items():
if name not in excluded_items:
for _ in range(data.amount):
item = self.set_classifications(name)
pool.append(item)
if self.options.progressive_weapons:
for i in range(2):
pool.append(self.set_classifications("Progressive Bat"))
for i in range(4):
pool.append(self.set_classifications("Progressive Gun"))
if self.options.progressive_armor:
for i in range(5):
pool.append(self.set_classifications("Progressive Bracelet"))
for i in range(3):
pool.append(self.set_classifications("Progressive Other"))
return pool

View File

@@ -0,0 +1,4 @@
{"game": "EarthBound",
"authors": ["Pink Switch"],
"minimum_ap_version": "0.6.3",
"world_version": "4.3.1"}

View File

@@ -0,0 +1,124 @@
# EarthBound
## Where is the options page?
The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
## What does randomization do to this game?
Randomization of EarthBound randomizes all items, from presents, PSI Locations, Character Locations, and gifts from NPCs.
## What is the goal of EarthBound when randomized?
By default, the goal is to reach the defined number of Sanctuaries, and then defeat Giygas at the Cave of the Past.
If Giygas is disabled, the game will end once the player reaches the required sanctuary number.
If Magicant is set to "required", players will need to defeat Ness's Nightmare in Magicant, unlocked upon reaching their required
Sanctuaries, before they can win.
Optionally, the player can set Alternate Goal conditions, on 1 or 2 sanctuaries more than required, but these will never be the expected
goal.
You can check your goals by speaking to Apple Kid in the Cave of the Present.
## What items and locations get shuffled?
Locations consist of PSI Locations, Character Locations, presents, and gifts from NPCs.
PSI Locations are locations tied to psychic events in the normal game, usually corresponding to the Sanctuaries, PSI Teleport, or PSI Starstorm.
PSI Locations:
- Checking the Mani-Mani Statue in Onett
- Speaking with Buzz Buzz
- Drinking the Coffee at Saturn Valley
- Defeating the Dept. Store Spook in Fourside
- Speaking with the Monkey with Talah Rama
- Eating the Magic Cake in Summers
- Completing Poo's Trial of Mu in Dalaam
- Speaking with the Star Master outside the Pyramid in Scaraba
- Drinking the Tea at Tenda Village
- Speaking with the Talking Rock in the Lost Underworld
- Speaking with the Star Master in the Cave of the Present
- Defeating Ness's Nightmare (Only if the Magicant Mode option is set to Psi location)
Character Locations:
- The cabin just outside Happy-Happy Village
- The graveyard prison in Threed
- The second floor bedroom in the Snow Wood Boarding School
- Monotoli's office in Fourside
- The Throne Room in Dalaam
- Defeating Master Barf in Deep Darkness
## Which items can be in another player's world?
Items you can find for EarthBound include PSI for Poo, characters, teleport destinations, money, and just about any regular or key item.
A few new key items have been added to replace certain plot flags.
## What does another world's item look like in EarthBound?
When collecting an item for another player, the item and player are identified as the receiver. If Shopsanity is enabled, items for other players
in shops will display the player they are for below the shop window. When attempting to purchase an item for another player, the full name of the item and its receiver
will be displayed to confirm. If the 'Presents Match Contents' option is enabled, presents will display the Archipelago symbol if they contain an item for another player.
## What happens when I receive an item?
When you receive an item, the sound effect of an item being received will play, and it will be put in your inventory. If your inventory is full, it will automatically be put in storage.
!If storage is full, you will not be able to receive any more items until you clear room! If you receive PSI, the PSI learn fanfare plays instead. If you receive a character, a cutscene
will play.
## My inventory is full and there's items I can't find!
You can press R at any time on the overworld to access a pocket storage menu that allows you to store, withdraw from storage, and toss unnessecary items within one menu.
## What about Escargo Express?
Escargo Express's item storage has been replaced with a cross-game gift delivery system. From any phone, you can call Escargo Express to receive or send Gift items to other players.
In order for players to be eligible, they have to have gifting enabled. Your gift inbox can hold 69 gifted items at a time, and up to 10 items can be queued to be sent to other players.
## Hints?
In most major towns, you can find a helpful Hint Man who will give hints about your game... for a price.
The location of the Hint Man will be marked on your map, viewable at all times with X.
## Repel Sandwiches?
Skip Sandwiches (And Skip Sandwich DXs) have been replaced with an item called the Repel Sandwich (and Repel Superwich).
Repel Sandwiches will prevent normal enemies from spawning for a short time. The help text can tell you more.
## Is there a run button?
You can hold down the Y button to run.
## I returned to Moonside after beating the Mani-Mani statue and now I can't leave!
Check behind the cafe counter as you would in Fourside. A sparkle indicates the general area.
## How do I get to the undeground prison in Threed?
Use the Bad Key Machine on the door from the outside.
## How do I get from Onett to Twoson?
The captain at the Police Station might be convinced to remove the barricade...
## How do I get rid of the ghosts in the road tunnels?
The ghosts will vanish after Master Belch is defeated, irregardless of anything else.
## How do I get back to Andonuts's Lab after using the Sky Runner?
You can either go back to the underground prison or talk to Bubble Monkey, regardless of if you have helped him before.
## My game automatically rolls credits after getting to Lumine Hall, but I need to get to the Lost Underworld
Once you defeat the boss of Lumine Hall, the Lost Underworld teleport is automatically unlocked. You will need to leave and warp there, though.
## How do I get into the Tenda Camp in the Lost Underworld?
Bring them Tendakraut. They tend to notice it if you walk up to the gate with it already in your inventory.
## I started as Jeff. Can I teleport?
Jeff starts with an item called the "Warp pad", which is an inventory item that functions as Teleport. It can safely be tucked into Key Item storage.
## Community Suggestions
Community suggestions for certain things are being accepted. You can find submissions below.
## Lumine Hall Text Scroll
https://docs.google.com/forms/d/e/1FAIpQLSff7FR0mxtQcZ3d87BoZcRMggBxqGYk2fwmY_ibVnR1sXXQWw/viewform
## Window Flavor Colors
https://docs.google.com/forms/d/e/1FAIpQLSd17Bp4NYGHJqadaYprKaEXFW4EgbUYSPKxGoyufhArb1BbYw/viewform
## Joke Hints
https://docs.google.com/forms/d/e/1FAIpQLScHWK0wHPCmDgHYh8juTrntsyVqEdzYWGCScLdEs21oZCa2mQ/viewform
## Equipment Randomizer
https://docs.google.com/forms/d/e/1FAIpQLSdy-aczL4LmMDirbeD-s5N6u8ae0G2BSrhGfCxOqFhZa_x48A/viewform
## Enemy Randomizer
https://docs.google.com/forms/d/e/1FAIpQLSdF0aFoIwCO7t7OpaObTE_b6dAgLpXC2_8fS-SjdgJDjAQJ6A/viewform

View File

@@ -0,0 +1,128 @@
# EarthBound Archipelago Randomizer Setup Guide
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI such as:
- snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases),
- BizHawk from: [TASVideos](https://tasvideos.org/BizHawk)
- snes9x-nwa from: [snes9x nwa](https://github.com/Skarsnik/snes9x-emunwa/releases)
- RetroArch 1.10.3 or newer from: RetroArch Website
- You can additionally use the FX Pack Pro, however some issues with it have been reported and it is not officially recommended.
- Your legally obtained EarthBound English ROM file, probably named `EarthBound (USA).sfc`
## Optional Software
- EarthBound Archipelago Poptracker Pack
- PopTracker from: [PopTracker Releases Page](https://github.com/black-sliver/PopTracker/releases/)
- EarthBound Archipelago PopTracker pack from: [EarthBound AP Tracker Releases Page](https://github.com/PinkSwitch/earthbound_poptracker/releases/tag/ebpoptracker1.0)
- The EarthBound Sprite Injector Tool (For custom sprite usage): [EarthBound Sprite Injector](https://github.com/PinkSwitch/eb-sprite-injector/releases/)
## Installation Procedures
### Windows Setup
1. Download and install Archipelago from the link above, making sure to install the most recent version.
2. During generation/patching, you will be asked to locate your base ROM file. This is your EarthBound ROM file.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right-click on a ROM file and select **Open with...**
3. Check the box next to **Always use this app to open .sfc files**
4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you
extracted in step one.
## Create a Config (.yaml) File
### What is a config file and why do I need one?
See the guide on setting up a basic YAML at the Archipelago setup
guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a config file?
The Player Options page on the website allows you to configure your personal options and export a config file from
them.
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
validator page: [YAML Validation page](/mysterycheck)
## Joining a MultiWorld Game
### Obtain your patch file and create your ROM
When you join a multiworld game, you will be asked to provide your config file to whomever is hosting. Once that is done,
the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
files. Your patch file should have a `.apeb` extension.
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
client, and will also create your ROM in the same place as your patch file.
### Connect to the client
#### With an emulator
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
##### snes9x-rr
1. Load your ROM file if it hasn't already been loaded.
2. Click on the File menu and hover on **Lua Scripting**
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
##### BizHawk
1. Ensure you have the BSNES core loaded. This is done with the main menubar, under:
- (≤ 2.8) `Config``Cores``SNES``BSNES`
- (≥ 2.9) `Config``Preferred Cores``SNES``BSNESv115+`
2. Load your ROM file if it hasn't already been loaded.
If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R).
3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window.
- Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
- You could instead open the Lua Console manually, click `Script``Open Script`, and navigate to `Connector.lua`
with the file picker.
### Connect to the Archipelago Server
The patch file which launched your client should have automatically connected you to the AP Server. There are a few
reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the
client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it
into the "Server" input field then press enter.
The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
### Play the game
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on
successfully joining a multiworld game!
## Hosting a MultiWorld game
The recommended way to host a game is to use our hosting service. The process is relatively simple:
1. Collect config files from your players.
2. Create a zip file containing your players' config files.
3. Upload that zip file to the Generate page above.
- Generate page: [WebHost Seed Generation Page](/generate)
4. Wait a moment while the seed is generated.
5. When the seed is generated, you will be redirected to a "Seed Info" page.
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so
they may download their patch files from there.
7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
players in the game. Any observers may also be given the link to this page.
8. Once all players have joined, you may begin playing.

View File

View File

@@ -0,0 +1,329 @@
battle_bg_bpp = {
0x00: 2,
0x01: 4,
0x02: 4,
0x03: 4,
0x04: 4,
0x05: 4,
0x06: 4,
0x07: 4,
0x08: 4,
0x09: 4,
0x0a: 4,
0x0b: 4,
0x0c: 4,
0x0d: 4,
0x0e: 4,
0x0f: 4,
0x10: 4,
0x11: 4,
0x12: 4,
0x13: 4,
0x14: 4,
0x15: 4,
0x16: 4,
0x17: 4,
0x18: 4,
0x19: 4,
0x1a: 4,
0x1b: 4,
0x1c: 4,
0x1d: 4,
0x1e: 4,
0x1f: 4,
0x20: 4,
0x21: 4,
0x22: 4,
0x23: 4,
0x24: 4,
0x25: 4,
0x26: 4,
0x27: 4,
0x28: 4,
0x29: 4,
0x2a: 4,
0x2b: 4,
0x2c: 4,
0x2d: 2,
0x2e: 4,
0x2f: 4,
0x30: 4,
0x31: 4,
0x32: 4,
0x33: 4,
0x34: 4,
0x35: 4,
0x36: 4,
0x37: 4,
0x38: 4,
0x39: 4,
0x3a: 4,
0x3b: 4,
0x3c: 4,
0x3d: 4,
0x3e: 4,
0x3f: 4,
0x40: 4,
0x41: 4,
0x42: 4,
0x43: 4,
0x44: 4,
0x45: 4,
0x46: 4,
0x47: 4,
0x48: 4,
0x49: 4,
0x4a: 4,
0x4b: 4,
0x4c: 4,
0x4d: 4,
0x4e: 4,
0x4f: 4,
0x50: 4,
0x51: 4,
0x52: 4,
0x53: 4,
0x54: 4,
0x55: 4,
0x56: 4,
0x57: 4,
0x58: 4,
0x59: 4,
0x5a: 4,
0x5b: 4,
0x5c: 4,
0x5d: 4,
0x5e: 4,
0x5f: 4,
0x60: 4,
0x61: 4,
0x62: 4,
0x63: 4,
0x64: 4,
0x65: 4,
0x66: 4,
0x67: 4,
0x68: 4,
0x69: 4,
0x6a: 4,
0x6b: 4,
0x6c: 4,
0x6d: 4,
0x6e: 4,
0x6f: 4,
0x70: 4,
0x71: 4,
0x72: 4,
0x73: 4,
0x74: 4,
0x75: 4,
0x76: 4,
0x77: 4,
0x78: 4,
0x79: 4,
0x7a: 4,
0x7b: 4,
0x7c: 4,
0x7d: 4,
0x7e: 4,
0x7f: 4,
0x80: 4,
0x81: 4,
0x82: 4,
0x83: 4,
0x84: 4,
0x85: 4,
0x86: 4,
0x87: 4,
0x88: 4,
0x89: 4,
0x8a: 4,
0x8b: 4,
0x8c: 4,
0x8d: 4,
0x8e: 4,
0x8f: 4,
0x90: 4,
0x91: 4,
0x92: 4,
0x93: 4,
0x94: 4,
0x95: 4,
0x96: 4,
0x97: 4,
0x98: 4,
0x99: 4,
0x9a: 4,
0x9b: 4,
0x9c: 4,
0x9d: 4,
0x9e: 4,
0x9f: 4,
0xa0: 4,
0xa1: 4,
0xa2: 4,
0xa3: 2,
0xa4: 2,
0xa5: 2,
0xa6: 2,
0xa7: 2,
0xa8: 2,
0xa9: 2,
0xaa: 2,
0xab: 2,
0xac: 2,
0xad: 2,
0xae: 2,
0xaf: 2,
0xb0: 2,
0xb1: 2,
0xb2: 2,
0xb3: 2,
0xb4: 2,
0xb5: 2,
0xb6: 2,
0xb7: 2,
0xb8: 2,
0xb9: 2,
0xba: 2,
0xbb: 2,
0xbc: 2,
0xbd: 4,
0xbe: 2,
0xbf: 2,
0xc0: 2,
0xc1: 2,
0xc2: 2,
0xc3: 4,
0xc4: 4,
0xc5: 4,
0xc6: 4,
0xc7: 4,
0xc8: 2,
0xc9: 2,
0xca: 2,
0xcb: 2,
0xcc: 2,
0xcd: 2,
0xce: 2,
0xcf: 2,
0xd0: 4,
0xd1: 4,
0xd2: 2,
0xd3: 2,
0xd4: 2,
0xd5: 2,
0xd6: 2,
0xd7: 2,
0xd8: 2,
0xd9: 2,
0xda: 2,
0xdb: 2,
0xdc: 4,
0xdd: 2,
0xde: 2,
0xdf: 2,
0xe0: 2,
0xe1: 4,
0xe2: 4,
0xe3: 4,
0xe4: 4,
0xe5: 4,
0xe6: 4,
0xe7: 4,
0xe8: 4,
0xe9: 4,
0xea: 4,
0xeb: 4,
0xec: 4,
0xed: 4,
0xee: 4,
0xef: 4,
0xf0: 4,
0xf1: 4,
0xf2: 4,
0xf3: 4,
0xf4: 4,
0xf5: 4,
0xf6: 4,
0xf7: 4,
0xf8: 2,
0xf9: 4,
0xfa: 4,
0xfb: 4,
0xfc: 2,
0xfd: 4,
0xfe: 4,
0xff: 4,
0x100: 4,
0x101: 4,
0x102: 2,
0x103: 4,
0x104: 4,
0x105: 4,
0x106: 4,
0x107: 2,
0x108: 2,
0x109: 4,
0x10a: 2,
0x10b: 2,
0x10c: 4,
0x10d: 2,
0x10e: 2,
0x10f: 4,
0x110: 2,
0x111: 2,
0x112: 2,
0x113: 2,
0x114: 2,
0x115: 2,
0x116: 4,
0x117: 4,
0x118: 4,
0x119: 2,
0x11a: 2,
0x11b: 4,
0x11c: 4,
0x11d: 4,
0x11e: 4,
0x11f: 4,
0x120: 4,
0x121: 4,
0x122: 4,
0x123: 4,
0x124: 2,
0x125: 2,
0x126: 4,
0x127: 4,
0x128: 4,
0x129: 4,
0x12a: 4,
0x12b: 4,
0x12c: 4,
0x12d: 4,
0x12e: 4,
0x12f: 4,
0x130: 4,
0x131: 4,
0x132: 2,
0x133: 2,
0x134: 4,
0x135: 4,
0x136: 4,
0x137: 4,
0x138: 4,
0x139: 4,
0x13a: 4,
0x13b: 4,
0x13c: 4,
0x13d: 4,
0x13e: 4,
0x13f: 4,
0x140: 4,
0x141: 4,
0x142: 4,
0x143: 4,
0x144: 4,
0x145: 4,
0x146: 2
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,297 @@
region_text = {
"Northern Onett": "Onett",
"Onett": "Onett",
"Giant Step": "Giant Step",
"Twoson": "Twoson",
"Peaceful Rest Valley": "Peaceful Rest Valley",
"Happy-Happy Village": "Happy-Happy Village",
"Lilliput Steps": "Lilliput Steps",
"Threed": "Threed",
"Belch's Factory": "Belch's factory",
"Saturn Valley": "Saturn Valley",
"Upper Saturn Valley": "Saturn Valley",
"Milky Well": "Milky Well",
"Dusty Dunes Desert": "the Dusty Dunes Desert",
"Gold Mine": "the gold mine",
"Monkey Caves": "the monkey caves",
"Fourside": "Fourside",
"Magnet Hill": "Fourside's sewers",
"Monotoli Building": "the Monotoli Building",
"Winters": "Winters",
"Snow Wood Boarding School": "the Snow Wood Boarding School",
"Southern Winters": "Winters",
"Rainy Circle": "Rainy Circle",
"Stonehenge Base": "Stonehenge",
"Summers": "Summers",
"Dalaam": "Dalaam",
"Pink Cloud": "Pink Cloud",
"Scaraba": "Scaraba",
"Pyramid": "the Pyramid",
"Southern Scaraba": "Scaraba",
"Dungeon Man": "Dungeon Man",
"Deep Darkness": "the Deep Darkness",
"Tenda Village": "Tenda Village",
"Lumine Hall": "Lumine Hall",
"Lost Underworld": "the Lost Underworld",
"Fire Spring": "Fire Spring",
"Magicant": "Magicant",
"Cave of the Present": "Cave of the Past",
"Cave of the Past": "Cave of the Past"
}
barf_text = {
"Paula": [0xE0, 0xC2, 0xEE, 0xBD, 0xC3, 0xEE],
"Jeff": [0xFA, 0xC2, 0xEE, 0x10, 0xC4, 0xEE],
"Poo": [0x1A, 0x74, 0xEF, 0x3E, 0xC4, 0xEE],
"Flying Man": [0x17, 0xC3, 0xEE, 0x95, 0xC4, 0xEE],
"Ness": [0x16, 0x07, 0xF3, 0x2B, 0x07, 0xF3],
"Photograph": [0x2F, 0x99, 0xC9, 0x2F, 0x99, 0xC9]
}
eb_text_table = {" ": ([0x50]), "!": ([0x51]), '"': ([0x52]), "": ([0x53]), "$": ([0x54]), "%": ([0x55]),
"°": ([0x56]), "'": ([0x57]), "(": ([0x58]), ")": ([0x59]), "*": ([0x5A]), "+": ([0x5B]),
",": ([0x5C]), "-": ([0x5D]), ".": ([0x5E]), "/": ([0x5F]), "0": ([0x60]), "1": ([0x61]),
"2": ([0x62]), "3": ([0x63]), "4": ([0x64]), "5": ([0x65]), "6": ([0x66]), "7": ([0x67]),
"8": ([0x68]), "9": ([0x69]), ":": ([0x6A]), ";": ([0x6B]), "": ([0x6C]), "=": ([0x6D]),
"": ([0x6E]), "?": ([0x6F]), "@": ([0x70]), "A": ([0x71]), "B": ([0x72]), "C": ([0x73]),
"D": ([0x74]), "E": ([0x75]), "F": ([0x76]), "G": ([0x77]), "H": ([0x78]), "I": ([0x79]),
"J": ([0x7A]), "K": ([0x7B]), "L": ([0x7C]), "M": ([0x7D]), "N": ([0x7E]), "O": ([0x7F]),
"P": ([0x80]), "Q": ([0x81]), "R": ([0x82]), "S": ([0x83]), "T": ([0x84]), "U": ([0x85]),
"V": ([0x86]), "W": ([0x87]), "X": ([0x88]), "Y": ([0x89]), "Z": ([0x8A]), "α": ([0x8B]),
"β": ([0x8C]), "γ": ([0x8D]), "Σ": ([0x8E]), "Ω": ([0x8F]), "a": ([0x91]), "b": ([0x92]),
"c": ([0x93]), "d": ([0x94]), "e": ([0x95]), "f": ([0x96]), "g": ([0x97]), "h": ([0x98]),
"i": ([0x99]), "j": ([0x9A]), "k": ([0x9B]), "l": ([0x9C]), "m": ([0x9D]), "n": ([0x9E]),
"o": ([0x9F]), "p": ([0xA0]), "q": ([0xA1]), "r": ([0xA2]), "s": ([0xA3]), "t": ([0xA4]),
"u": ([0xA5]), "v": ([0xA6]), "w": ([0xA7]), "x": ([0xA8]), "y": ([0xA9]), "z": ([0xAA]),
"[": ([0xAB]), "": ([0xAC]), "]": ([0xAD]), "~": ([0xAE]), "": ([0xAF]), "_": ([0xB0]),
"<": ([0xB1]), "": ([0x90]), "&": ([0xB6]), "#": ([0xB7]), "\n": ([0x03, 0x00]), "ñ": ([0xB9]),
"ä": ([0xBA]), "é": ([0xBB])}
# 0x90 is a half-width space
pixel_width_table = {" ": 2, "!": 2, '"': 3, "": 2, "$": 5, "%": 9,
"°": 7, "'": 2, "(": 3, ")": 3, "*": 3, "+": 5,
",": 2, "-": 2, ".": 2, "/": 4, "0": 5, "1": 5,
"2": 5, "3": 5, "4": 5, "5": 5, "6": 5, "7": 5,
"8": 5, "9": 5, ":": 2, ";": 3, "": 3, "=": 5,
"": 3, "?": 4, "@": 5, "A": 6, "B": 5, "C": 5,
"D": 5, "E": 4, "F": 4, "G": 5, "H": 5, "I": 1,
"J": 4, "K": 5, "L": 4, "M": 7, "N": 5, "O": 5,
"P": 5, "Q": 5, "R": 5, "S": 5, "T": 5, "U": 5,
"V": 6, "W": 7, "X": 5, "Y": 5, "Z": 4, "α": 5,
"β": 4, "γ": 6, "Σ": 4, "Ω": 5, "a": 4, "b": 4,
"c": 4, "d": 4, "e": 4, "f": 3, "g": 4, "h": 4,
"i": 1, "j": 2, "k": 4, "l": 1, "m": 7, "n": 4,
"o": 4, "p": 4, "q": 4, "r": 3, "s": 4, "t": 3,
"u": 4, "v": 5, "w": 7, "x": 4, "y": 4, "z": 4,
"[": 2, "": 5, "]": 2, "~": 6, "": 7, "_": 5,
"<": 4, "": 1, "&": 6, "#": 6, "ñ": 6, "ä": 5,
"é": 5}
lumine_hall_text = [
"You paid 350 bucks for this seed",
"You could be playing Mario is Missing!",
"youtu.be/dQw4w9WgXcQ",
"Please help I am in BK mode",
"But anyways, its not about the glitch, its not about that its a video game, its because you don't care about how this is affecting other people, your heartless, you can apparently live with yourself with this.",
"Do you still remember your pin? It's the price of a cheese pizza and and a large soda back where I used to work; Panucci's Pizza.",
"Did you remember to clear your sphere 1?",
"You should use a tracker, personally I recommend Poptracker",
"You should try Mario Is Missing AP, its fun and funny.",
"Despite everything, you're still you.",
"Bababooey",
"Reticulating Splines",
"You only have one shot.",
"human.. i remember you're genocides.",
'"May your woes be many, and your days few."',
"For you, the day Pokey graced your village was the most important day of your life. But for me, it was Tuesday.",
"To shreds you say?",
"Hey player, I'm in BK, can you get my item at Starman Super Drop Sword of Kings? It's my go mode.",
"Why did you make me do this? You're fighting so you can watch everyone around you die! Think, Player Name! You'll outlast every fragile, insignificant being on this planet.",
"Is this thing on? Testing. Oh hey, its me, Tony! Is Jeff doing okay? I worry about him a lot, and he's like, on my mind a lot. Maybe stop by Winters sometime! Pretty please?",
"If you have your club card, please scan it now.",
"Come on Reggie, give us MOTHER 3!",
"Growing up, Man I spent hours of my life stomping................................................................................................................................................ Koopas.",
"We've been trying to reach you about your car's extended warranty...",
"YOU MUST CONSTRUCT ADDITIONAL PYLONS!!!",
"It all start on the day the Happy Happyists attacked...",
"You just lost the game.",
"ERROR 404: HALL TEXT NOT FOUND.",
"Long ago, two races ruled over Earth: HUMANS and MONSTERS. One day, war broke out between the two races. After a long battle, the humans were victorious. They sealed the monsters underground with a magic spell.",
"Once upon a time, a LEGEND was whispered among shadows. It was a LEGEND of HOPE. It was a LEGEND of DREAMS. It was a LEGEND of LIGHT. It was a LEGEND of DARK.",
"OwO? What's This?",
"What about now it's time to rock with the Bickedy Buck Buzz Buzz! What about now it's time to rock with the Bickedy Buck Buzz Buzz! Bum to the bum to the bum to the bass to the bum to the boom to the Buzz Buzz!",
"HOW ARE YOU GENTLEMEN !! ALL YOUR BASE ARE BELONG TO US.",
"You're just the delivery boy. Sorry, your parts over now. Here, go play hero with this.",
"My friends are my power, and I'm theirs!",
"Did somebody mention the Door to Darkness?",
"We hope you'll be a great asset to the company. Great asset to the company. Asset-Great-Great-Asset to the Company.",
"So thanks, goat-bunny-cat lady.",
"It's dangerous to go alone, take this!",
"The rabbit virus got so happy that it exploded!!",
"Steve tried to swim in lava.",
"The flow of time is always cruel... its speed seems different for each person, but no one can change it... A thing that does not change with time is a memory of younger days...",
"Hah! You better have BURN HEAL!",
"..and so she left, heart still racing.",
"I've found you faker! Faker? I think you're the fake hedgehog around here. In fact, you're not even good enough be my fake. I'll make you eat those words!",
"Don't ask about Mother 3 AP.",
"Hello. This is Punk-Sure. I've been trying to reach you about your bicycle's extended warranty.",
"Who's that kid with blue hair whose name sounds like evil? Who suspiciously showed up to say we'd die like wretches? Surely HE'S not the bad guy, right? Nah, not when he has such a cool violin. Perish the thought!",
"I wonder if xfisjmg1 is going to release another Ninten Speaks video anytime soon?",
"weedle",
"hi",
"Look Ma, I'm in Lumine Hall!",
"Did you check Aginah?",
"Eat at Joe's",
"FOR A GOOD TIME, CALL 555-...",
"!hint Moon Pearl",
"'); DROP TABLE Lumine Hall Text;--",
"No, no! We can't! This baby is a demon child!",
"Rhydon Earthquake vs. Mr. Mime: 169-199 (59.7 - 70.3%) -- guaranteed 2HKO",
"Dakota",
"Ness, I am your father.",
"Hello, it's your dad. You've been out there for a long time now... It may be none of my business, but don't you think it would be a good idea if you took a break?",
"RIP AND TEAR",
"Sorry Ness, Pokey died of ligma",
"A horrible chill goes down your spine...",
"You feel an evil presence watching you...",
"Going to BK, BRB.",
"My favorite Mr. Saturn... It... cute... lovely... smart... plus... amazing... you think so? Hug it... when... sleeping... warm and cuddly... spectacular... ravishing... Oops! Look at the time! I kept you too long!",
"THE CLUMSY ROBOT IN NAME ONLY, FOR IT IS NEITHER.",
"The Arcana is the means by which all is revealed.",
"Say the password! ",
"Hello? Is there anybody in there? Just nod if you can hear me. Is there anyone home?",
"I bought my sneakers from a drug dealer in Fourside. I don't know what they were laced with but I've been tripping all day.",
"Sans Is Ness",
"Your Goal This Game: Join Happy Happyism",
"There's only junk items that way.",
"GET OUT WHILE YOU STILL CAN",
"Why isn't my SNI connecting?",
"It's 10 p.m. Do you know where your kids are?",
"If they censored what the adults were drinking to coffee, then what did Mr. Saturn give Ness originally?",
"Fourside Frappuccino. Work can wait.",
"Let's do an Archibald seed !",
"WARNING! Scheduled Maintenance in 4 Minutes...",
"I'm still stuck in BK",
"John Madden! John Madden! John Madden! John Madden! John Madden! John Madden! John Madden! ",
"Did you know that by reading this message you are wasting time not getting checks?",
"Your text here! Check the game's design thread on the Archipelago Server for how to submit!",
"WITNESS JOKE HERE",
"Also try The Witness!",
"Also try Adventure!",
"Also try DOOM 1993!",
"Also try Super Mario World!",
"Also try Yoshi's Island!",
"Also try Castlevania 64!",
"Hi Mom! I'm on AP!",
"Looking for hot multiworlds in your area?",
"Paleontologists do it in the dirt.",
"Wait... something seems off... Things seem more random than usual...",
"Shoutouts to SimpleFlips.",
"How long have you been BK?",
"Also try PK Scramble!",
"Also try Ancient Cave!",
"I am playing this on AP version 1.0 and it is amazing.",
"WHAT A HORRIBLE NIGHT TO HAVE A CURSE.",
"It's so jover",
"The National Weather Service has issued a severe thunderstorm warning for the following areas, starting at 7:49 PM Eastern Daylight Time and ending at 7:49 PM Eastern Daylight Time.",
"Where is everyone? I've done most of my checks and I still can't find my friends, it's so lonely here.",
"Can you hurry up and go to Ness's Nightmare it has my Morph Ball for my Super Metroid seed Thanks in advance Berserker",
"gottem",
"Dear Ness, please come to the house. I've baked a steak for you. Yours truly, mom.",
"Phar why",
"Tell Figment to check POD!",
"Congratulations, you're the Lumine Hall's 100th visitor! Click here for $2 off your next order at the Onett Burger Shop!",
"Well, that's never happened before...",
"99 bottles of DXWater on the wall, 99 bottles of DXWater. Take one down, pass it around, 98 bottles of DXWater on the wall!",
"After all these years. Finally, I have them all.",
"If there were two guys on the moon and one killed the other with a rock would that be screwed up or what",
"Help, I'm trapped in a Lumine Hall factory!",
"They say that Ice Palace can be found at Ice Palace...",
"Hey Mom, I'm in AP!",
"It says “gullible” on the wall",
"64 bits! 32 bits! 16 bits! 8 bits! 4 bits! 2 bits! 1 bit! Half bit! Quarter bit! THE WRIST GAME!!!",
"Where did YOU learn to teleport?",
"My statistics show about 70% of kids these days don't stop to read bulletin boards anymore. Well, I had to shell out a lot of money for this, but let's see you ignore THIS one, suckers! ...Brick Road",
"I'm Ness... I could go for a burger right about now... That girl from Twoson sure is cute... I wonder if I should ask her out on a date... W...wha...? What's...? H-HEY! These thoughts are PRIVATE!",
"Chris, when is Parasite Eve in AP?",
"Praise the Fool!",
"Am I the only one who likes Sonic Unleashed?"
]
spoiler_psi = {
"Special": "PSI Rockin",
"Fire": "PSI Fire",
"Freeze": "PSI Freeze",
"Thunder": "PSI Thunder",
"Flash": "PSI Flash",
"Starstorm": "PSI Starstorm",
"Shield": "Shield",
"PSI Shield": "PSI Shield",
"Offense Up": "Offense up",
"Defense Down": "Defense down",
"Hypnosis": "Hypnosis",
"Paralysis": "Paralysis",
"Brainshock": "Brainshock",
"Blast": "PSI Blast",
"Missile": "PSI Missile",
"Stop": "Stop",
"Drain": "Drain",
"Defense up": "Defense up",
"Neutralize": "Neutralize",
"Disable": "Disable"
}
spoiler_starts = [
"Ness's House",
"Onett Downtown",
"Twoson",
"Happy-Happy Village",
"Threed",
"Saturn Valley",
"Fourside",
"Winters",
"Summers",
"Dalaam",
"Scaraba",
"Deep Darkness",
"Tenda Village",
"Lost Underworld",
"Magicant"
]
spoiler_badges = {
"special": "Rockin",
"fire": "Fire",
"freeze": "Freeze",
"flash": "Flash",
"starstorm": "Starstorm",
"explosive": "Bomb",
"thunder": "Thunder",
}
def text_encoder(text: str, textcap: int) -> bytearray:
"""Return an encoded bytearray of in-game text from a string. Unknown characters will be replaced with a ?.
textcap is the maximum allowed length of the text."""
encoded_text = bytearray()
for char in text[:textcap]:
if char in eb_text_table:
encoded_text.extend(eb_text_table[char])
else:
encoded_text.extend([0x6F])
return encoded_text
def calc_pixel_width(text: str) -> int:
"""Return the in-game width of a string. EarthBound uses a VWF, and some text strings
need to be shortened to fit within in-game menus."""
width = 0
for char in text:
if char in pixel_width_table:
width += pixel_width_table[char]
else:
width += 4
width += 1
width -= 1
return width

View File

View File

@@ -0,0 +1,606 @@
from dataclasses import dataclass
from typing import List
gift_qualities = {
"Super Plush Bear": {"MeatShield": 3},
"Cracked Bat": {"Weapon": 0.07},
"Tee Ball Bat": {"Weapon": 0.14},
"Sand Lot Bat": {"Weapon": 0.27},
"Minor League Bat": {"Weapon": 0.48},
"Mr. Baseball Bat": {"Weapon": 0.70},
"Big League Bat": {"Weapon": 0.90},
"Hall of Fame Bat": {"Weapon": 1.14},
"Magicant Bat": {"Weapon": 1.48},
"Legendary Bat": {"Weapon": 2.03},
"Gutsy Bat": {"Weapon": 4.0},
"Casey Bat": {"Weapon": 0.1},
"T-Rex's Bat": {"Weapon": 0.88},
"Ultimate Bat": {"Weapon": 1.25},
"Fry Pan": {"Weapon": 0.24},
"Thick Fry Pan": {"Weapon": 0.48},
"Deluxe Fry Pan": {"Weapon": 0.73},
"Chef's Fry Pan": {"Weapon": 0.97},
"Non-Stick Frypan": {"Weapon": 1.19},
"French Fry Pan": {"Weapon": 1.46},
"Holy Fry Pan": {"Weapon": 1.90},
"Magic Frypan": {"Weapon": 2.5},
"Pop Gun": {"Weapon": 0.25},
"Stun Gun": {"Weapon": 0.34},
"Toy Air Gun": {"Weapon": 0.50},
"Magnum Air Gun": {"Weapon": 0.57},
"Zip Gun": {"Weapon": 0.63},
"Laser Gun": {"Weapon": 0.76},
"Hyper Beam": {"Weapon": 0.92},
"Double Beam": {"Weapon": 1.04},
"Crusher Beam": {"Weapon": 1.14},
"Spectrum Beam": {"Weapon": 1.24},
"Death Ray": {"Weapon": 1.42},
"Baddest Beam": {"Weapon": 1.55},
"Moon Beam Gun": {"Weapon": 1.74},
"Gaia Beam": {"Weapon": 1.98},
"Sword of Kings": {"Weapon": 2.00},
"Bag of Fries": {"Heal": 0.24},
"Banana": {"Heal": 0.22},
"Bean Croquette": {"Heal": 0.42},
"Beef Jerky": {"Heal": 1.50},
"Boiled Egg": {"Heal": 0.42},
"Bowl of Rice Gruel": {"Heal": 2.16},
"Brain Food Lunch": {"Heal": 3.00, "Mana": 1.42},
"Bread Roll": {"Heal": 0.30},
"Calorie Stick": {"Heal": 0.60},
"Can of Fruit Juice": {"Heal": 0.06},
"Chef's Special": {"Heal": 2.16},
"Cookie": {"Heal": 0.06},
"Croissant": {"Heal": 0.60},
"Cup of Coffee": {"Heal": 0.12},
"Cup of Noodles": {"Heal": 0.42},
"Double Burger": {"Heal": 0.96},
"Fresh Egg": {"Heal": 0.84},
"Hamburger": {"Heal": 0.48},
"Kabob": {"Heal": 1.26},
"Kraken Soup": {"Heal": 9.99},
"Large Pizza": {"Heal": 2.40},
"Lucky Sandwich": {"Heal": 0.60, "Mana": 0.57},
"Luxury Jerky": {"Heal": 3.00},
"Mammoth Burger": {"Heal": 2.05},
"Molokheiya Soup": {"Heal": 0.84},
"Pasta di Summers": {"Heal": 1.10},
"Peanut Cheese Bar": {"Heal": 1.00},
"Picnic Lunch": {"Heal": 0.80},
"Piggy Jelly": {"Heal": 3.00},
"Pizza": {"Heal": 1.20},
"Plain Roll": {"Heal": 0.80},
"Plain Yogurt": {"Heal": 1.60},
"Popsicle": {"Heal": 0.18},
"Protein Drink": {"Heal": 0.80},
"Royal Iced Tea": {"Heal": 0.60},
"Repel Sandwich": {"Heal": 0.06},
"Repel Superwich": {"Heal": 0.06},
"Spicy Jerky": {"Heal": 2.52},
"Trout Yogurt": {"Heal": 0.30},
"Hand-Aid": {"Heal": 9.99},
"Cold Remedy": {"Cure": 0.10},
"Vial of Serum": {"Cure": 0.25},
"Wet Towel": {"Cure": 0.50},
"Refreshing Herb": {"Cure": 1.00},
"Secret Herb": {"Cure": 2.00, "Life": 0.50},
"Horn of Life": {"Cure": 3.00, "Life": 1.00},
"Cup of Lifenoodles": {"Cure": 3.00, "Life": 1.00},
"Bottle of Water": {"Mana": 0.28},
"Bottle of DXwater": {"Mana": 1.14},
"Magic Pudding": {"Mana": 1.14},
"Magic Tart": {"Mana": 0.57},
"Magic Truffle": {"Mana": 2.28},
"PSI Caramel": {"Mana": 0.57},
"Cheap Bracelet": {"Armor": 0.12},
"Copper Bracelet": {"Armor": 0.25},
"Silver Bracelet": {"Armor": 0.37},
"Gold Bracelet": {"Armor": 0.75},
"Platinum Band": {"Armor": 1.00},
"Diamond Band": {"Armor": 1.25},
"Pixie's Bracelet": {"Armor": 1.50},
"Cherub's Band": {"Armor": 1.75},
"Goddess Band": {"Armor": 2.00},
"Bracer of Kings": {"Armor": 0.87, "Fire": 0.50},
"Ribbon": {"Armor": 0.35},
"Red Ribbon": {"Armor": 0.43},
"Defense Ribbon": {"Armor": 0.70},
"Talisman Ribbon": {"Armor": 1.05},
"Saturn Ribbon": {"Armor": 1.57},
"Goddess Ribbon": {"Armor": 1.92},
"Yo-yo": {"Weapon": 0.06},
"Slingshot": {"Weapon": 0.12},
"Bionic Slingshot": {"Weapon": 0.32},
"Trick Yo-yo": {"Weapon": 0.46},
"Combat Yo-yo": {"Weapon": 0.54},
"Travel Charm": {"AntiNumb": 0.25},
"Great Charm": {"Armor": 0.01, "AntiNumb": 0.50},
"Crystal Charm": {"Armor": 0.02, "AntiNumb": 1.00},
"Rabbit's Foot": {"Armor": 0.03, "AntiNumb": 1.00, "Speed": 2.00},
"Flame Pendant": {"Armor": 1.00, "Fire": 1.00},
"Rain Pendant": {"Armor": 1.00, "Ice": 1.00},
"Night Pendant": {"Armor": 1.00, "Light": 1.00},
"Sea Pendant": {"Armor": 2.00, "Fire": 1.00, "Ice": 1.00, "Light": 1.00},
"Star Pendant": {"Armor": 3.00, "Fire": 1.00, "Ice": 1.00, "Light": 1.00, "AntiNumb": 1.00},
"Earth Pendant": {"Armor": 1.50, "Fire": 0.50, "Ice": 0.50, "Light": 0.50},
"Cloak of Kings": {"Armor": 0.50},
"Shield Killer": {"Neutralizing": 0.50},
"Neutralizer": {"Neutralizing": 1.00},
"HP-Sucker": {"Draining": 0.50},
"Hungry HP-Sucker": {"Draining": 1.00},
"Baseball Cap": {"Armor": 0.13},
"Holmes Hat": {"Armor": 0.27},
"Mr. Baseball Cap": {"Armor": 0.16},
"Hard Hat": {"Armor": 0.41},
"Coin of Slumber": {"Armor": 0.83},
"Coin of Defense": {"Armor": 1.11},
"Lucky Coin": {"Armor": 1.38},
"Talisman Coin": {"Armor": 1.66},
"Shiny Coin": {"Armor": 1.94},
"Souvenir Coin": {"Armor": 2.22},
"Coin of Silence": {"Armor": 1.25},
"Mr. Saturn Coin": {"Armor": 1.30},
"Diadem of Kngs": {"Armor": 0.50, "Fire": 0.25, "Ice": 0.25, "Light": 0.25, "AntiNumb": 0.25}
}
@dataclass
class EarthBoundGift:
name: str
value: int
traits: list[str]
def make_trait(trait: str, name: str) -> dict[str, str | int]:
if name in gift_qualities and trait in gift_qualities[name]:
quality = gift_qualities[name][trait]
else:
quality = None
if quality:
return {"trait": trait, "quality": quality}
else:
return {"trait": trait}
def make_default_traits(traits: list[str], name: str) -> list[dict[str, str | int]]:
return [make_trait(trait, name) for trait in traits]
def create_gift(name: str, value: int, traits: list[str]) -> EarthBoundGift:
"""Create a Gift with the specified tag and values."""
return EarthBoundGift(name, value, make_default_traits(traits, name))
gift_properties = {
2: create_gift("Teddy Bear", 178, ["Toy", "Doll"]),
3: create_gift("Super Plush Bear", 1198, ["Toy", "Doll"]),
4: create_gift("Broken Machine", 0, ["Broken", "Machine", "Metal", "Material", "Resource", "Trash"]),
5: create_gift("Broken Gadget", 109, ["Broken", "Machine", "Metal", "Material", "Resource", "Trash"]),
6: create_gift("Broken Air Gun", 0, ["Broken", "Machine", "Metal", "Material", "Resource", "Trash"]),
7: create_gift("Broken Spray Can", 189, ["Broken", "Machine", "Metal", "Material", "Resource", "Trash"]),
8: create_gift("Broken Laser", 0, ["Broken", "Machine", "Metal", "Material", "Resource", "Trash"]),
9: create_gift("Broken Iron", 149, ["Broken", "Machine", "Metal", "Material", "Resource", "Trash"]),
10: create_gift("Broken Pipe", 149, ["Broken", "Machine", "Metal", "Material", "Resource", "Pipe", "Trash"]),
11: create_gift("Broken Cannon", 218, ["Broken", "Machine", "Metal", "Material", "Resource", "Trash"]),
12: create_gift("Broken Tube", 0, ["Broken", "Machine", "Metal", "Material", "Resource", "Trash"]),
13: create_gift("Broken Bazooka", 0, ["Broken", "Machine", "Metal", "Material", "Resource", "Weapon", "Trash"]),
14: create_gift("Broken Trumpet", 0, ["Broken", "Machine", "Metal", "Material", "Resource", "Instrument",
"Trash"]),
15: create_gift("Broken Harmonica", 0, ["Broken", "Machine", "Metal", "Material", "Resource", "Instrument",
"Trash"]),
16: create_gift("Broken Antenna", 0, ["Broken", "Machine", "Metal", "Material", "Resource", "Trash"]),
17: create_gift("Cracked Bat", 18, ["MeleeWeapon", "Wood", "Baseball", "Toy", "Weapon"]),
18: create_gift("Tee Ball Bat", 48, ["MeleeWeapon", "Metal", "Toy", "Weapon"]),
19: create_gift("Sand Lot Bat", 98, ["MeleeWeapon", "Wood", "Baseball", "Toy", "Weapon"]),
20: create_gift("Minor League Bat", 399, ["MeleeWeapon", "Wood", "Baseball", "Toy", "Weapon"]),
21: create_gift("Mr. Baseball Bat", 498, ["MeleeWeapon", "Wood", "Baseball", "Toy", "Weapon"]),
22: create_gift("Big League Bat", 3080, ["MeleeWeapon", "Wood", "Baseball", "Toy", "Weapon"]),
23: create_gift("Hall of Fame Bat", 1880, ["MeleeWeapon", "Wood", "Baseball", "Toy", "Weapon"]),
24: create_gift("Magicant Bat", 0, ["MeleeWeapon", "Wood", "Baseball", "Toy", "Weapon", "Dreamlike"]),
25: create_gift("Legendary Bat", 0, ["MeleeWeapon", "Wood", "Baseball", "Toy", "Weapon", "Legendary"]),
26: create_gift("Gutsy Bat", 0, ["MeleeWeapon", "Wood", "Baseball", "Toy", "Weapon", "Guts"]),
27: create_gift("Casey Bat", 0, ["MeleeWeapon", "Wood", "Baseball", "Toy", "Weapon"]),
28: create_gift("Fry Pan", 0, ["MeleeWeapon", "Metal", "Tool", "Weapon"]),
29: create_gift("Thick Fry Pan", 0, ["MeleeWeapon", "Metal", "Tool", "Weapon"]),
30: create_gift("Deluxe Fry Pan", 0, ["MeleeWeapon", "Metal", "Tool", "Weapon"]),
31: create_gift("Chef's Fry Pan", 0, ["MeleeWeapon", "Metal", "Tool", "Weapon"]),
32: create_gift("French Fry Pan", 0, ["MeleeWeapon", "Metal", "Tool", "Weapon"]),
33: create_gift("Magic Fry Pan", 0, ["MeleeWeapon", "Metal", "Tool", "Weapon"]),
34: create_gift("Holy Fry Pan", 0, ["MeleeWeapon", "Metal", "Tool", "Weapon"]),
35: create_gift("Sword of Kings", 0, ["Weapon"]),
36: create_gift("Pop Gun", 0, ["RangedWeapon", "Toy", "Weapon"]),
37: create_gift("Stun Gun", 0, ["RangedWeapon", "Weapon"]),
38: create_gift("Toy Air Gun", 0, ["RangedWeapon", "Toy", "Weapon"]),
39: create_gift("Magnum Air Gun", 0, ["RangedWeapon", "Toy", "Weapon"]),
40: create_gift("Zip Gun", 0, ["RangedWeapon", "Toy", "Weapon"]),
41: create_gift("Laser Gun", 0, ["RangedWeapon", "Beam", "Weapon"]),
42: create_gift("Hyper Beam", 0, ["RangedWeapon", "Beam", "Weapon"]),
43: create_gift("Crusher Beam", 0, ["RangedWeapon", "Beam", "Weapon"]),
44: create_gift("Spectrum Beam", 0, ["RangedWeapon", "Beam", "Weapon"]),
45: create_gift("Death Ray", 0, ["RangedWeapon", "Beam", "Weapon"]),
46: create_gift("Baddest Beam", 0, ["RangedWeapon", "Beam", "Weapon"]),
47: create_gift("Moon Beam Gun", 0, ["RangedWeapon", "Beam", "Weapon"]),
48: create_gift("Gaia Beam", 0, ["RangedWeapon", "Beam", "Weapon"]),
49: create_gift("Yo-yo", 0, ["RangedWeapon", "Toy", "Weapon"]),
50: create_gift("Slingshot", 0, ["RangedWeapon", "Toy", "Weapon"]),
51: create_gift("Bionic Slingshot", 0, ["RangedWeapon", "Toy", "Weapon"]),
52: create_gift("Trick Yo-yo", 0, ["RangedWeapon", "Toy", "Weapon"]),
53: create_gift("Combat Yo-yo", 0, ["RangedWeapon", "Toy", "Weapon"]),
54: create_gift("Travel Charm", 0, ["Jewelry"]),
55: create_gift("Great Charm", 0, ["Jewelry", "Defense", "Armor"]),
56: create_gift("Crystal Charm", 0, ["Jewelry", "Defense", "Armor", "Crystal"]),
57: create_gift("Rabbit's Foot", 0, ["Armor", "Jewelry", "Defense", "Speed"]),
58: create_gift("Flame Pendant", 0, ["Armor", "Jewelry", "Defense", "Fire"]),
59: create_gift("Rain Pendant", 0, ["Armor", "Jewelry", "Defense", "Water"]),
60: create_gift("Night Pendant", 0, ["Armor", "Jewelry", "Defense", "Light"]),
61: create_gift("Sea Pendant", 0, ["Armor", "Jewelry", "Defense", "Light", "Fire", "Water"]),
62: create_gift("Star Pendant", 0, ["Armor", "Jewelry", "Defense", "Light", "Fire", "Water"]),
63: create_gift("Cloak of Kings", 0, ["Armor", "Jewelry", "Defense"]),
64: create_gift("Cheap Bracelet", 0, ["Armor", "Jewelry", "Defense", "Plastic"]),
65: create_gift("Copper Bracelet", 0, ["Armor", "Jewelry", "Defense", "Copper"]),
66: create_gift("Silver Bracelet", 0, ["Armor", "Jewelry", "Defense", "Silver"]),
67: create_gift("Gold Bracelet", 0, ["Armor", "Jewelry", "Defense", "Gold"]),
68: create_gift("Platinum Band", 0, ["Armor", "Jewelry", "Defense", "Platinum"]),
69: create_gift("Diamond Band", 0, ["Armor", "Jewelry", "Defense", "Diamond"]),
70: create_gift("Pixie's Bracelet", 0, ["Armor", "Jewelry", "Defense"]),
71: create_gift("Cherub's Band", 0, ["Armor", "Jewelry", "Defense"]),
72: create_gift("Goddess Band", 0, ["Armor", "Jewelry", "Defense"]),
73: create_gift("Bracer of Kings", 0, ["Armor", "Jewelry", "Defense", "Fire"]),
74: create_gift("Baseball Cap", 0, ["Armor", "Baseball", "Defense", "Hat"]),
75: create_gift("Holmes Hat", 0, ["Armor", "Defense", "Hat"]),
76: create_gift("Mr. Baseball Cap", 0, ["Armor", "Defense", "Hat", "Baseball"]),
77: create_gift("Hard Hat", 0, ["Armor", "Defense", "Hat"]),
78: create_gift("Ribbon", 0, ["Armor", "Cloth", "Defense"]),
79: create_gift("Red Ribbon", 0, ["Armor", "Cloth", "Defense", "Red"]),
80: create_gift("Goddess Ribbon", 0, ["Armor", "Cloth", "Defense"]),
81: create_gift("Coin of Slumber", 0, ["Armor", "Defense"]),
82: create_gift("Coin of Defense", 0, ["Armor", "Defense"]),
83: create_gift("Lucky Coin", 0, ["Armor", "Defense", "Luck"]),
84: create_gift("Talisman Coin", 0, ["Armor", "Defense"]),
85: create_gift("Shiny Coin", 0, ["Armor", "Defense"]),
86: create_gift("Souvenir Coin", 0, ["Armor", "Defense"]),
87: create_gift("Diadem of Kings", 0, ["Armor", "Jewelry", "Defense", "Fire", "Water", "Light"]),
88: create_gift("Cookie", 7, ["Confectionary", "Comsumable", "Heal", "Food"]),
89: create_gift("Bag of Fries", 8, ["FastFood", "Comsumable", "Heal", "Food"]),
90: create_gift("Hamburger", 14, ["FastFood", "Comsumable", "Heal", "Food", "Beef", "Meat"]),
91: create_gift("Boiled Egg", 0, ["Egg", "Comsumable", "Heal", "Food", "White"]),
92: create_gift("Fresh Egg", 0, ["Egg", "Comsumable", "Heal", "Food", "White"]),
93: create_gift("Picnic Lunch", 24, ["Comsumable", "Heal", "Food"]),
94: create_gift("Pasta di Summers", 0, ["Pasta", "Comsumable", "Heal", "Food", "Cooking"]),
95: create_gift("Pizza", 0, ["Comsumable", "Heal", "Food"]),
96: create_gift("Chef's Special", 0, ["Comsumable", "Heal", "Food"]),
97: create_gift("Large Pizza", 0, ["Comsumable", "Heal", "Food"]),
98: create_gift("PSI Caramel", 0, ["Comsumable", "Mana", "Food", "Candy"]),
99: create_gift("Magic Truffle", 0, ["Comsumable", "Mana", "Food", "Candy"]),
100: create_gift("Brain Food Lunch", 0, ["Comsumable", "Mana", "Food", "Heal", "ExoticFood"]),
101: create_gift("Rock Candy", 0, ["Comsumable", "Candy", "Food", "Buff"]),
102: create_gift("Croissant", 0, ["Comsumable", "Food", "Heal", "Bread"]),
103: create_gift("Bread Roll", 0, ["Comsumable", "Food", "Heal", "Bread"]),
106: create_gift("Can of Fruit Juice", 0, ["Comsumable", "Heal", "Drink", "Liquid", "Fruit", "Juice"]),
107: create_gift("Royal Iced Tea", 0, ["Comsumable", "Heal", "Drink", "Liquid"]),
108: create_gift("Protein Drink", 0, ["Comsumable", "Heal", "Drink", "Liquid"]),
109: create_gift("Kraken Soup", 0, ["Comsumable", "Heal", "Food", "Liquid", "Cooking"]),
110: create_gift("Bottle of Water", 0, ["Comsumable", "Mana", "Drink", "Liquid", "Water"]),
111: create_gift("Cold Remedy", 0, ["Comsumable", "Medicine", "Drink", "Liquid", "Cure"]),
112: create_gift("Vial of Serum", 0, ["Comsumable", "Medicine", "Drink", "Liquid", "Cure"]),
113: create_gift("IQ Capsule", 0, ["Comsumable", "Medicine", "IQ", "Buff"]),
114: create_gift("Guts Capsule", 0, ["Comsumable", "Medicine", "Guts", "Buff"]),
115: create_gift("Speed Capsule", 0, ["Comsumable", "Medicine", "Speed", "Buff"]),
116: create_gift("Vital Capsule", 0, ["Comsumable", "Medicine", "Buff", "Life"]),
117: create_gift("Luck Capsule", 0, ["Comsumable", "Medicine", "Buff", "Luck"]),
118: create_gift("Ketchup Packet", 0, ["Comsumable", "Heal", "Food", "Condiment", "Red"]),
119: create_gift("Sugar Packet", 0, ["Comsumable", "Heal", "Food", "Condiment", "White"]),
120: create_gift("Tin of Cocoa", 0, ["Comsumable", "Heal", "Food", "Condiment", "Brown", "Chocolate"]),
121: create_gift("Carton of Cream", 0, ["Comsumable", "Heal", "Food", "Condiment", "White", "Liquid"]),
122: create_gift("Sprig of Parsley", 0, ["Comsumable", "Heal", "Food", "Condiment", "Green", "Plant"]),
123: create_gift("Jar of Hot Sauce", 0, ["Comsumable", "Heal", "Food", "Condiment", "Orange", "Spicy"]),
124: create_gift("Salt Packet", 0, ["Comsumable", "Heal", "Food", "Condiment", "White", "Salted"]),
126: create_gift("Jar of Delisauce", 0, ["Comsumable", "Heal", "Food", "Condiment", "Green"]),
127: create_gift("Wet Towel", 0, ["Comsumable", "Cure"]),
128: create_gift("Refreshing Herb", 0, ["Comsumable", "Cure", "Food", "Herb"]),
129: create_gift("Secret Herb", 0, ["Comsumable", "Cure", "Food", "Life", "Herb"]),
130: create_gift("Horn of Life", 0, ["Comsumable", "Cure", "Life"]),
131: create_gift("Counter-PSI Unit", 0, ["Machine", "Electronics", "Metal"]),
132: create_gift("Shield Killer", 0, ["Machine", "Electronics", "Metal", "Neutralizing"]),
133: create_gift("Bazooka", 0, ["Machine", "Weapon", "Electronics", "Explosive", "RangedWeapon"]),
134: create_gift("Heavy Bazooka", 0, ["Machine", "Weapon", "Electronics", "Explosive", "RangedWeapon"]),
135: create_gift("HP-Sucker", 0, ["Machine", "Draining", "Electronics"]),
136: create_gift("Hungry HP-Sucker", 0, ["Machine", "Draining", "Electronics"]),
137: create_gift("Xterminator Spray", 0, ["Can", "Metal", "Insect", "Weapon", "Chemicals"]),
138: create_gift("Slime Generator", 0, ["Machine", "Slime", "Electronics"]),
140: create_gift("Ruler", 0, ["Long", "Wood", "Trash", "IQ"]),
141: create_gift("Snake Bag", 0, ["Animal", "Container", "Throwing"]),
142: create_gift("Mummy Wrap", 0, ["Ancient", "Paper", "Weapon", "Throwing", "Consumable"]),
143: create_gift("Protractor", 0, ["Metal", "Trash", "IQ"]),
144: create_gift("Bottle Rocket", 0, ["Weapon", "Explosive", "Rocket", "Fireworks", "Consumable"]),
145: create_gift("Big Bottle Rocket", 0, ["Weapon", "Explosive", "Rocket", "Fireworks", "Consumable"]),
146: create_gift("Multi Bottle Rocket", 0, ["Weapon", "Explosive", "Rocket", "Fireworks", "Consumable"]),
147: create_gift("Bomb", 0, ["Weapon", "Explosive", "Throwing", "Consumable", "Bomb"]),
148: create_gift("Super Bomb", 0, ["Weapon", "Explosive", "Throwing", "Consumable", "Bomb"]),
149: create_gift("Insecticide Spray", 0, ["Can", "Metal", "Insect", "Weapon", "Consumable", "Chemicals"]),
150: create_gift("Rust Promoter", 0, ["Can", "Metal", "Rusting", "Weapon", "Consumable", "Chemicals"]),
151: create_gift("Rust Promoter DX", 0, ["Can", "Metal", "Rusting", "Weapon", "Consumable", "Chemicals", "Insect"]),
152: create_gift("Pair of Dirty Socks", 0, ["Consumable", "Throwing", "Stinky", "Clothing"]),
153: create_gift("Stag Beetle", 0, ["Consumable", "Throwing", "Animal", "Insect"]),
154: create_gift("Toothbrush", 0, ["Consumable", "Tool"]),
155: create_gift("Handbag Strap", 0, ["Consumable", "Weapon", "Throwing", "Leather"]),
156: create_gift("Pharaoh's Curse", 0, ["Consumable", "Weapon", "Throwing", "Goo", "Slime", "Poison", "Chemicals"]),
157: create_gift("Defense Shower", 0, ["Can", "Machine", "Chemicals", "Buff", "Defense", "Liquid"]),
159: create_gift("Sudden Guts Pill", 0, ["Consumable", "Guts", "Buff"]),
160: create_gift("Bag of Dragonite", 0, ["Consumable", "Weapon", "Powder"]),
161: create_gift("Defense Spray", 0, ["Can", "Consumable", "Chemicals", "Buff", "Defense", "Liquid"]),
165: create_gift("Picture Postcard", 0, ["Paper", "Trash"]),
168: create_gift("Chick", 0, ["Animal", "Yellow", "Chicken"]),
169: create_gift("Chicken", 0, ["Animal", "White", "Chicken"]),
186: create_gift("Meteotite", 0, ["Mineral", "Artifact", "Brown", "Gem"]),
188: create_gift("Hand-Aid", 0, ["Consumable", "Heal", "Cloth"]),
189: create_gift("Trout Yogurt", 0, ["Consumable", "Heal", "Food", "Fish", "Dairy"]),
190: create_gift("Banana", 0, ["Consumable", "Heal", "Food", "Fruit", "Yellow"]),
191: create_gift("Calorie Stick", 0, ["Consumable", "Heal", "Food", "Jerky"]),
194: create_gift("Earth Pendant", 0, ["Armor", "Jewelry", "Fire", "Ice", "Light"]),
195: create_gift("Neutralizer", 0, ["Machine", "Electronics", "Metal", "Neutralizing"]),
198: create_gift("Gelato de Resort", 0, ["Consumable", "Food", "Heal", "Dairy", "FrozenFood"]),
199: create_gift("Snake", 0, ["Animal", "Weapon", "Throwing", "Consumable"]),
200: create_gift("Viper", 0, ["Animal", "Weapon", "Throwing", "Poison", "Consumable"]),
201: create_gift("Brain Stone", 0, ["Stone", "Mineral", "Trash"]),
207: create_gift("Magic Tart", 0, ["Food", "Consumable", "Mana", "Confectionary"]),
209: create_gift("Monkey's Love", 0, ["Weapon", "Animal"]),
212: create_gift("T-Rex's Bat", 0, ["MeleeWeapon", "Wood", "Baseball", "Toy", "Weapon"]),
213: create_gift("Big League Bat", 0, ["MeleeWeapon", "Wood", "Baseball", "Toy", "Weapon"]),
214: create_gift("Ultimate Bat", 0, ["MeleeWeapon", "Wood", "Baseball", "Toy", "Weapon"]),
215: create_gift("Double Beam", 0, ["RangedWeapon", "Gun", "Beam", "Weapon"]),
216: create_gift("Platinum Band", 0, ["Armor", "Defense", "Platinum", "Jewelry"]),
217: create_gift("Diamond Band", 0, ["Armor", "Defense", "Diamond", "Jewelry"]),
218: create_gift("Defense Ribbon", 0, ["Armor", "Cloth", "Defense"]),
219: create_gift("Talisman Ribbon", 0, ["Armor", "Cloth", "Defense"]),
220: create_gift("Saturn Ribbon", 0, ["Armor", "Cloth", "Defense"]),
221: create_gift("Coin of Silence", 0, ["Armor", "Defense"]),
222: create_gift("Charm Coin", 0, ["Armor", "Defense"]),
223: create_gift("Cup of Noodles", 0, ["Food", "Consumable", "Heal", "FastFood", "Pasta"]),
224: create_gift("Repel Sandwich", 0, ["Food", "Consumable", "Heal", "Repellant", "Sandwich"]),
225: create_gift("Repel Superwich", 0, ["Food", "Consumable", "Heal", "Repellant", "Sandwich"]),
226: create_gift("Lucky Sandwich", 0, ["Food", "Consumable", "Heal", "Luck", "Mana", "Sandwich"]),
232: create_gift("Cup of Coffee", 0, ["Drink", "Consumable", "Heal", "Liquid", "Coffee"]),
233: create_gift("Double Burger", 0, ["FastFood", "Comsumable", "Heal", "Food", "Beef", "Meat"]),
234: create_gift("Peanut Cheese Bar", 0, ["Comsumable", "Heal", "Food", "Candy", "ExoticFood"]),
235: create_gift("Piggy Jelly", 0, ["Comsumable", "Heal", "Food", "ExoticFood", "Gelatin", "Jelly"]),
236: create_gift("Bowl of Rice Gruel", 0, ["Comsumable", "Heal", "Food", "Cooking", "ExoticFood", "Liquid"]),
237: create_gift("Bean Croquette", 0, ["Comsumable", "Heal", "Food", "Cooking", "ExoticFood"]),
238: create_gift("Molokheiya Soup", 0, ["Comsumable", "Heal", "Food", "Cooking", "ExoticFood", "Vegetable",
"Liquid"]),
239: create_gift("Plain Roll", 0, ["Comsumable", "Heal", "Food", "Bread"]),
240: create_gift("Kabob", 0, ["Comsumable", "Heal", "Food", "ExoticFood", "Meat"]),
241: create_gift("Plain Yogurt", 0, ["Comsumable", "Heal", "Food", "Slime", "Dairy"]),
242: create_gift("Beef Jerky", 0, ["Comsumable", "Heal", "Food", "Meat", "Dried", "Jerky"]),
243: create_gift("Mammoth Burger", 0, ["FastFood", "Comsumable", "Heal", "Food", "Beef", "Meat"]),
244: create_gift("Spicy Jerky", 0, ["Comsumable", "Heal", "Food", "Meat", "Dried", "Jerky", "Spicy"]),
245: create_gift("Luxury Jerky", 0, ["Comsumable", "Heal", "Food", "Meat", "Dried", "Jerky", "Luxury"]),
246: create_gift("Bottle of DXwater", 0, ["Comsumable", "Mana", "Drink", "Liquid", "Water"]),
247: create_gift("Magic Pudding", 0, ["Comsumable", "Mana", "Food", "Candy"]),
248: create_gift("Non-Stick Frypan", 0, ["MeleeWeapon", "Metal", "Tool", "Weapon"]),
249: create_gift("Mr. Saturn Coin", 0, ["Armor", "Defense"]),
250: create_gift("Meteornium", 0, ["Mineral", "Artifact", "SpaceMineral"]),
251: create_gift("Popsicle", 0, ["Consumable", "Food", "Heal", "FrozenFood"]),
252: create_gift("Cup of Lifenoodles", 0, ["Consumable", "Food", "Cure", "Life", "Pasta"])
# Todo; separate traits for GoodWeapon and BadWeapon
# Todo; Satus heals should be Medicine, Cure
}

View File

@@ -0,0 +1,454 @@
from ..game_data.local_data import item_id_table
import random
from typing import Any
gift_exclusions = [
"Franklin Badge",
"Pak of Bubble Gum",
"Jar of Fly Honey",
"Tiny Key",
"Yogurt Dispenser",
"UFO Engine",
"Piggy Nose",
"Shyness Book",
"King Banana",
"Letter For Tony",
"Key to the Shack",
"Key to the Cabin",
"Bad Key Machine",
"Zombie Paper",
"Hawk Eye",
"ATM Card",
"Show Ticket",
"Tenda Lavapants",
"Wad of Bills",
"Receiver Phone",
"Diamond",
"Signed Banana",
"Pencil Eraser",
"Hieroglyph Copy",
"Contact Lens",
"Key to the Tower",
"Meteorite Piece",
"Sound Stone",
"Police Badge",
"Mining Permit",
"Key to the Locker",
"Insignificant Item",
"Tiny Ruby",
"Eraser Eraser",
"Tendakraut",
"Progressive Bat",
"Progressive Fry Pan",
"Progressive Gun",
"Progressive Bracelet",
"Progressive Other",
"Carrot Key"
]
wanted_traits = [
"Armor",
"Weapon",
"Cure",
"Bomb",
"Mana",
"Heal",
"Life",
"Neutralizing",
"Draining",
"Beef",
"Jerky",
"Egg",
"Chicken",
"Spicy",
"Broken",
"Pasta",
"Pizza",
"Condiment",
"Dairy",
"AnimalProduct",
"Copper",
"Silver",
"Gold",
"Diamond",
"Plastic",
"Herb",
"Repellant",
"Slime",
"Animal",
"Juice",
"Meat",
"Water",
"Drink",
"FastFood",
"Bread",
"FrozenFood",
"Fruit",
"Toy",
"Salted",
"Speed",
"Guts",
"Luck",
"Doll",
"Legendary",
"Buff",
"Pipe",
"Hat",
"Trash",
"ExoticFood",
"Insect",
"Fire",
"Ice",
"Light",
"Food",
"Consumable",
"Electronics",
"Candy",
"Medicine",
"Coffee",
"Artifact",
"Fireworks",
"Confectionary",
"Explosive",
"Jewelry",
"Rock",
"Metal"
]
# If these traits are in the item, then pick randomly from the results
# If multiple fit, pick the combined highest quality.
secondary_trait_list = {
"Beef": ["Hamburger", "Double Burger", "Mammoth Burger", "Beef Jerky"],
"Jerky": ["Beef Jerky", "Spicy Jerky", "Luxury Jerky"],
"Egg": ["Fresh Egg", "Boiled Egg"],
"Chicken": ["Chicken"],
"Spicy": ["Jar of Hot Sauce", "Spicy Jerky"],
"Broken": ["Broken Machine", "Broken Gadget", "Broken Air Gun", "Broken Spray Can",
"Broken Laser", "Broken Iron", "Broken Pipe", "Broken Cannon", "Broken Tube",
"Broken Bazooka", "Broken Trumpet", "Broken Harmonica", "Broken Antenna"],
"Pasta": ["Pasta di Summers", "Cup of Noodles", "Cup of Lifenoodles"],
"Pizza": ["Pizza", "Large Pizza"],
"Condiment": ["Ketchup Packet", "Sugar Packet", "Salt Packet", "Tin of Cocoa",
"Carton of Cream", "Sprig of Parsley", "Jar of Delisauce", "Jar of Hot Sauce"],
"Dairy": ["Plain Yogurt", "Trout Yogurt", "Gelato de Resort"],
"AnimalProduct": ["Fresh Egg"],
"Copper": ["Copper Bracelet"],
"Silver": ["Silver Bracelet"],
"Gold": ["Gold Bracelet"],
"Diamond": ["Diamond Band"],
"Plastic": ["Cheap Bracelet", "Bottle of Water", "Bottle of DXwater"],
"Herb": ["Refreshing Herb", "Secret Herb"],
"Repellant": ["Repel Sandwich", "Repel Superwich"],
"Slime": ["Slime Generator"],
"Animal": ["Chicken", "Chick", "Snake", "Viper"],
"Juice": ["Can of Fruit Juice"],
"Meat": ["Hamburger", "Double Burger", "Mammoth Burger", "Beef Jerky",
"Spicy Jerky", "Luxury Jerky", "Kabob"],
"Water": ["Bottle of Water", "Bottle of DXwater"],
"Drink": ["Bottle of Water", "Bottle of DXwater", "Cup of Coffee", "Can of Fruit Juice", "Protein Drink",
"Royal Iced Tea"],
"FastFood": ["Hamburger", "Ketchup Packet", "Double Burger", "Bag of Fries"],
"Bread": ["Plain Roll", "Bread Roll", "Croissant"],
"FrozenFood": ["Popsicle", "Gelato de Resort"],
"Fruit": ["Banana", "Can of Fruit Juice"],
"Toy": ["Toy Air Gun", "Teddy Bear", "Super Plush Bear", "Yo-yo", "Slingshot"],
"Salted": ["Salt Packet"],
"Speed": ["Speed Capsule", "Rabbit's Foot"],
"Guts": ["Guts Capsule", "Sudden Guts Pill", "Gutsy Bat"],
"Luck": ["Lucky Coin", "Luck Capsule", "Lucky Sandwich"],
"Doll": ["Teddy Bear", "Super Plush Bear"],
"Legendary": ["Legendary Bat"],
"Buff": ["Sudden Guts Pill", "Guts Capsule", "Speed Capsule", "IQ Capsule", "Luck Capsule", "Vital Capsule",
"Defense Spray", "Defense Shower", "Rock Candy"],
"Pipe": ["HP-Sucker", "Hungry HP-Sucker", "Broken Pipe"],
"Hat": ["Holmes Hat", "Hard Hat", "Baseball Cap", "Mr. Baseball Cap"],
"Trash": ["Broken Machine", "Broken Gadget", "Broken Air Gun", "Broken Spray Can",
"Broken Laser", "Broken Iron", "Broken Pipe", "Broken Cannon", "Broken Tube",
"Broken Bazooka", "Broken Trumpet", "Broken Harmonica", "Broken Antenna",
"Ruler", "Pair of Dirty Socks", "Protractor"],
"ExoticFood": ["Piggy Jelly", "Peanut Cheese Bar", "Bowl of Rice Gruel",
"Molokheiya Soup", "Kabob", "Bean Croquette", "Brain Food Lunch"],
"Insect": ["Insecticde Spray", "Xterminator Spray", "Stag Beetle"],
"Fire": ["Flame Pendant"],
"Ice": ["Rain Pendant"],
"Light": ["Night Pendant"],
"Electronics": ["Slime Generator", "Shield Killer", "Neutralizer", "Defense Shower", "Counter-PSI Unit",
"HP-Sucker", "Hungry HP-Sucker"],
"Candy": ["PSI Caramel", "Magic Truffle", "Rock Candy", "Magic Pudding", "Peanut Cheese Bar"],
"Medicine": ["Vial of Serum", "Cold Remedy", "IQ Capsule", "Guts Capsule", "Speed Capsule", "Vital Capsule", "Luck Capsule"],
"Coffee": ["Cup of Coffee"],
"Artifact": ["Metotite", "Meteornium"],
"Fireworks": ["Bottle Rocket", "Big Bottle Rocket", "Multi Bottle Rocket"],
"Confectionary": ["Cookie", "Magic Tart"],
"Explosive": ["Bottle Rocket", "Big Bottle Rocket", "Multi Bottle Rocket", "Heavy Bazooka",
"Bazooka", "Bomb", "Super Bomb"],
"Jewelry": ["Cheap Bracelet", "Copper Bracelet", "Silver Bracelet", "Gold Bracelet",
"Platinum Band", "Diamond Band", "Flame Pendant", "Sea Pendant", "Star Pendant", "Earth Pendant",
"Rain Pendant", "Night Pendant"],
"Rock": ["Rock Candy", "Brain Stone"],
"Metal": ["Broken Machine", "Broken Gadget", "Broken Air Gun", "Broken Spray Can",
"Broken Laser", "Broken Iron", "Broken Pipe", "Broken Cannon", "Broken Tube",
"Broken Bazooka", "Broken Trumpet", "Broken Harmonica", "Broken Antenna", "Slime Generator",
"Fry Pan", "Magic Fry Pan", "Thick Fry Pan", "Deluxe Fry Pan", "Chef's Fry Pan",
"French Fry Pan", "Holy Fry Pan", "Non-Stick Frypan"],
"Food": ["Cookie", "Bag of Fries", "Hamburger", "Boiled Egg", "Fresh Egg", "Picnic Lunch",
"Pasta di Summers", "Pizza", "Chef's Special", "Large Pizza", "PSI Caramel", "Magic Truffle",
"Brain Food Lunch", "Rock Candy", "Croissant", "Bread Roll", "Kraken Soup",
"Trout Yogurt", "Banana", "Calorie Stick", "Gelato de Resort", "Magic Tart",
"Cup of Noodles", "Repel Sandwich", "Repel Superwich", "Lucky Sandwich", "Double Burger",
"Peanut Cheese Bar", "Piggy Jelly", "Bowl of Rice Gruel", "Bean Croquette",
"Molokheiya Soup", "Plain Roll", "Kabob", "Plain Yogurt", "Beef Jerky",
"Mammoth Burger", "Spicy Jerky", "Luxury Jerky", "Magic Pudding",
"Popsicle"]
}
tertiary_trait_list = {
"Consumable": ["Cookie", "Bag of Fries", "Hamburger", "Boiled Egg", "Fresh Egg", "Picnic Lunch",
"Pasta di Summers", "Pizza", "Chef's Special", "Large Pizza", "PSI Caramel", "Magic Truffle",
"Brain Food Lunch", "Rock Candy", "Croissant", "Bread Roll", "Kraken Soup",
"Trout Yogurt", "Banana", "Calorie Stick", "Gelato de Resort", "Magic Tart",
"Cup of Noodles", "Repel Sandwich", "Repel Superwich", "Lucky Sandwich", "Double Burger",
"Peanut Cheese Bar", "Piggy Jelly", "Bowl of Rice Gruel", "Bean Croquette",
"Molokheiya Soup", "Plain Roll", "Kabob", "Plain Yogurt", "Beef Jerky",
"Mammoth Burger", "Spicy Jerky", "Luxury Jerky", "Magic Pudding",
"Popsicle", "Can of Fruit Juice", "Royal Iced Tea", "Protein Drink",
"Bottle of Water", "Cold Remedy", "Vial of Serum", "IQ Capsule",
"Guts Capsule", "Speed Capsule", "Vital Capsule", "Luck Capsule",
"Ketchup Packet", "Sugar Packet", "Tin of Cocoa", "Carton of Cream", "Sprig of Parsley",
"Jar of Hot Sauce", "Salt Packet", "Jar of Delisauce", "Wet Towel", "Refreshing Herb",
"Secret Herb", "Horn of Life", "Mummy Wrap", "Bottle Rocket", "Big Bottle Rocket",
"Multi Bottle Rocket", "Bomb", "Super Bomb", "Insecticide Spray", "Rust Promoter",
"Rust Promoter DX", "Pair of Dirty Socks", "Stag Beetle", "Toothbrush",
"Handbag Strap", "Pharaoh's Curse", "Sudden Guts Pill", "Bag of Dragonite",
"Defense Spray", "Chick", "Chicken", "Hand-Aid", "Snake", "Viper",
"Cup of Coffee", "Bottle of DXwater", "Cup of Lifenoodles"]
}
scaled_traits = [
"Armor",
"Weapon",
"Cure",
"Bomb",
"Mana",
"Heal",
"Life",
"Neutralizing",
"Draining"
]
gift_by_quality = {
"Heal": {
0.06: "Cookie",
0.08: "Can of Fruit Juice",
0.12: "Cup of Coffee",
0.18: "Popsicle",
0.22: "Banana",
0.24: "Bag of Fries",
0.30: "Trout Yogurt",
0.35: "Bread Roll",
0.42: "Bean Croquette",
0.43: "Cup of Noodles",
0.45: "Boiled Egg",
0.48: "Hamburger",
0.60: "Royal Iced Tea",
0.63: "Calorie Stick",
0.65: "Croissant",
0.70: "Lucky Sandwich",
0.80: "Picnic Lunch",
0.82: "Plain Roll",
0.84: "Fresh Egg",
0.88: "Molokheiya Soup",
0.96: "Double Burger",
1.00: "Peanut Cheese Bar",
1.10: "Pasta di Summers",
1.20: "Pizza",
1.26: "Kabob",
1.50: "Beef Jerky",
1.60: "Plain Yogurt",
2.05: "Mammoth Burger",
2.16: "Bowl of Rice Gruel",
2.20: "Chef's Special",
2.52: "Spicy Jerky",
2.40: "Large Pizza",
3.00: "Piggy Jelly",
3.10: "Luxury Jerky",
3.50: "Brain Food Lunch",
4.00: "Kraken Soup",
4.01: "Hand-Aid"
},
"Armor": {
0.05: "Travel Charm",
0.10: "Great Charm",
0.12: "Cheap Bracelet",
0.13: "Baseball Cap",
0.14: "Mr. Baseball Cap",
0.24: "Copper Bracelet",
0.26: "Holmes Hat",
0.20: "Crystal Charm",
0.36: "Silver Bracelet",
0.38: "Hard Hat",
0.48: "Ribbon",
0.50: "Diadem of Kings",
0.60: "Red Ribbon",
0.73: "Gold Bracelet",
0.75: "Bracer of Kings",
0.78: "Coin of Slumber",
0.97: "Platinum Band",
0.98: "Defense Ribbon",
0.99: "Coin of Defense",
1.00: "Cloak of Kings",
1.21: "Diamond Band",
1.25: "Lucky Coin",
1.46: "Pixie's Bracelet",
1.48: "Talisman Coin",
1.50: "Talisman Ribbon",
1.70: "Cherub's Band",
1.75: "Shiny Coin",
1.95: "Goddess Band",
2.00: "Souvenir Coin",
2.19: "Saturn Ribbon",
2.68: "Goddess Ribbon"
},
"Draining": {
0.50: "HP-Sucker",
1.00: "Hungry HP-Sucker"
},
"Bomb": {
0.50: "Bomb",
1.00: "Super Bomb"
},
"Neutralizing": {
0.50: "Shield Killer",
1.00: "Neutralizer"
},
"Cure": {
0.10: "Cold Remedy",
0.25: "Vial of Serum",
0.50: "Wet Towel",
1.00: "Refreshing Herb",
2.00: "Secret Herb",
3.00: "Horn of Life",
3.01: "Cup of Lifenoodles"
},
"Life": {
0.50: "Secret Herb",
1.00: "Cup of Lifenoodles",
1.01: "Horn of Life",
},
"Weapon": {
0.01: "Casey Bat",
0.04: "Cracked Bat",
0.11: "Yo-yo",
0.15: "Tee Ball Bat",
0.19: "Fy Pan",
0.23: "Slingshot",
0.28: "Sand Lot Bat",
0.30: "Pop Gun",
0.38: "Thick Fry Pan",
0.40: "Bionic Slingshot",
0.46: "Stun Gun",
0.50: "Minor League Bat",
0.57: "Deluxe Fry Pan",
0.61: "Toy Air Gun",
0.69: "Magnum Air Gun",
0.73: "Mr. Baseball Bat",
0.76: "Chef's Fry Pan",
0.78: "Zip Gun",
0.88: "Trick Yo-yo",
0.92: "Laser Gun",
0.95: "T-Rex's Bat",
0.96: "Non-Stick Frypan",
1.00: "Big League Bat",
1.03: "Combat Yo-yo",
1.11: "Hyper Beam",
1.15: "French Fry Pan",
1.19: "Hall of Fame Bat",
1.26: "Double Beam",
1.32: "Ultimate Bat",
1.38: "Crusher Beam",
1.50: "Spectrum Beam",
1.53: "Magicant Bat",
1.73: "Death Ray",
1.80: "Sword of Kings",
1.88: "Baddest Beam",
2.00: "Magic Fry Pan",
2.11: "Legendary Bat",
2.15: "Moon Beam Gun",
2.40: "Holy Fry Pan",
2.45: "Gaia Beam",
2.55: "Gutsy Bat",
},
"Mana": {
0.28: "Bottle of Water",
0.50: "PSI Caramel",
0.51: "Magic Tart",
1.14: "Bottle of DXwater",
1.20: "Magic Pudding",
2.28: "Magic Truffle"
}
}
def trait_interpreter(gift: dict[str, Any]) -> int:
"""Converts received gifts into in-game items.
If the item name perfectly matches an in-game item, that item will be received.
If any of the traits can be scaled i.e. a healing item, the gift will be converted into an item of roughly that value.
If any of the traits are not scaled, but are in the secondary trait list i.e. a food item, it will be converted into a random appropriate item.
If none of the traits are applicable, return a random consumable."""
item = None
trait_list = []
got_trait = False
if "Traits" in gift:
gift["traits"] = gift.pop("Traits")
for trait in gift["traits"]:
if "Trait" in trait:
trait["trait"] = trait.pop("Trait")
if "Quality" in trait:
trait["quality"] = trait.pop("Quality")
if "quality" not in trait:
trait["quality"] = 1
trait_list.append(trait["trait"])
if trait["trait"] in scaled_traits:
item_quality_table = gift_by_quality[trait["trait"]]
quality = min(item_quality_table.keys(), key=lambda x: abs(x - trait["quality"]))
item = item_quality_table[quality]
got_trait = True
break
if not got_trait:
for trait in trait_list:
if trait in secondary_trait_list:
item = random.choice(secondary_trait_list[trait])
got_trait = True
break
if not got_trait:
for trait in trait_list:
if trait in tertiary_trait_list:
item = random.choice(tertiary_trait_list[trait])
break
if item is not None:
item = item_id_table[item]
else:
item = random.choice(secondary_trait_list["Consumable"])
item = item_id_table[item]
return item
# IF trait is in special traits, give that item.
# Else if the trait is in a Scaled trait (Food, Armor, etc., then break them up by scaling)

View File

View File

@@ -0,0 +1,603 @@
from ..modules.enemy_data import combat_regions
from ..Options import MagicantMode
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .. import EarthBoundWorld
expected_level_gains = {
"Ness's Mind": 0,
"Global ATM Access": 0,
"Northern Onett": 1,
"Onett": 1,
"Arcade": 0,
"Giant Step": 2,
"Twoson": 0,
"Everdred's House": 1,
"Common Condiment Shop": 0,
"Peaceful Rest Valley": 2,
"Happy-Happy Village": 0,
"Happy-Happy HQ": 1,
"Lilliput Steps": 2,
"Threed": 0,
"Boogey Tent": 0,
"Threed Underground": 2,
"Grapefruit Falls": 1,
"Saturn Valley": 0,
"Belch's Factory": 2,
"Upper Saturn Valley": 0,
"Milky Well": 3,
"Dusty Dunes Desert": 2,
"Gold Mine": 2,
"Monkey Caves": 2,
"Fourside": 0,
"Fourside Dept. Store": 1,
"Moonside": 1,
"Monotoli Building": 2,
"Magnet Hill": 2,
"Winters": 2,
"Snow Wood Boarding School": 0,
"Southern Winters": 1,
"Brickroad Maze": 1,
"Andonuts Lab Area": 0,
"Rainy Circle": 2,
"Stonehenge Base": 2,
"Summers": 0,
"Summers Museum": 0,
"Dalaam": 0,
"Pink Cloud": 2,
"Scaraba": 0,
"Pyramid": 2,
"Southern Scaraba": 1,
"Dungeon Man": 2,
"Deep Darkness": 0,
"Deep Darkness Darkness": 2,
"Tenda Village": 0,
"Lumine Hall": 2,
"Lost Underworld": 2,
"Fire Spring": 2,
"Magicant": 1,
"Sea of Eden": 1,
"Cave of the Present": 0,
"Cave of the Past": 2,
"Endgame": 0
}
locations_with_item_requirements = [
"Onett - Traveling Entertainer",
"Onett - South Road Present",
"Onett - Tracy Gift",
"Twoson - Paula's Mother",
"Twoson - Everdred Meeting",
"Twoson - Insignificant Location",
"Happy-Happy Village - Defeat Carpainter",
"Happy-Happy Village - Prisoner",
"Threed - Boogey Tent Trashcan",
"Threed - Zombie Prisoner",
"Saturn Valley - Post Belch Gift #1",
"Saturn Valley - Post Belch Gift #2",
"Saturn Valley - Post Belch Gift #3",
"Saturn Valley - Saturn Coffee",
"Monkey Caves - Talah Rama Chest #1",
"Monkey Caves - Talah Rama Chest #2",
"Monkey Caves - Talah Rama Gift",
"Monkey Caves - Monkey Power",
"Dusty Dunes - Mine Reward",
"Snow Wood - Upper Right Locker",
"Snow Wood - Upper Left Locker",
"Snow Wood - Bottom Right Locker",
"Snow Wood - Bottom Left Locker",
"Fourside - Bakery 2F Gift",
"Fourside - Department Store Blackout",
"Fourside - Venus Gift",
"Summers - Museum Item",
"Dalaam - Trial of Mu",
"Deep Darkness - North Alcove Truffle",
"Deep Darkness - Near Land Truffle",
"Deep Darkness - Present Truffle",
"Deep Darkness - Village Truffle",
"Deep Darkness - Entrance Truffle",
"Tenda Village - Tenda Tea",
"Tenda Village - Tenda Gift",
"Tenda Village - Tenda Gift #2",
"Lost Underworld - Talking Rock",
"Lost Underworld - Tenda Camp Shop Slot 1",
"Lost Underworld - Tenda Camp Shop Slot 2",
"Lost Underworld - Tenda Camp Shop Slot 3",
"Lost Underworld - Tenda Camp Shop Slot 4",
"Lost Underworld - Tenda Camp Shop Slot 5",
"Lost Underworld - Tenda Camp Shop Slot 6",
"Lost Underworld - Tenda Camp Shop Slot 7",
"Dusty Dunes - Mine Food Cart Slot 1",
"Dusty Dunes - Mine Food Cart Slot 2",
"Dusty Dunes - Mine Food Cart Slot 3",
"Dusty Dunes - Mine Food Cart Slot 4",
"Dusty Dunes - Mine Food Cart Slot 5",
"Dusty Dunes - Mine Food Cart Slot 6",
"Dusty Dunes - Mine Food Cart Slot 7",
"Saturn Valley Shop - Post-Belch Saturn Slot 1",
"Saturn Valley Shop - Post-Belch Saturn Slot 2",
"Saturn Valley Shop - Post-Belch Saturn Slot 3",
"Saturn Valley Shop - Post-Belch Saturn Slot 4",
"Deep Darkness - Arms Dealer Slot 1",
"Deep Darkness - Arms Dealer Slot 2",
"Deep Darkness - Arms Dealer Slot 3",
"Deep Darkness - Arms Dealer Slot 4",
"Deep Darkness - Businessman Slot 1",
"Deep Darkness - Businessman Slot 2",
"Deep Darkness - Businessman Slot 3",
"Deep Darkness - Businessman Slot 4",
"Deep Darkness - Businessman Slot 5",
"Deep Darkness - Businessman Slot 6",
"Deep Darkness - Businessman Slot 7",
"Dalaam Restaurant - Slot 1",
"Dalaam Restaurant - Slot 2",
"Dalaam Restaurant - Slot 3",
"Dalaam Restaurant - Slot 4"]
def calculate_scaling(world: "EarthBoundWorld") -> None:
"""Calculates the individual scaled level of each region/major area."""
arcade = world.dungeon_connections["Arcade"]
giant_step = world.dungeon_connections["Giant Step"]
lilliput_steps = world.dungeon_connections["Lilliput Steps"]
happy_happy_hq = world.dungeon_connections["Happy-Happy HQ"]
belch_factory = world.dungeon_connections["Belch's Factory"]
milky_well = world.dungeon_connections["Milky Well"]
gold_mine = world.dungeon_connections["Gold Mine"]
monotoli_building = world.dungeon_connections["Monotoli Building"]
magnet_hill = world.dungeon_connections["Magnet Hill"]
moonside = world.dungeon_connections["Moonside"]
pink_cloud = world.dungeon_connections["Pink Cloud"]
rainy_circle = world.dungeon_connections["Rainy Circle"]
stonehenge_base = world.dungeon_connections["Stonehenge Base"]
brickroad_maze = world.dungeon_connections["Brickroad Maze"]
pyramid = world.dungeon_connections["Pyramid"]
dungeon_man = world.dungeon_connections["Dungeon Man"]
lumine_hall = world.dungeon_connections["Lumine Hall"]
fire_spring = world.dungeon_connections["Fire Spring"]
sea_of_eden = world.dungeon_connections["Sea of Eden"]
world.area_exits = {
"Ness's Mind": ["Onett", "Twoson", "Happy-Happy Village", "Threed", "Saturn Valley", "Dusty Dunes Desert",
"Fourside", "Winters", "Summers", "Dalaam", "Scaraba", "Deep Darkness", "Tenda Village",
"Lost Underworld", "Magicant"],
"Northern Onett": ["Onett"],
"Onett": ["Northern Onett", "Twoson", giant_step, arcade, "Global ATM Access"],
arcade: [arcade],
"Giant Step": ["Giant Step"],
"Twoson": ["Onett", "Peaceful Rest Valley", "Threed", "Everdred's House", "Common Condiment Shop", "Global ATM Access"],
"Everdred's House": ["Everdred's House"],
"Peaceful Rest Valley": ["Twoson", "Happy-Happy Village"],
"Happy-Happy Village": ["Peaceful Rest Valley", lilliput_steps, happy_happy_hq, "Global ATM Access"],
"Happy-Happy HQ": ["Happy-Happy HQ"],
"Lilliput Steps": ["Lilliput Steps"],
"Threed": ["Twoson", "Dusty Dunes Desert", "Andonuts Lab Area", "Threed Underground", "Boogey Tent", "Winters", "Global ATM Access"],
"Boogey Tent": ["Boogey Tent"],
"Threed Underground": ["Grapefruit Falls"],
"Grapefruit Falls": [belch_factory, "Saturn Valley", "Threed Underground"],
"Saturn Valley": ["Grapefruit Falls", "Cave of the Present", "Global ATM Access"],
belch_factory: ["Upper Saturn Valley"],
"Upper Saturn Valley": ["Saturn Valley", world.dungeon_connections["Milky Well"]],
"Milky Well": ["Milky Well"],
"Dusty Dunes Desert": ["Threed", "Monkey Caves", gold_mine, "Fourside", "Global ATM Access"],
"Monkey Caves": ["Monkey Caves"],
"Gold Mine": ["Gold Mine"],
"Fourside": ["Dusty Dunes Desert", monotoli_building, magnet_hill, "Threed", "Fourside Dept. Store", moonside, "Global ATM Access"],
"Moonside": ["Moonside", "Global ATM Access"],
"Monotoli Building": ["Monotoli Building"],
"Fourside Dept. Store": ["Fourside Dept. Store"],
"Magnet Hill": ["Magnet Hill"],
"Winters": ["Snow Wood Boarding School", "Southern Winters", "Global ATM Access"],
"Snow Wood Boarding School": ["Snow Wood Boarding School"],
"Southern Winters": [brickroad_maze],
brickroad_maze: [rainy_circle, "Southern Winters"],
"Stonehenge Base": ["Stonehenge Base"],
rainy_circle: [brickroad_maze, "Andonuts Lab Area"],
"Andonuts Lab Area": [rainy_circle, "Winters", stonehenge_base],
"Summers": ["Scaraba", "Summers Museum", "Global ATM Access"],
"Summers Museum": ["Summers Museum"],
"Dalaam": [pink_cloud],
"Pink Cloud": ["Pink Cloud"],
"Scaraba": [pyramid, "Common Condiment Shop", "Global ATM Access"],
pyramid: ["Southern Scaraba"],
"Southern Scaraba": [dungeon_man],
"Dungeon Man": ["Deep Darkness"],
"Deep Darkness": ["Deep Darkness Darkness"],
"Deep Darkness Darkness": ["Tenda Village", "Deep Darkness"],
"Tenda Village": [lumine_hall, "Deep Darkness Darkness"],
"Lumine Hall": ["Lost Underworld"],
"Lost Underworld": [fire_spring],
"Fire Spring": ["Fire Spring"],
"Magicant": [sea_of_eden, "Global ATM Access"],
"Sea of Eden": ["Sea of Eden"],
"Cave of the Present": ["Cave of the Past"],
"Cave of the Past": ["Endgame"],
"Endgame": ["Endgame"],
"Global ATM Access": ["Global ATM Access"],
"Common Condiment Shop": ["Common Condiment Shop"]
}
world.area_rules = {
"Ness's Mind": {"Onett": [["Onett Teleport"]],
"Twoson": [["Twoson Teleport"]],
"Happy-Happy Village": [["Happy-Happy Village Teleport"]],
"Threed": [["Threed Teleport"]],
"Saturn Valley": [["Saturn Valley Teleport"]],
"Dusty Dunes Desert": [["Dusty Dunes Teleport"]],
"Fourside": [["Fourside Teleport"]],
"Winters": [["Winters Teleport"]],
"Summers": [["Summers Teleport"]],
"Dalaam": [["Dalaam Teleport"]],
"Scaraba": [["Scaraba Teleport"]],
"Deep Darkness": [["Deep Darkness Teleport"]],
"Tenda Village": [["Tenda Village Teleport"]],
"Lost Underworld": [["Lost Underworld Teleport"]],
"Magicant": [["Magicant Teleport"], ["Magicant Unlock"]]
},
"Northern Onett": {"Onett": [["Nothing"]]},
"Onett":
{"Northern Onett": [["Police Badge"]],
"Twoson": [["Police Badge"]],
giant_step: [["Key to the Shack"]],
arcade: [["Nothing"]],
"Global ATM Access": [["Nothing"]]},
arcade: {arcade: [["Nothing"]]},
"Giant Step": {"Giant Step": [["Nothing"]]},
"Twoson": {"Onett": [["Police Badge"]],
"Peaceful Rest Valley": [["Pencil Eraser"], ["Valley Bridge Repair"]],
"Threed": [["Wad of Bills"], ["Threed Tunnels Clear"]],
"Everdred's House": [["Paula"]],
"Common Condiment Shop": [["Nothing"]],
"Global ATM Access": [["Nothing"]]},
"Everdred's House": {"Everdred's House": [["Nothing"]]},
"Peaceful Rest Valley": {"Twoson": [["Pencil Eraser"], ["Valley Bridge Repair"]],
"Happy-Happy Village": [["Nothing"]]},
"Happy-Happy Village": {"Peaceful Rest Valley": [["Nothing"]],
lilliput_steps: [["Nothing"]],
happy_happy_hq: [["Nothing"]],
"Global ATM Access": [["Nothing"]]},
"Happy-Happy HQ": {"Happy-Happy HQ": [["Nothing"]]},
"Lilliput Steps": {"Lilliput Steps": [["Nothing"]]},
"Threed": {"Twoson": [["Threed Tunnels Clear"]],
"Dusty Dunes Desert": [["Threed Tunnels Clear"]],
"Andonuts Lab Area": [["UFO Engine", "Bad Key Machine"]],
"Threed Underground": [["Zombie Paper"]],
"Boogey Tent": [["Jeff"]],
"Winters": [["UFO Engine", "Bad Key Machine"]],
"Global ATM Access": [["Nothing"]]},
"Boogey Tent": {"Boogey Tent": [["Nothing"]]},
"Threed Underground": {"Grapefruit Falls": [["Nothing"]]},
"Grapefruit Falls": {belch_factory: [["Jar of Fly Honey"]],
"Saturn Valley": [["Nothing"]],
"Threed Underground": [["Nothing"]]},
"Saturn Valley": {"Grapefruit Falls": [["Nothing"]],
"Cave of the Present": [["Meteorite Piece"]],
"Global ATM Access": [["Nothing"]]},
belch_factory: {"Upper Saturn Valley": [["Threed Tunnels Clear"]]},
"Upper Saturn Valley": {"Saturn Valley": [["Nothing"]],
milky_well: [["Nothing"]]},
"Milky Well": {"Milky Well": [["Nothing"]]},
"Dusty Dunes Desert": {"Threed": [["Threed Tunnels Clear"]],
"Monkey Caves": [["King Banana"]],
gold_mine: [["Mining Permit"]],
"Fourside": [["Nothing"]],
"Global ATM Access": [["Nothing"]]},
"Monkey Caves": {"Monkey Caves": [["Nothing"]]},
"Gold Mine": {"Gold Mine": [["Nothing"]]},
"Fourside": {"Dusty Dunes Desert": [["Nothing"]],
monotoli_building: [["Yogurt Dispenser"]],
"Threed": [["Diamond"]],
magnet_hill: [["Signed Banana"]],
"Fourside Dept. Store": [["Jeff"]],
moonside: [["Nothing"]],
"Global ATM Access": [["Nothing"]]},
"Monotoli Building": {"Monotoli Building": [["Nothing"]]},
"Moonside": {"Moonside": [["Nothing"]],
"Global ATM Access": [["Nothing"]]},
"Fourside Dept. Store": {"Fourside Dept. Store": [["Nothing"]]},
"Magnet Hill": {"Magnet Hill": [["Nothing"]]},
"Winters": {"Snow Wood Boarding School": [["Letter For Tony"]],
"Southern Winters": [["Pak of Bubble Gum"]],
"Global ATM Access": [["Nothing"]]},
"Snow Wood Boarding School": {"Snow Wood Boarding School": [["Nothing"]]},
"Southern Winters": {brickroad_maze: [["Nothing"]]},
brickroad_maze: {rainy_circle: [["Nothing"]],
"Southern Winters": [["Nothing"]],
brickroad_maze: [["Nothing"]]},
rainy_circle: {rainy_circle: [["Nothing"]],
"Andonuts Lab Area": [["Nothing"]],
brickroad_maze: [["Nothing"]]},
"Andonuts Lab Area": {rainy_circle: [["Nothing"]],
stonehenge_base: [["Eraser Eraser"]],
"Winters": [["Nothing"]]},
"Stonehenge Base": {"Stonehenge Base": [["Nothing"]]},
"Summers": {"Scaraba": [["Nothing"]],
"Summers Museum": [["Tiny Ruby"]],
"Global ATM Access": [["Nothing"]]},
"Summers Museum": {"Summers Museum": [["Nothing"]]},
"Dalaam": {pink_cloud: [["Carrot Key"]]},
"Pink Cloud": {"Pink Cloud": [["Nothing"]]},
"Scaraba": {pyramid: [["Hieroglyph Copy"]],
"Common Condiment Shop": [["Nothing"]],
"Global ATM Access": [["Nothing"]]},
pyramid: {"Southern Scaraba": [["Nothing"]]},
"Southern Scaraba": {dungeon_man: [["Key to the Tower"]]},
"Dungeon Man": {"Deep Darkness": [["Submarine to Deep Darkness"]]},
"Deep Darkness": {"Deep Darkness Darkness": [["Hawk Eye"]]},
"Deep Darkness Darkness": {"Tenda Village": [["Nothing"]],
"Deep Darkness": [["Nothing"]]},
"Tenda Village": {lumine_hall: [["Shyness Book"]],
"Deep Darkness Darkness": [["Hawk Eye"]]},
"Lumine Hall": {"Lost Underworld": [["Nothing"]]},
"Lost Underworld": {fire_spring: [["Nothing"]]},
"Fire Spring": {"Fire Spring": [["Nothing"]]},
"Magicant": {sea_of_eden: [["Ness"]],
"Global ATM Access": [["Nothing"]]},
"Sea of Eden": {"Sea of Eden": [["Nothing"]]},
"Cave of the Present": {"Cave of the Past": [["Power of the Earth"]]},
"Cave of the Past": {"Endgame": [["Paula"]]},
"Endgame": {"Endgame": [["Nothing"]]},
"Common Condiment Shop": {"Common Condiment Shop": [["Nothing"]]},
"Global ATM Access": {"Global ATM Access": [["Nothing"]]}
}
teleports = {
"Onett Teleport": "Onett",
"Twoson Teleport": "Twoson",
"Happy-Happy Village Teleport": "Happy-Happy Village",
"Threed Teleport": "Threed",
"Saturn Valley Teleport": "Saturn Valley",
"Dusty Dunes Teleport": "Dusty Dunes Desert",
"Fourside Teleport": "Fourside",
"Winters Teleport": "Winters",
"Summers Teleport": "Summers",
"Scaraba Teleport": "Scaraba",
"Dalaam Teleport": "Dalaam",
"Deep Darkness Teleport": "Deep Darkness",
"Tenda Village Teleport": "Tenda Village",
"Lost Underworld Teleport": "Lost Underworld",
"Magicant Teleport": "Magicant"
}
if world.options.no_free_sanctuaries:
world.area_rules["Happy-Happy Village"][lilliput_steps] = [["Tiny Key"]]
world.area_rules["Lost Underworld"][fire_spring] = [["Tenda Lavapants"]]
else:
world.area_rules["Happy-Happy Village"][lilliput_steps] = [["Nothing"]]
world.area_rules["Lost Underworld"][fire_spring] = [["Nothing"]]
inventory = {0: ["Nothing"]} # Nothing means no item needed for connection
item_regions = {}
for item in world.multiworld.precollected_items[world.player]:
inventory[0].append(item.name)
unconnected_regions = [world.starting_region, "Ness's Mind"]
world.accessible_regions = [world.starting_region, "Ness's Mind"]
if world.options.random_start_location:
unconnected_regions.append(teleports[world.starting_teleport])
world.accessible_regions.append(teleports[world.starting_teleport])
world.scaled_area_order = []
passed_connections = []
local_prog = []
ness_scaled = False
paula_scaled = False
jeff_scaled = False
poo_scaled = False
badge_scaled = False
scaled_chars = {
"Ness": ness_scaled,
"Paula": paula_scaled,
"Jeff": jeff_scaled,
"Poo": poo_scaled
}
sphere_count = 0
last_region = "Ness's Mind"
regions_that_were_already_scaled = []
early_regions = []
world.Ness_region = "Ness's Mind"
world.Paula_region = "Ness's Mind"
world.Jeff_region = "Ness's Mind"
world.Poo_region = "Ness's Mind"
world.Badge_region = "Ness's Mind"
for item in world.multiworld.precollected_items[world.player]:
if item.name in ["Ness", "Paula", "Jeff", "Poo"]:
scaled_chars[item.name] = True
if item.name == "Franklin Badge":
badge_scaled = True
for num, sphere in enumerate(world.multiworld.earthbound_locations_by_sphere):
if num + 1 not in inventory:
inventory[num + 1] = []
for location in sorted(sphere):
if num == 0:
if location.parent_region.name not in world.accessible_regions and location.player == world.player:
early_regions.append(location.parent_region.name)
world.accessible_regions.append(location.parent_region.name)
unconnected_regions.append(location.parent_region.name)
if location.item.player == world.player and location.item.advancement:
inventory[num + 1].append(location.item.name)
if location.player == world.player:
local_prog.append(location.item.name)
if location.item.name not in item_regions:
item_regions[location.item.name] = []
item_regions[location.item.name].append(location.parent_region.name)
# TODO; all areas have levels now, so I can skip the combat regions check
if location.player == world.player and location.parent_region.name in combat_regions and (
location.parent_region.name not in regions_that_were_already_scaled):
last_region = location.parent_region.name
regions_that_were_already_scaled.append(last_region)
if location.item.player == world.player and location.item.name == "Ness" and not scaled_chars["Ness"]:
if location.parent_region.name in combat_regions and (location.player == world.player) and (
location.name not in locations_with_item_requirements):
world.Ness_region = location.parent_region.name
else:
world.Ness_region = last_region
scaled_chars["Ness"] = True
if location.item.player == world.player and location.item.name == "Paula" and not scaled_chars["Paula"]:
if location.parent_region.name in combat_regions and (location.player == world.player) and (
location.name not in locations_with_item_requirements):
world.Paula_region = location.parent_region.name
else:
world.Paula_region = last_region
scaled_chars["Paula"] = True
if location.item.player == world.player and location.item.name == "Jeff" and not scaled_chars["Jeff"]:
if location.parent_region.name in combat_regions and (location.player == world.player) and (
location.name not in locations_with_item_requirements):
world.Jeff_region = location.parent_region.name
else:
world.Jeff_region = last_region
scaled_chars["Jeff"] = True
if location.item.player == world.player and location.item.name == "Poo" and not scaled_chars["Poo"]:
if location.parent_region.name in combat_regions and (location.player == world.player) and (
location.name not in locations_with_item_requirements):
world.Poo_region = location.parent_region.name
else:
world.Poo_region = last_region
scaled_chars["Poo"] = True
if location.item.player == world.player and location.item.name == "Franklin Badge" and not badge_scaled:
if location.parent_region.name in combat_regions and (location.player == world.player) and (
location.name not in locations_with_item_requirements):
world.Badge_region = location.parent_region.name
else:
world.Badge_region = last_region
badge_scaled = True
sphere_count = num
for item in range(1, len(inventory)):
if item in inventory:
inventory[item] = inventory[item - 1] + inventory[item]
else:
inventory[item] = inventory[item - 1]
for i in range(sphere_count):
# Ness's mind needs to be calculated last, always. (Players are more likely to walk around
# and explore areas than suddenly leave with a teleport)
# Shuffle it to the end of the list on each loop so it gets deprioritized
# Is there a better way to do this?
if "Ness's Mind" in unconnected_regions:
unconnected_regions.remove("Ness's Mind")
unconnected_regions.append("Ness's Mind") # probably do this differently earlier
for region in unconnected_regions:
for connection in world.area_exits[region]:
if f"{region} -> {connection}" not in passed_connections:
for rule_set in world.area_rules[region][connection]:
# check if this sphere has the items needed to make this connection
if all(item in inventory[i] for item in rule_set):
passed_connections.append(f"{region} -> {connection}")
if connection not in world.accessible_regions:
world.accessible_regions.append(connection)
unconnected_regions.append(connection)
else:
world.area_exits[region].remove(connection)
if "Endgame" in unconnected_regions:
unconnected_regions.remove("Endgame")
unconnected_regions.insert(0, "Endgame")
for region in world.multiworld.get_regions(world.player):
if region.name not in world.accessible_regions and region.name != "Menu":
world.accessible_regions.append(region.name)
if world.options.magicant_mode == MagicantMode.option_alternate_goal and world.options.giygas_required:
# If magicant is an alternate goal it should be scaled after Giygas
world.accessible_regions.remove("Magicant")
world.accessible_regions.append("Sea of Eden")
world.accessible_regions.insert(world.accessible_regions.index("Endgame") + 1, "Magicant")
elif world.options.magicant_mode == MagicantMode.option_optional_boost and world.options.giygas_required:
world.accessible_regions.insert(world.accessible_regions.index("Endgame") - 1, "Magicant")
elif world.options.magicant_mode == MagicantMode.option_optional_boost and not world.options.giygas_required:
# Just add it to the end of scaling
world.accessible_regions.append("Magicant")
world.accessible_regions.append("Sea of Eden")
# calculate which areas need to have enemies scaled
for region in world.accessible_regions:
if region in world.regional_enemies:
world.scaled_area_order.append(region)
current_level = 1
world.area_levels = {}
for region in world.accessible_regions:
world.area_levels[region] = current_level
current_level += expected_level_gains[region]
if world.Ness_region == "Ness's Mind":
world.Ness_region = world.scaled_area_order[0]
if world.Paula_region == "Ness's Mind":
world.Paula_region = world.scaled_area_order[0]
if world.Jeff_region == "Ness's Mind":
world.Jeff_region = world.scaled_area_order[0]
if world.Poo_region == "Ness's Mind":
world.Poo_region = world.scaled_area_order[0]
if world.Badge_region == "Ness's Mind":
world.Badge_region = world.scaled_area_order[0]

View File

@@ -0,0 +1,411 @@
from typing import NamedTuple, TYPE_CHECKING
from logging import warning
import struct
if TYPE_CHECKING:
from .. import EarthBoundWorld
from ..Rom import LocalRom
import struct
boss_sprite_pointers = {
"Frank": 0xEF2B69,
"Frankystein Mark II": 0xEF43F9,
"Titanic Ant": 0xEF3B57,
"Captain Strong": 0xEF23A9,
"Everdred": 0xEF2BCD,
"Mr. Carpainter": 0xEF2BFF,
"Mondo Mole": 0xEF4557,
"Boogey Tent": 0xEF3748,
"Mini Barf": 0xEF3B89,
"Master Belch": 0xEF3CD6,
"Trillionage Sprout": 0xEF3BBB,
"Guardian Digger": 0xEF45BB,
"Dept. Store Spook": 0xEF495F,
"Evil Mani-Mani": 0xEF395F,
"Clumsy Robot": 0xEF45A2,
"Shrooom!": 0xEF392B,
"Plague Rat of Doom": 0xEF4570,
"Thunder and Storm": 0xEF3C70,
"Kraken": 0xEF3991,
"Guardian General": 0xEF3C3C,
"Master Barf": 0xEF39F5,
"Starman Deluxe": 0xEF3A59,
"Electro Specter": 0xEF45ED,
"Carbon Dog": 0xEF3D08,
"Ness's Nightmare": 0xEF395F,
"Heavily Armed Pokey": 0xEF49AA,
"Starman Junior": 0xEF3A59,
"Diamond Dog": 0xEF3D08,
"Giygas": 0xEF40F2
}
boss_plando_keys = {
"Frank",
"Frankystein Mark II",
"Frankystein",
"Captain Strong",
"Strong",
"Everdred",
"Mr. Carpainter",
"Mr Carpainter",
"Carpainter",
"Mondo Mole",
"Boogey Tent",
"Mini Barf",
"Master Belch",
"Belch",
"Trillionage Sprout",
"Guardian Digger",
"Dept. Store Spook",
"Dept Store Spook",
"Evil Mani Mani",
"Mani Mani",
"Clumsy Robot",
"Shrooom!",
"Shrooom",
"Shroom!",
"Shroooooom!",
"Shroom",
"Plague Rat of Doom",
"Thunder and Storm",
"Kraken",
"The Kraken",
"Guardian General",
"Master Barf",
"Starman Deluxe",
"Starman DX",
"Electro Specter",
"Carbon Dog",
"Ness's Nightmare",
"Nesss Nightmare",
"Heavily Armed Pokey",
"Pokey"
"Starman Junior",
"Diamond Dog",
"Giygas"
}
boss_typo_key = {
"Frankystein": "Frankystein Mark II",
"Strong": "Captain Strong",
"Mr Carpainter": "Mr. Carpainter",
"Carpainter": "Mr. Carpainter",
"Belch": "Master Belch",
"Dept Store Spook": "Dept. Store Spook",
"Evil Mani Mani": "Evil Mani-Mani",
"Mani Mani": "Evil Mani-Mani",
"Shroom": "Shrooom!",
"Shrooom": "Shrooom!",
"Shroom!": "Shrooom!",
"Shroooooom!": "Shrooom!",
"The Kraken": "Kraken",
"Starman DX": "Starman Deluxe",
"Nesss Nightmare": "Ness's Nightmare",
"Pokey": "Heavily Armed Pokey"
}
banned_transformations = ["Master Belch", "Master Barf", "Kraken", "Heavily Armed Pokey"]
hard_final_bosses = ["Carbon Dog", "Kraken", "Clumsy Robot", "Starman Junior", "Starman Deluxe", "Giygas", "Thunder and Storm", "Electro Specter",
"Evil Mani-Mani", "Ness's Nightmare", "Shrooom!", "Master Belch"]
class SlotInfo(NamedTuple):
sprite_addrs: list[int]
short_names: list[int]
long_names: list[int]
battle_data: list[int]
class BossData(NamedTuple):
sprite_pointer: int
short_name_pointer: int
long_name_pointer: int
battle_group: int
enemy_id: int
music: int
def initialize_bosses(world: "EarthBoundWorld") -> None:
from ..Options import BossShuffle
world.boss_list = [
"Frank",
"Frankystein Mark II",
"Titanic Ant",
"Captain Strong",
"Everdred",
"Mr. Carpainter",
"Mondo Mole",
"Boogey Tent",
"Mini Barf",
"Master Belch",
"Trillionage Sprout",
"Guardian Digger",
"Dept. Store Spook",
"Evil Mani-Mani",
"Clumsy Robot",
"Shrooom!",
"Plague Rat of Doom",
"Thunder and Storm",
"Kraken",
"Guardian General",
"Master Barf",
"Starman Deluxe",
"Electro Specter",
"Carbon Dog",
"Ness's Nightmare",
"Heavily Armed Pokey",
"Starman Junior",
"Diamond Dog",
"Giygas"
]
world.boss_slots = {
"Frank": SlotInfo([0x0F9338],
[0x066111, 0x066198, 0x0661AC],
[0x065F11, 0x065F20, 0x066482, 0x0660C5, 0x0746E2, 0x074BC1, 0x074E1D],
[0x0683FF]),
"Frankystein Mark II": SlotInfo([0x0F96F0], [], [0x066146, 0x06648B, 0x0664FC], [0x068406]),
"Titanic Ant": SlotInfo([], [], [], [0x06840D]),
"Captain Strong": SlotInfo([], [0x5FC2B, 0x05FCF7, 0x065F88, 0x066085],
[0x05FC59, 0x3317DB], [0x068468]),
"Everdred": SlotInfo([0x0F9A64, 0x0F9FB4], [0x2EEEEA], [0x095C70], [0x06846F]),
"Mr. Carpainter": SlotInfo([0x0FA27E, 0x0FA5D0],
[0x0990DA, 0x0684D0],
[0x0993DB, 0x09945E, 0x099311, 0x099364, 0x098EF6, 0x099143, 0x099028,
0x0983BB, 0x09840C, 0x09835B, 0x09056F, 0x0794EC],
[0x0684FD]),
"Mondo Mole": SlotInfo([], [], [], [0x068414]),
"Boogey Tent": SlotInfo([0x0FACEB], [], [], [0x06853C]),
"Mini Barf": SlotInfo([0x0FB0B4], [], [], [0x2F9515]),
"Master Belch": SlotInfo([0x0FB7CF],
[0x09E64D, 0x09E690, 0x2EEED7, 0x08EF21, 0x08EF38],
[0x2F6297, 0x2F62B3, 0x2F6910, 0x2F6973],
[0x068558]),
"Trillionage Sprout": SlotInfo([], [], [], [0x068422]),
"Guardian Digger": SlotInfo([0x0FC11B, 0x0FC0B5, 0x0FC12C, 0x0FC0D7, 0x0FC0C6],
[],
[],
[0x06858E, 0x068595, 0x06859C, 0x0685A3, 0x0685AA]),
"Dept. Store Spook": SlotInfo([0x0FC803], [], [], [0x06855F]),
"Evil Mani-Mani": SlotInfo([0x0FE6E4], [], [0x0978AD, 0x09782D, 0x097998], [0x068587]),
"Clumsy Robot": SlotInfo([0x0FC429], [], [], [0x06856D]),
"Shrooom!": SlotInfo([], [], [], [0x06841B]),
"Plague Rat of Doom": SlotInfo([], [], [], [0x068429]),
"Thunder and Storm": SlotInfo([], [], [], [0x068430]),
"Kraken": SlotInfo([0x092CD0, 0x0FE370, 0x0FE381, 0x0FE392, 0x092D13],
[0x092D4D],
[0x086061, 0x086139, 0x08B430, 0x08B6FC, 0x08B8B4, 0x08B591, 0x09AB2B],
[0x0685B1, 0x2F9472, 0x2F9491, 0x2F94B0]),
"Guardian General": SlotInfo([0x0FD7E2], [], [], [0x2F9453]),
"Master Barf": SlotInfo([0x0FDB23], [], [], [0x068574]),
"Starman Deluxe": SlotInfo([0x0FB626], [], [0x092C29], [0x2F942F]),
"Electro Specter": SlotInfo([], [], [], [0x068437]),
"Carbon Dog": SlotInfo([], [], [], [0x06843E]),
"Ness's Nightmare": SlotInfo([0x0FE3B4], [], [], [0x068580]),
"Heavily Armed Pokey": SlotInfo([0x09C2EC], [0x2EEEC3, 0x2EEECC], [], []),
"Starman Junior": SlotInfo([], [], [], []),
"Diamond Dog": SlotInfo([], [], [], []),
"Giygas": SlotInfo([0x09C2BF, 0x09C2E5], [0x2EF0A9], [0x2EF09F], [])
}
world.boss_info = {
"Frank": BossData(0x0099, 0xEEEEBC, 0xEEEEBC, 0x01C0, 0x83, 0x64),
"Frankystein Mark II": BossData(0x0191, 0xEEEF0A, 0xEEEEF6, 0x01C1, 0x82, 0x66),
"Titanic Ant": BossData(0x0139, 0xEEEF1E, 0xEEEF16, 0x01C2, 0x25, 0x67),
"Captain Strong": BossData(0x004B, 0xEEEF2A, 0xEEEF22, 0x01C4, 0xE4, 0x66),
"Everdred": BossData(0x009D, 0xEEEF31, 0xEEEF31, 0x01C5, 0x6E, 0x62),
"Mr. Carpainter": BossData(0x009F, 0xEEEF3E, 0xEEEF3A, 0x01C6, 0x1A, 0x94),
"Mondo Mole": BossData(0x019F, 0xEEEF4F, 0xEEEF49, 0x01C7, 0x29, 0x67),
"Boogey Tent": BossData(0x0110, 0xEEEF5B, 0xEEEF54, 0x01CA, 0x66, 0x66),
"Mini Barf": BossData(0x013B, 0xEEEF65, 0xEEEF60, 0x01E2, 0xE2, 0x63),
"Master Belch": BossData(0x0148, 0xEEEF71, 0xEEEF6A, 0x01C8, 0x5D, 0x63),
"Trillionage Sprout": BossData(0x013D, 0xEEEF83, 0xEEEF77, 0x01C9, 0x5A, 0x67),
"Guardian Digger": BossData(0x01A3, 0xEEEF93, 0xEEEF8A, 0x01CB, 0x2A, 0x67),
"Dept. Store Spook": BossData(0x01C7, 0xEEEFA6, 0xEEEF9A, 0x01CC, 0x02, 0x66),
"Evil Mani-Mani": BossData(0x0125, 0xEEEFC1, 0xEEEFAC, 0x01CD, 0x89, 0x94),
"Clumsy Robot": BossData(0x01A2, 0xEEEFD2, 0xEEEFCB, 0x01CE, 0x92, 0x94),
"Shrooom!": BossData(0x0123, 0xEEEFD8, 0xEEEFD8, 0x01D1, 0x27, 0x67),
"Plague Rat of Doom": BossData(0x01A0, 0xEEEFEE, 0xEEEFE0, 0x01CF, 0x28, 0x67),
"Thunder and Storm": BossData(0x0144, 0xEEEFFF, 0xEEEFF3, 0x01D0, 0x80, 0x68),
"Kraken": BossData(0x0127, 0xEEF005, 0xEEF005, 0x01D3, 0x31, 0x68),
"Guardian General": BossData(0x0142, 0xEEF015, 0xEEF00C, 0x01D4, 0x49, 0x67),
"Master Barf": BossData(0x012B, 0xEEEF65, 0xEEF01D, 0x01D5, 0x5F, 0x63),
"Starman Deluxe": BossData(0x012F, 0xEEF034, 0xEEF029, 0x01D2, 0x4A, 0x61),
"Electro Specter": BossData(0x01A5, 0xEEF043, 0xEEF03B, 0x01D6, 0x74, 0x68),
"Carbon Dog": BossData(0x014A, 0xEEF052, 0xEEF04B, 0x01D7, 0x1B, 0x67),
"Ness's Nightmare": BossData(0x0125, 0xEEF070, 0xEEF06A, 0x01D8, 0x15, 0x94),
"Heavily Armed Pokey": BossData(0x01CA, 0xEEF064, 0xEEF056, 0x000E, 0xD8, 0x69),
"Starman Junior": BossData(0x012F, 0xEEF082, 0xEEF07A, 0x01DA, 0xD6, 0x94),
"Diamond Dog": BossData(0x014A, 0xEEF052, 0xEEF089, 0x01D9, 0x53, 0x61),
"Giygas": BossData(0x0172, 0xEEF095, 0xEEF095, 0x01DD, 0xDC, 0x49)
}
if world.options.skip_prayer_sequences:
# Boss shuffle sprites needs to apply to the skip prayer cleanup too
world.boss_slots["Giygas"].sprite_addrs.append(0x07B9AC)
world.boss_slots["Heavily Armed Pokey"].sprite_addrs.append(0x07B9A7)
# mole/rat text
# todo; Giygas sprites/text
world.boss_slot_order = world.boss_list.copy()
if type(world.options.boss_shuffle.value) == str:
boss_plando = world.options.boss_shuffle.value.split(";")
shuffle_result = boss_plando.pop()
else:
boss_plando = []
shuffle_result = world.options.boss_shuffle.value
if shuffle_result == "true" or shuffle_result == 1:
world.random.shuffle(world.boss_list)
if not world.options.decouple_diamond_dog:
world.boss_list.remove("Diamond Dog")
insert_index = 28 if not world.options.boss_shuffle_add_giygas else 27
world.boss_list.insert(insert_index, "Diamond Dog")
if not world.options.boss_shuffle_add_giygas:
world.boss_list.remove("Giygas")
world.boss_list.insert(29, "Giygas")
if world.options.safe_final_boss:
while world.boss_list[25] in hard_final_bosses:
i = world.random.randrange(len(world.boss_list))
if (world.boss_list[i] == "Diamond Dog" and not world.options.decouple_diamond_dog) or (
world.boss_list[i] == "Giygas" and not world.options.boss_shuffle_add_giygas
):
continue
world.boss_list[25], world.boss_list[i] = world.boss_list[i], world.boss_list[25]
did_plando_diam_dog = False
diamond_dog_plando_slot = None
for item in boss_plando:
boss_block = item.split("-")
boss = boss_block[0].title() # Boss is what's being placed
slot = boss_block[1].title() # SLot is where the boss is going.
if boss in boss_typo_key:
boss = boss_typo_key[boss]
if slot in boss_typo_key:
slot = boss_typo_key[boss]
if slot == "Diamond Dog":
did_plando_diam_dog = True
diamond_dog_plando_slot = boss
old_index = world.boss_list.index(boss) # This should be the slot where the chosen boss currently is
new_index = world.boss_slot_order.index(slot) # Boss slots should use the original position
world.boss_list[old_index] = world.boss_list[new_index] # We want to replace the boss that was originally there with the boss we're swapping with
world.boss_list[new_index] = boss
if world.boss_list[25] == "Carbon Dog" and world.boss_list[27] in banned_transformations:
if did_plando_diam_dog:
warning(f"""Unable to plando {diamond_dog_plando_slot} for {world.multiworld.get_player_name(world.player)}'s EarthBound world.
This boss cannot be placed onto Diamond Dog's slot if Carbon Dog is on Heavily Armed Pokey's slot.
This message is likely the result of randomization and can be safely ignored.""") # Why is this spacing the only way to get the message to render legibly
original_boss = world.boss_list[27]
transformation_replacement = world.random.randint(0, 24)
while world.boss_list[transformation_replacement] in banned_transformations:
transformation_replacement = world.random.randint(0, 24)
world.boss_list[27] = world.boss_list[transformation_replacement]
world.boss_list[transformation_replacement] = original_boss
def write_bosses(world: "EarthBoundWorld", rom: "LocalRom") -> None:
rom.write_bytes(0x15E527, bytearray([0x00, 0x00])) # Blank out Pokey's end battle action
rom.write_bytes(0x15B8B9, bytearray([0x00, 0x00]))
rom.write_bytes(0x15DD13, bytearray([0x00, 0x00])) # Blank out barf's end battle script
rom.write_bytes(0x15E69F, bytearray([0x00, 0x00])) # Blank giygas
if world.boss_list[25] == "Carbon Dog": # Heavily armed Pokey
pokey_adjust = 27
else:
pokey_adjust = 25
rom.write_bytes(world.enemies[world.boss_list[pokey_adjust]].address + 78, bytearray([0x13, 0x01]))
for i in range(1, world.enemies[world.boss_list[pokey_adjust]].attack_extensions):
enemy_new = f"{world.enemies[world.boss_list[pokey_adjust]].name} ({i + 1})"
rom.write_bytes(world.enemies[enemy_new].address + 78, bytearray([0x13, 0x01]))
if world.boss_list[20] == "Carbon Dog": # Master Barf
barf_adjust = 27
else:
barf_adjust = 20
rom.write_bytes(world.enemies[world.boss_list[barf_adjust]].address + 78, bytearray([0xF4, 0x00]))
for i in range(1, world.enemies[world.boss_list[barf_adjust]].attack_extensions):
enemy_new = f"{world.enemies[world.boss_list[barf_adjust]].name} ({i + 1})"
rom.write_bytes(world.enemies[enemy_new].address + 78, bytearray([0xF4, 0x00]))
if world.boss_list[28] == "Carbon Dog": # Giygas 2
# I should probably just hard stop Carbon Dog from being here
giygas_2_adjust = 27 # Set to the diamond dog slot
else:
giygas_2_adjust = 28
rom.write_bytes(world.enemies[world.boss_list[giygas_2_adjust]].address + 78, bytearray([0x16, 0x01]))
for i in range(1, world.enemies[world.boss_list[giygas_2_adjust]].attack_extensions):
enemy_new = f"{world.enemies[world.boss_list[giygas_2_adjust]].name} ({i + 1})"
rom.write_bytes(world.enemies[enemy_new].address + 78, bytearray([0x16, 0x01]))
if world.boss_list[25] != "Heavily Armed Pokey":
rom.write_bytes(0x15E50A, bytearray([0x19, 0x6E, 0xEF]))
rom.write_bytes(0x15E4FE, bytearray([0x70, 0x11, 0x01])) # Add to the scaling list?
for slot, boss in enumerate(world.boss_slot_order):
for address in world.boss_slots[boss].sprite_addrs: # sprite
rom.write_bytes(address, struct.pack("H", world.boss_info[world.boss_list[slot]].sprite_pointer))
for address in world.boss_slots[boss].short_names: # short name
rom.write_bytes(address, struct.pack("I", world.boss_info[world.boss_list[slot]].short_name_pointer))
for address in world.boss_slots[boss].long_names: # long name
rom.write_bytes(address, struct.pack("I", world.boss_info[world.boss_list[slot]].long_name_pointer))
for address in world.boss_slots[boss].battle_data: # battle
rom.write_bytes(address, struct.pack("H", world.boss_info[world.boss_list[slot]].battle_group))
rom.write_bytes(0x10DF7F, struct.pack("H", world.boss_info[world.boss_list[25]].enemy_id))
rom.write_bytes(0x10DF86, struct.pack("H", world.boss_info[world.boss_list[25]].enemy_id))
# rom.write_bytes(0x10DF8D, struct.pack("H", world.boss_info[world.boss_list[25]].enemy_id))
rom.write_bytes(0x10DFA2, struct.pack("H", world.boss_info[world.boss_list[25]].enemy_id))
rom.write_bytes(0x10D563, struct.pack("H", world.boss_info[world.boss_list[25]].enemy_id))
rom.write_bytes(world.enemies[world.boss_list[25]].address + 91, bytearray([0x00])) # Row of the enemy
rom.write_bytes(0x10DF83, struct.pack("H", world.boss_info[world.boss_list[28]].enemy_id))
rom.write_bytes(0x02C4FD, struct.pack("H", world.boss_info[world.boss_list[28]].enemy_id))
rom.write_bytes(0x10D560, struct.pack("H", world.boss_info[world.boss_list[28]].enemy_id))
rom.write_bytes(0x159FC7, struct.pack("H", world.boss_info[world.boss_list[27]].enemy_id))
rom.write_bytes(0x15D5C1, struct.pack("H", world.boss_info[world.boss_list[27]].enemy_id))
# carbon dog's transformation
rom.write_bytes(0x10DF69, struct.pack("H", world.boss_info[world.boss_list[27]].enemy_id))
rom.write_bytes(0x02C503, bytearray([world.boss_info[world.boss_list[28]].music])) # music
rom.write_bytes(0x2F188F, struct.pack("I", boss_sprite_pointers[world.boss_list[3]]))
rom.write_bytes(0x0302CE, struct.pack("H", 0x0154))
rom.write_bytes(0x05F870, struct.pack("H", 0x0154))
rom.write_bytes(0x0F8E3D, struct.pack("H", 0x0154))
rom.write_bytes(0x05F886, struct.pack("H", 0x0154))
rom.write_bytes(0x05F8A1, struct.pack("H", 0x0154))
rom.write_bytes(0x05F8E3, struct.pack("H", 0x0154))
rom.write_bytes(0x05FB0E, struct.pack("H", 0x0154))
rom.write_bytes(0x05FBFC, struct.pack("H", 0x0154))
rom.write_bytes(0x05FD08, struct.pack("H", 0x0154))
rom.write_bytes(0x05FD5C, struct.pack("H", 0x0154))
rom.copy_bytes(0x10DF7B, 6, 0x2FFF10)
if world.boss_list[25] == "Carbon Dog":
rom.write_bytes(0x2FFF16, bytearray([0x00])) # Count of enemies
rom.write_bytes(0x2FFF17, struct.pack("H", world.boss_info[world.boss_list[27]].enemy_id)) # Add diamond dog
rom.write_bytes(0x2FFF19, bytearray([0xFF]))
rom.write_bytes(world.enemies[world.boss_list[27]].address + 91, bytearray([0x00])) # Force to front row
elif world.boss_list[25] == "Giygas":
rom.write_bytes(0x0121DF, bytearray([0x00]))
rom.write_bytes(0x2FFF16, bytearray([0xFF]))
else:
rom.write_bytes(0x2FFF16, bytearray([0xFF]))
# c2c505 sets the song

View File

@@ -0,0 +1,212 @@
import struct
from typing import Optional, TYPE_CHECKING
from dataclasses import dataclass
from ..game_data.local_data import item_id_table
from typing import Optional, TYPE_CHECKING
from dataclasses import dataclass
from ..game_data.local_data import item_id_table
if TYPE_CHECKING:
from .. import EarthBoundWorld
from ..Rom import LocalRom
@dataclass
class EBDungeonDoor:
address: int
copyaddress: int
direction: int
is_script: bool = False # Script warps invert the x and y coordinate
def shuffle_dungeons(world: "EarthBoundWorld") -> None:
# Is the dept. store a dungeon
single_exit_dungeons = [
"Giant Step",
"Happy-Happy HQ",
"Lilliput Steps",
"Milky Well",
"Gold Mine",
"Moonside",
"Monotoli Building",
"Magnet Hill",
"Pink Cloud",
"Dungeon Man",
"Stonehenge Base",
"Lumine Hall",
"Fire Spring",
"Sea of Eden"
]
double_exit_dungeons = [
"Arcade",
"Brickroad Maze",
"Rainy Circle",
"Belch's Factory",
"Pyramid"
]
world.dungeon_connections = {
"Arcade": "Arcade",
"Giant Step": "Giant Step",
"Happy-Happy HQ": "Happy-Happy HQ",
"Lilliput Steps": "Lilliput Steps",
"Belch's Factory": "Belch's Factory",
"Milky Well": "Milky Well",
"Gold Mine": "Gold Mine",
"Moonside": "Moonside",
"Monotoli Building": "Monotoli Building",
"Magnet Hill": "Magnet Hill",
"Pink Cloud": "Pink Cloud",
"Dungeon Man": "Dungeon Man",
"Stonehenge Base": "Stonehenge Base",
"Brickroad Maze": "Brickroad Maze",
"Rainy Circle": "Rainy Circle",
"Pyramid": "Pyramid",
"Lumine Hall": "Lumine Hall",
"Fire Spring": "Fire Spring",
"Sea of Eden": "Sea of Eden"
}
if world.options.magicant_mode:
# Don't shuffle Magicant when it's important
single_exit_dungeons.remove("Sea of Eden")
shuffled_single_dungeons = single_exit_dungeons.copy()
shuffled_double_dungeons = double_exit_dungeons.copy()
if world.options.dungeon_shuffle:
world.random.shuffle(shuffled_single_dungeons)
world.random.shuffle(shuffled_double_dungeons)
for index, entrance in enumerate(single_exit_dungeons):
world.dungeon_connections[entrance] = shuffled_single_dungeons[index]
for index, entrance in enumerate(double_exit_dungeons):
world.dungeon_connections[entrance] = shuffled_double_dungeons[index]
def write_dungeon_entrances(world: "EarthBoundWorld", rom: "LocalRom") -> None:
dungeon_entrances = {
"Arcade": ["Arcade Entrance", "Arcade Exit", "Arcade Back Exit", "Arcade Back Entrance"],
"Giant Step": ["Giant Step Entrance", "Giant Step Exit"],
"Happy-Happy HQ": ["Happy-Happy HQ Entrance", "Happy-Happy HQ Exit"],
"Lilliput Steps": ["Lilliput Steps Entrance", "Lilliput Steps Exit"],
"Belch's Factory": ["Factory Script Warp", "Factory Exit", "Factory Back Exit", "Factory Back Entrance"],
"Milky Well": ["Milky Well Entrance", "Milky Well Exit"],
"Gold Mine": ["Mine Entrance", "Mine Exit"],
"Monotoli Building": ["Monotoli Entrance", "Monotoli Exit"],
"Moonside": ["Cafe Entrance", "Cafe Exit"],
"Brickroad Maze": ["Maze Entrance", "Maze Exit", "Maze Back Exit", "Maze Back Entrance"],
"Rainy Circle": ["Rainy Entrance", "Rainy Exit", "Rainy Back Exit", "Rainy Back Entrance"],
"Magnet Hill": ["Sewer Entrance", "Sewer Exit"],
"Pink Cloud": ["Pink Cloud Entrance", "Pink Cloud Exit"],
"Pyramid": ["Pyramid Entrance", "Pyramid Exit", "Pyramid Back Exit", "Pyramid Back Entrance"],
"Dungeon Man": ["D.M. Entrance Script", "D.M. Exit Script"],
"Stonehenge Base": ["Stonehenge Entrance", "Stonehenge Exit"],
"Lumine Hall": ["Lumine Entrance", "Lumine Exit"],
"Fire Spring": ["Fire Spring Entrance", "Fire Spring Exit"],
"Sea of Eden": ["Sea Entrance Script", "Sea Exit Script"]
}
all_dungeon_doors = {
"Arcade Entrance": EBDungeonDoor(0x0F00CC, 0x321000, 7),
"Arcade Exit": EBDungeonDoor(0x0F029A, 0x321010, 5),
"Arcade Back Exit": EBDungeonDoor(0x0F026E, 0x321020, 1),
"Arcade Back Entrance": EBDungeonDoor(0x0F00C1, 0x321030, 5),
"Giant Step Entrance": EBDungeonDoor(0x0F0032, 0x321040, 7),
"Giant Step Exit": EBDungeonDoor(0x0F04B5, 0x321050, 5),
"Happy-Happy HQ Entrance": EBDungeonDoor(0x0F09E9, 0x321060, 7),
"Happy-Happy HQ Exit": EBDungeonDoor(0x0F0A99, 0x321070, 5),
"Lilliput Steps Entrance": EBDungeonDoor(0x0F09F4, 0x321080, 3),
"Lilliput Steps Exit": EBDungeonDoor(0x0F0B1A, 0x321090, 7),
"Factory Entrance": EBDungeonDoor(0x0F1277, 0x3210A0, 5),
"Factory Script Warp": EBDungeonDoor(0x15EECB, 0x3210B0, 5, True),
"Factory Exit": EBDungeonDoor(0x0F1159, 0x3210C0, 5),
"Factory Back Exit": EBDungeonDoor(0x0F11BC, 0x3210D0, 5),
"Factory Back Entrance": EBDungeonDoor(0x0F11FE, 0x3210E0, 3),
"Milky Well Entrance": EBDungeonDoor(0x0F12E9, 0x3210F0, 3),
"Milky Well Exit": EBDungeonDoor(0x0F11E8, 0x321100, 5),
"Mine Entrance": EBDungeonDoor(0x0F1378, 0x321110, 7),
"Mine Exit": EBDungeonDoor(0x0F1400, 0x321120, 5),
"Cafe Entrance": EBDungeonDoor(0x0F165D, 0x321150, 3),
"Cafe Exit": EBDungeonDoor(0x0F1A25, 0x321160, 7),
"Monotoli Entrance": EBDungeonDoor(0x0F1928, 0x321170, 7),
"Monotoli Exit": EBDungeonDoor(0x0F1862, 0x321180, 3),
"Maze Entrance": EBDungeonDoor(0x0F0EB6, 0x321190, 3),
"Maze Exit": EBDungeonDoor(0x0F0FD8, 0x3211A0, 5),
"Maze Back Exit": EBDungeonDoor(0x0F0FE3, 0x3211B0, 5),
"Maze Back Entrance": EBDungeonDoor(0x0F0EC1, 0x3211C0, 7),
"Rainy Entrance": EBDungeonDoor(0x0F0ED7, 0x3211D0, 7),
"Rainy Exit": EBDungeonDoor(0x0F1030, 0x3211E0, 5),
"Rainy Back Exit": EBDungeonDoor(0x0F0FEE, 0x3211F0, 5),
"Rainy Back Entrance": EBDungeonDoor(0x0F0EAB, 0x321200, 3),
"Sewer Entrance": EBDungeonDoor(0x0F1A3B, 0x321210, 5),
"Sewer Exit": EBDungeonDoor(0x0F1A9E, 0x321220, 1),
"Pink Cloud Entrance": EBDungeonDoor(0x0F1E32, 0x321230, 7),
"Pink Cloud Exit": EBDungeonDoor(0x0F1EAB, 0x321240, 5),
"Pyramid Entrance": EBDungeonDoor(0x0F1F3A, 0x321250, 3),
"Pyramid Exit": EBDungeonDoor(0x0F1FA9, 0x321260, 5),
"Pyramid Back Exit": EBDungeonDoor(0x0F20E8, 0x321290, 5),
"Pyramid Back Entrance": EBDungeonDoor(0x0F1F45, 0x3212A0, 3),
"D.M. Entrance Script": EBDungeonDoor(0x15F0A3, 0x321270, 7, True),
"D.M. Exit Script": EBDungeonDoor(0x15F0CB, 0x321280, 5, True),
"Stonehenge Entrance": EBDungeonDoor(0x0F105C, 0x3212B0, 7),
"Stonehenge Exit": EBDungeonDoor(0x0F1072, 0x3212C0, 3),
"Lumine Entrance": EBDungeonDoor(0x0F239C, 0x3212D0, 7),
"Lumine Exit": EBDungeonDoor(0x0F2318, 0x3212E0, 3),
"Fire Spring Entrance": EBDungeonDoor(0x0F23D4, 0x3212F0, 3),
"Fire Spring Exit": EBDungeonDoor(0x0F2437, 0x321300, 5),
"Sea Entrance Script": EBDungeonDoor(0x15F25B, 0x321310, 3, True),
"Sea Exit Script": EBDungeonDoor(0x15ECEB, 0x321320, 5, True),
"Post-Nightmare Script": EBDungeonDoor(0x15ED4B, 0x321330, 5, True),
"Carpainter Failure Script": EBDungeonDoor(0x15EEF3, 0x321340, 7, True)
}
paired_doors = {}
for door in all_dungeon_doors:
rom.copy_bytes(all_dungeon_doors[door].address, 6, all_dungeon_doors[door].copyaddress) # Copy 6 bytes at the source of the door to the destination of the door
for door in world.dungeon_connections:
for index, entrance in enumerate(dungeon_entrances[door]):
if "Exit" in entrance:
paired_doors[dungeon_entrances[world.dungeon_connections[door]][index]] = entrance
else:
paired_doors[entrance] = dungeon_entrances[world.dungeon_connections[door]][index]
paired_doors["Post-Nightmare Script"] = paired_doors["Sea Exit Script"]
paired_doors["Carpainter Failure Script"] = paired_doors["Happy-Happy HQ Exit"]
for door in paired_doors:
destination = all_dungeon_doors[paired_doors[door]]
source = all_dungeon_doors[door]
if source.is_script:
if destination.is_script:
rom.copy_bytes(destination.copyaddress, 6, source.address)
else:
rom.copy_bytes(destination.copyaddress + 2, 2, source.address)
rom.copy_bytes(destination.copyaddress, 2, source.address + 2)
rom.write_bytes(source.address + 4, bytearray([destination.direction]))
rom.copy_bytes(destination.copyaddress + 4, 1, source.address + 5)
else:
if destination.is_script:
rom.copy_bytes(destination.copyaddress + 2, 2, source.address)
rom.copy_bytes(destination.copyaddress, 2, source.address + 2)
rom.copy_bytes(destination.copyaddress + 5, 1, source.address + 4)
else:
rom.copy_bytes(destination.copyaddress, 5, source.address)
rom.write_bytes(0x101664, struct.pack("H", 0x041F)) # Flag controlling the Saturn Valley ladder
rom.write_bytes(0x0F19C7, struct.pack("I", 0xF3104C)) # Replacement for the Moonside deliveryman
rom.write_bytes(0x0F0A93, struct.pack("I", 0x000000)) # Skip Pokey walking up after HHHQ
rom.write_bytes(0x086DFC, struct.pack("H", 0x00C7)) # Flag-independent Moonside entry
rom.write_bytes(0x0F1657, struct.pack("I", 0xF311A7)) # Fourside Cafe Door Script
rom.write_bytes(0x0F165B, struct.pack("H", 0x8091)) # Lock Cafe
rom.write_bytes(0x0FC8C6, struct.pack("I", 0xF3120B)) # Everdred script
rom.write_bytes(0x10784A, struct.pack("H", 0x0000)) # Mook spawn in stonehenge anteroom
rom.write_bytes(0x0FC51E, bytearray([0x00])) # Moonside sparkle always active
rom.write_bytes(0x0FA4D6, bytearray([0xC7, 0x00, 0x01]))
moonside_reward = world.multiworld.get_location("Fourside - Post-Moonside Delivery", world.player).item
if (moonside_reward.player != world.player) or world.options.remote_items or moonside_reward.name not in item_id_table:
rom.write_bytes(0x3310F7, struct.pack("I", 0xF310FB))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,236 @@
import struct
from .enemy_attributes import excluded_enemies
from ..enemy_data import spell_breaks
from ..enemy_shuffler import enemy_ids
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ... import EarthBoundWorld
from ...Rom import LocalRom
battle_actions = { # Actions in camel case are scaled
"Attack": 0x04,
"Shoot": 0x05,
"Spy": 0x06,
"Defend": 0x08,
"None": 0x09,
"Magnet Alpha": 0x36,
"Magnet Omega": 0x37,
"Call": 0x3E,
"Sow Seeds": 0x3F,
"Steal": 0x42,
"Freeze in Time": 0x43,
"diamond_eyes": 0x44,
"Strange Beam": 0x46,
"nauseous_breath": 0x47,
"poison_stinger": 0x48,
"kiss_of_death": 0x49,
"Arctic Breath": 0x4A,
"Mushroom Spores": 0x4B,
"Possess": 0x4C,
"Sprinkle Powder": 0x4D,
"Mold Spores": 0x4E,
"Binding Attack": 0x4F,
"Sticky Mucus": 0x50,
"Spew Fly Honey": 0x51,
"Shoot Silk": 0x52,
"Say Something Scary": 0x53,
"Do Something Mysterious": 0x54,
"Disrupt Sense": 0x55,
"Size Up Situation": 0x56,
"Exhale Stinky": 0x57,
"summon_storm": 0x58,
"scalding_espresso": 0x59,
"Haunting Melody": 0x5A,
"extinguishing_blast": 0x5B,
"crashing_boom_bang": 0x5C,
"spray_fire": 0x5D,
"breathe_fire": 0x5E,
"Spin Around": 0x5F,
"Lose Temper": 0x60,
"Say Nasty": 0x61,
"Vacuum Attack": 0x62,
# "Replenish Fuel",
"poisonous_fangs": 0x64,
# Dizzy Missile
"Continuous Attack": 0x66,
"Guard": 0x67,
"flaming_fireball": 0x68,
"Intertwine": 0x69,
"Crushing Chop": 0x6A,
"Submission Hold": 0x6B,
"Rev and Accelerate": 0x6C,
"Brandish a Knife": 0x6D,
"Tear Into": 0x6E,
"Bite": 0x6F,
"Claw with Nails": 0x70,
"Swing Tail": 0x71,
"Lunge": 0x72,
"Shopping Bag": 0x73,
"Swing Club": 0x74,
"Tornado": 0x75,
"Spray Water": 0x76,
"Flash a Menacing Smile": 0x77,
"Laugh Hysterically": 78,
"Edge Closer": 0x79,
"Whisper 3": 0x7A,
"Murmur 2": 0x7B,
"Mutter 1": 0x7C,
"Fall down": 0x7D,
"Be Absentminded": 0x7E,
"Burst of Steam": 0x7F,
"Wobble": 0x80,
"Reel": 0x81,
"Big Grin": 0x82,
"Take Breath": 0x83,
"Greeting": 0x84,
"Howl": 0x85,
"Tick Tock": 0x86,
# "Eat Food": 0x8A,
"PSI Food": 0x8E,
# Counter PSI: 0x9F World.gadget actions?
# Shield Killer
# HP Sucker
"Yogurt Dispenser": 0xAA,
"Toothbrush": 0xAE,
"Sudden Guts": 0xB6,
"Ruler": 0xBA,
"Protractor": 0xBB,
"Fly Honey": 0xBD,
"glorious_light": 0xC9,
"electrical_shock": 0xCA,
"paralyzing_pollen": 0xCB,
"Icy Hand": 0xCC,
"poison_flute": 0xCD,
"Exhaust Fumes": 0xCE,
"Laugh Maniacally": 0xCF,
"Breathe Flute": 0xD0,
"Leap and Spread Wings": 0xD1,
"Become Friendly": 0xD2,
"Rumble": 0xD3,
"Give Hug": 0xD4,
"hacking_cough": 0xD5,
"Misery Attack": 0xD6,
"Paint Attack": 0xD7,
"Come Out Swinging": 0xD8,
"Scratch with Claws": 0xD9,
"Peck at Eyes": 0xDA,
"Ram and Trample": 0xDB,
"Punch": 0xDC,
"Spit Seeds": 0xDD,
"Fire a Beam": 0xDE,
"Spear": 0xDF,
"Stomp with Foot": 0xE0,
"Hula Hoop": 0xE1,
"Charge Forward": 0xE2,
"Shred on Skateboard": 0xE3,
"diamond_bite": 0xE4,
"Grumble": 0xE5,
"Lecture": 0xE6,
"Scow": 0xE7,
"Vent Odor": 0xE8,
"Shout": 0xE9,
"Shriek": 0xEA,
"Knit Brow": 0xEB,
"scatter_spores": 0xED,
"Bite Attack": 0xEE,
"stuffiness_beam": 0xF1,
"Coil Around": 0xF2,
"Emit Light": 0xF8,
"Homesick": 0xFB,
"Fake PSI": 0x0100,
"Bark": 0x0108,
"Chant": 0x0109,
"Scratch Head": 0x010B,
"Discharge Gas": 0x0111,
"Monkey Love": 0x011B,
"Lost Bolts": 0x013A,
"Clean Area": 0x013C,
"Want Battery": 0x013D,
"throw_bomb": 0x01FC,
"shoot_rocket": 0x0203
}
needs_argument = {
"PSI Food": [0xCF, 0xF7, 0x6E, 0xF6, 0x63, 0x62],
"Ruler": [0x8C],
"Protractor": [0x8F],
"Toothbrush": [0x9A],
"Monkey Love": [0xD1],
"Sudden Guts": [0x9F],
"Shield Alpha": [0x1F],
"Shield Beta": [0x21],
"PSI Shield Alpha": [0x23],
"PSI Shield Beta": [0x25],
"Offense Up Alpha": [0x27],
"Offense Up Omega": [0x28],
"Defense Down Alpha": [0x29],
"Defense Down Omega": [0x2A],
"Hypnosis Alpha": [0x2B],
"Hypnosis Omega": [0x2C],
"Brainshock": [0x31],
"Magnet Alpha": [0x2D],
"Magnet Omega": [0x2E],
"Yogurt Dispenser": [0x8B],
"Fake PSI": [0x55, 0x04, 0x14, 0x16, 0x30, 0x32, 0x4D, 0x57]
}
psi_actions = {
"special": 0x0A,
"fire": 0x0E,
"freeze": 0x12,
"thunder": 0x16,
"flash": 0x1A,
"starstorm": 0x1E,
"lifeup": 0x20,
"healing": 0x24,
"Shield Alpha": 0x28,
"Shield Beta": 0x29,
"PSI Shield Alpha": 0x2C,
"PSI Shield Beta": 0x2D,
"Offense Up Alpha": 0x30,
"Offense Up Omega": 0x31,
"Defense Down Alpha": 0x32,
"Defense Down Omega": 0x33,
"Hypnosis Alpha": 0x34,
"Hypnosis Omega": 0x35,
"paralysis": 0x38,
"Brainshock": 0x3A,
"blast": 0x01A4,
"missile": 0x01A8
}
def randomize_enemy_attacks(world: "EarthBoundWorld", rom: "LocalRom") -> None:
"""Generates random attacks for enemies.
Certain attacks need to have an argument variable attached.
PSI moves have a 19% chance of being rolled only if the enemy has a non-zero max PP stat."""
for enemy in world.enemies:
if enemy not in excluded_enemies:
enemy_ai = world.random.randint(0, 3)
world.enemy_psi[enemy] = ["null", "null", "null", "null"]
max_calls = 0
for i in range(4):
if world.enemies[enemy].pp and world.random.randint(1, 100) < 20:
attack = world.random.choice(list(psi_actions.keys()))
attack_id = psi_actions[attack]
else:
attack = world.random.choice(list(battle_actions.keys()))
attack_id = battle_actions[attack]
if attack in spell_breaks:
world.enemy_psi[enemy][i] = attack
if attack in needs_argument:
argument = world.random.choice(needs_argument[attack])
elif attack in ["Sow Seeds", "Call"]:
argument = enemy_ids[enemy]
max_calls = world.random.randint(1, 4)
else:
argument = 0
rom.write_bytes(world.enemies[enemy].address + (0x46 + (i * 2)), struct.pack("H", attack_id))
rom.write_bytes(world.enemies[enemy].address + (0x50 + (i)), bytearray([argument]))
rom.write_bytes(world.enemies[enemy].address + 0x5C, bytearray([max_calls]))
rom.write_bytes(world.enemies[enemy].address + 0x45, bytearray([enemy_ai]))
# Todo; attack extenders?

View File

@@ -0,0 +1,143 @@
import struct
from .enemy_attributes import (enemy_species, enemy_adjectives, battle_sprites, field_sprites, excluded_enemies,
insects, robots, movement_patterns, start_texts, death_texts, weakness_table)
from ...game_data.text_data import calc_pixel_width, text_encoder
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ... import EarthBoundWorld
from ...Rom import LocalRom
shield_statuses = [
"phys_1",
"phys_2",
"psi_1",
"psi_2"
]
battle_songs = [
0x60,
0x61,
0x62,
0x63,
0x64,
0x65,
0x66,
0x67,
0x68,
0x69,
0x8D,
0x94
]
def randomize_enemy_attributes(world: "EarthBoundWorld", rom: "LocalRom") -> None:
"""Randomizes various attributes of enemies. This includes the name,
gender, sprite, color, etc. Data can be found in enemy_attributes."""
taken_names = []
for enemy in world.enemies:
if enemy not in excluded_enemies and " (" not in enemy:
new_name = "FFFFFFFFFFFFFFFFFFFFFFFFFF"
pixel_width = calc_pixel_width(new_name)
species = "Null"
while not (len(new_name) <= 25 and new_name not in taken_names and pixel_width <= 95):
species = world.random.choice(enemy_species)
adjective = world.random.choice(enemy_adjectives)
new_name = adjective + species
pixel_width = calc_pixel_width(new_name)
taken_names.append(new_name)
sprite = world.random.choice(battle_sprites[species])
field_sprite = field_sprites[sprite]
world.enemy_sprites[enemy] = field_sprite
movement_pattern = movement_patterns[field_sprite]
palette = world.random.randint(1, 31)
gender = world.random.randint(1, 3)
if species in robots:
enemy_type = 2
elif species in insects:
enemy_type = 1
else:
enemy_type = 0
row = world.random.randint(0, 1)
mirror_chance = world.random.randint(0, 100)
start_text = world.random.choice(start_texts)
death_text = world.random.choice(death_texts)
if species in ["Power Robot", "Reactor Robot", "Sphere"]:
death_action = 0x0040
elif species == "Oak":
death_action = 0x0041
else:
death_action = 0x0000
music = world.random.choice(battle_songs)
drop_rate = world.random.randint(0, 7)
base_drop = world.random.choice(world.filler_drops)
if world.random.randint(1, 100) < 6:
status = world.random.randint(1, 7)
if status < 5:
world.enemies[enemy].has_shield = shield_statuses[status - 1]
else:
status = 0
if species in ["Party Man", "Reveler"]:
miss_rate = 6
elif species == "Boy":
miss_rate = 8
elif species == "Bot":
miss_rate = 5
else:
miss_rate = world.random.randint(0, 4)
fire_weakness = get_weakness("Fire", species)
freeze_weakness = get_weakness("Freeze", species)
flash_weakness = get_weakness("Flash", species)
paralysis_weakness = get_weakness("Paralysis", species)
hypnosis_weakness = get_weakness("Hypnosis", species)
address = world.enemies[enemy].address
new_name = text_encoder(new_name, 0x18)
if len(new_name) < 0x18:
new_name.extend([0x00])
if world.enemies[enemy].attack_extensions > 1:
num_enemies = world.enemies[enemy].attack_extensions
else:
num_enemies = 1
for i in range(num_enemies):
rom.write_bytes(address, bytearray([0x01]))
rom.write_bytes(address + 1, new_name)
rom.write_bytes(address + 0x1A, bytearray([gender]))
rom.write_bytes(address + 0x1B, bytearray([enemy_type]))
rom.write_bytes(address + 0x1C, struct.pack("H", sprite))
rom.write_bytes(address + 0x1E, struct.pack("H", field_sprite))
rom.write_bytes(address + 0x2B, struct.pack("H", movement_pattern))
rom.write_bytes(address + 0x2D, struct.pack("I", start_text))
rom.write_bytes(address + 0x31, struct.pack("I", death_text))
rom.write_bytes(address + 0x35, bytearray([palette]))
rom.write_bytes(address + 0x37, bytearray([music]))
rom.write_bytes(address + 0x3F, bytearray([fire_weakness]))
rom.write_bytes(address + 0x40, bytearray([freeze_weakness]))
rom.write_bytes(address + 0x41, bytearray([flash_weakness]))
rom.write_bytes(address + 0x42, bytearray([paralysis_weakness]))
rom.write_bytes(address + 0x43, bytearray([hypnosis_weakness]))
rom.write_bytes(address + 0x43, bytearray([miss_rate]))
rom.write_bytes(address + 0x4E, struct.pack("H", death_action))
rom.write_bytes(address + 0x57, bytearray([drop_rate]))
rom.write_bytes(address + 0x58, bytearray([base_drop]))
rom.write_bytes(address + 0x59, bytearray([status]))
rom.write_bytes(address + 0x5B, bytearray([row]))
rom.write_bytes(address + 0x5D, bytearray([mirror_chance]))
if world.enemies[enemy].attack_extensions > 1:
address = world.enemies[f"{enemy} ({i + 2})"].address
enemy = f"{enemy} ({i + 2})"
def get_weakness(element: str, species: str) -> int:
"""Returns a weakness to given element, given the enemy's base species."""
if species in weakness_table[element]:
weakness = weakness_table[element][species]
else:
weakness = 1
return weakness

View File

@@ -0,0 +1,68 @@
from .enemy_attributes import excluded_enemies
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ... import EarthBoundWorld
from ...Rom import LocalRom
class EnemyStatCopy:
def __init__(self, hp: int, exp: int, money: int, speed: int, offense: int,
defense: int, level: int, guts: int, luck: int):
self.hp = hp
self.exp = exp
self.money = money
self.speed = speed
self.offense = offense
self.defense = defense
self.level = level
self.guts = guts
self.luck = luck
def randomize_enemy_stats(world: "EarthBoundWorld", rom: "LocalRom") -> None:
"""Randomizes enemy stats. It does not actually randomize them, rather it gives enemies the stats of a random other enemy.
Enemies have a 19% chance to have PP."""
stat_copies = {}
for enemy in world.enemies:
if enemy not in excluded_enemies:
stat_copies[enemy] = EnemyStatCopy(
hp=world.enemies[enemy].hp,
exp=world.enemies[enemy].exp,
money=world.enemies[enemy].money,
speed=world.enemies[enemy].speed,
offense=world.enemies[enemy].offense,
defense=world.enemies[enemy].defense,
level=world.enemies[enemy].level,
guts=world.enemies[enemy].guts,
luck=world.enemies[enemy].luck
)
for enemy in world.enemies:
if enemy not in excluded_enemies:
copied_stat_base = world.random.choice(list(stat_copies))
world.enemies[enemy].hp = stat_copies[copied_stat_base].hp
if world.random.randint(1, 100) < 20:
world.enemies[enemy].pp = int(world.random.randint(10, 500) / 2)
else:
world.enemies[enemy].pp = 0
world.enemies[enemy].offense = stat_copies[copied_stat_base].offense
world.enemies[enemy].defense = stat_copies[copied_stat_base].defense
world.enemies[enemy].speed = stat_copies[copied_stat_base].speed
world.enemies[enemy].level = stat_copies[copied_stat_base].level
world.enemies[enemy].exp = stat_copies[copied_stat_base].exp
world.enemies[enemy].money = stat_copies[copied_stat_base].money
world.enemies[enemy].guts = stat_copies[copied_stat_base].guts
world.enemies[enemy].luck = stat_copies[copied_stat_base].luck
rom.write_bytes(world.enemies[enemy].address + 0x3D, bytearray([world.enemies[enemy].guts]))
rom.write_bytes(world.enemies[enemy].address + 0x3E, bytearray([world.enemies[enemy].luck]))
if world.enemies[enemy].attack_extensions > 0:
world.enemies[f"{enemy} (2)"].hp = world.enemies[enemy].hp
world.enemies[f"{enemy} (2)"].pp = world.enemies[enemy].pp
world.enemies[f"{enemy} (2)"].offense = world.enemies[enemy].offense
world.enemies[f"{enemy} (2)"].defense = world.enemies[enemy].defense
world.enemies[f"{enemy} (2)"].speed = world.enemies[enemy].speed
world.enemies[f"{enemy} (2)"].level = world.enemies[enemy].level
world.enemies[f"{enemy} (2)"].exp = world.enemies[enemy].exp
world.enemies[f"{enemy} (2)"].money = world.enemies[enemy].money
rom.write_bytes(world.enemies[f"{enemy} (2)"].address + 0x3D, bytearray([world.enemies[enemy].guts]))
rom.write_bytes(world.enemies[f"{enemy} (2)"].address + 0x3E, bytearray([world.enemies[enemy].luck]))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,994 @@
from dataclasses import dataclass
import copy
from ..game_data.text_data import text_encoder, calc_pixel_width
from ..Options import Armorizer, Weaponizer
from operator import attrgetter
import struct
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .. import EarthBoundWorld
from ..Rom import LocalRom
@dataclass
class EBArmor:
address: int
equip_type: str
defense: int = 0
aux_stat: int = 0
poo_def: int = 0
flash_res: int = 0
freeze_res: int = 0
fire_res: int = 0
par_res: int = 0
sleep_res: int = 0
name: str = "None"
can_equip: str = "All"
total_resistance = 0
double_price_item: str = "None"
@dataclass
class EBWeapon:
can_equip: str
equip_type: str
address: int
name: str = "None"
offense: int = 0
aux_stat: int = 0
poo_off: int = 0
miss_rate: int = 0
double_price_item: str = "None"
def roll_resistances(world: "EarthBoundWorld", element: str, armor: EBArmor) -> None:
chance = world.random.randint(0, 100)
if chance < world.options.armorizer_resistance_chance.value:
setattr(armor, element, world.random.randint(1, 3))
else:
setattr(armor, element, 0)
def price_weapons(world: "EarthBoundWorld", weapons: list[EBWeapon], rom: "LocalRom") -> None:
for index, weapon in enumerate(weapons):
if weapon.can_equip == "Poo":
price = 10 * weapon.poo_off
else:
price = 10 * weapon.offense
if price > 300:
price = price * 2
price += (20 * weapon.aux_stat)
price -= (50 * weapon.miss_rate)
price += world.random.randint(-20, 20)
price = max(5, price)
rom.write_bytes((weapon.address + 26), struct.pack("H", price))
if weapon.double_price_item in summers_addresses:
price = min(0xFFFF, price * 2)
rom.write_bytes((summers_addresses[weapon.double_price_item] + 26), struct.pack("H", price))
def price_armors(world: "EarthBoundWorld", armor_pricing_list: list[EBArmor], rom: "LocalRom") -> None:
for index, armor in enumerate(armor_pricing_list):
if armor.can_equip == "Poo":
price = 10 * armor.poo_def
else:
price = 10 * armor.defense
if price > 300:
price = price * 2
price += (20 * armor.aux_stat)
price += (50 * armor.fire_res)
price += (50 * armor.freeze_res)
price += (50 * armor.flash_res)
price += (50 * armor.par_res)
price += world.random.randint(-20, 20)
price = max(5, price)
rom.write_bytes((armor.address + 26), struct.pack("H", price))
if armor.double_price_item in summers_addresses:
price = min(0xFFFF, price * 2)
rom.write_bytes((summers_addresses[armor.double_price_item] + 26), struct.pack("H", price))
def apply_progressive_weapons(world: "EarthBoundWorld", weapons: list[str], progressives: list[str], rom: "LocalRom") -> None:
for index, item in enumerate(weapons):
weapon = world.weapon_list[item]
weapon.offense = progressives[index].offense
rom.write_bytes(weapon.address + 31, bytearray([weapon.offense]))
def apply_progressive_armor(world: "EarthBoundWorld", armors: list[str], progressives: list[str], rom: "LocalRom") -> None:
for index, item in enumerate(armors):
armor = world.armor_list[item]
armor.defense = progressives[index].defense
rom.write_bytes(armor.address + 31, bytearray([armor.defense]))
adjectives = [
"Hard",
"Wild",
"Boring",
"Lavish",
"Grouchy",
"Elastic",
"Unsightly",
"Long",
"Wide",
"Cheap",
"Copper",
"Silver",
"Gold",
"Platinum",
"Diamond",
"Jade",
"Ruby",
"Sapphire",
"Pearl",
"Dull",
"Cold",
"Fair",
"Awful",
"Bad",
"Dry",
"Wet",
"Shiny",
"Damp",
"Elite",
"Beefy",
"Better",
"Alright",
"Okay",
"Metal",
"Pixie's",
"Cherub's",
"Demon's",
"Goddess",
"Sprite's",
"Fairy's",
"Devil's",
"Best",
"Spiteful",
"Travel",
"Great",
"Crystal",
"Baseball",
"Holmes",
"Red",
"Talisman",
"Defense",
"Mr. Saturn",
"Slumber",
"Lucky",
"Shiny",
"Souvenir",
"Silence",
"Ultimate",
"Charm",
"Saturn",
"Tenda",
"Sturdy",
"Sleek",
"Green",
"Blue",
"White",
"Yellow",
"Azure",
"Emerald",
"Handmade",
"Hank's",
"Real",
"Peace",
"Magic",
"Protect",
"Brass",
"Cursed",
"Rabbit's",
"Odd",
"Cheese",
"Casual",
"Silk",
"Gutsy",
"Hyper",
"Crusher",
"Thick",
"Deluxe",
"Chef's",
"Cracked",
"Plastic",
"Cotton",
"Mr. Baseball",
"Razor",
"Gilded",
"Master",
"Fighter's",
"Worn",
"Magicant",
"Happy",
"Well-Done",
"Rare",
"Gnarly",
"Wicked",
"Bionic",
"Combat",
"Tee ball",
"Sand lot",
"Minor league",
"Big league",
"Hall of fame",
"Famous",
"Legendary",
"Casey",
"French",
"Holy",
"Pop",
"Zip",
"Gaia",
"Baddest",
"Death",
"Spectrum",
"Laser",
"Moon",
"Toy",
"Magnum",
"Stun",
"Trick",
"Dirty",
"Washed",
"Laundered",
"Fresh",
"New",
"Old",
"Alien",
"T-rex's",
"Double",
"Non-stick",
"Football",
"Tennis",
"Golf",
"Hockey",
"Burnt",
"Boiled"
]
char_nums = {
"Ness": 0x01,
"Paula": 0x02,
"Jeff": 0x03,
"Poo": 0x04
}
usage_bytes = {
"All": 0x0F,
"Ness": 0x01,
"Paula": 0x02,
"Jeff": 0x04,
"Poo": 0x08
}
type_bytes = {
"body": 0x14,
"arm": 0x18,
"other": 0x1C,
"Bash": 0x10,
"Shoot": 0x11
}
summers_addresses = {
"Platinum Band": 0x155A5C,
"Diamond Band": 0x155A83,
"Big League Bat": 0x15535A
}
royal_names = [
"of kings",
"of dukes",
"of princes",
"of barons",
"of lords",
"of sultans",
"of counts",
"of England"
]
def randomize_armor(world: "EarthBoundWorld", rom: "LocalRom") -> None:
if world.options.equipamizer_cap_stats:
armor_caps = {
"body": 30,
"arm": 80,
"other": 110
}
else:
armor_caps = {
"body": 127,
"arm": 127,
"other": 127
}
other_adjectives = adjectives.copy()
arm_adjectives = adjectives.copy()
body_adjectives = adjectives.copy()
taken_names = []
armor_dict = {
"arm": arm_adjectives,
"body": body_adjectives,
"other": other_adjectives
}
equalized_names = [
"Mild",
"Earth",
"Sea"
]
ult_names = [
"Day",
"Sun",
"Star"
]
elemental_names = {
"Flash": [
"Dark",
"Cloud",
"Night"
],
"Freeze": [
"Puddle",
"Drizzle",
"Rain"
],
"Fire": [
"Smoke",
"Ember",
"Flame"
]
}
plain_elemental_names = [
"Dark",
"Cloud",
"Night",
"Puddle",
"Drizzle",
"Rain",
"Smoke",
"Ember",
"Flame",
"Mild",
"Earth",
"Sea",
"Day",
"Sun",
"Star"
]
all_armor = [
"Travel Charm",
"Great Charm",
"Crystal Charm",
"Rabbit's Foot",
"Flame Pendant",
"Rain Pendant",
"Night Pendant",
"Sea Pendant",
"Star Pendant",
"Cloak of Kings",
"Cheap Bracelet",
"Copper Bracelet",
"Silver Bracelet",
"Gold Bracelet",
"Platinum Band",
"Diamond Band",
"Pixie's Bracelet",
"Cherub's Band",
"Goddess Band",
"Bracer of Kings",
"Baseball Cap",
"Mr. Baseball Cap",
"Holmes Hat",
"Hard Hat",
"Coin of Slumber",
"Coin of Defense",
"Coin of Silence",
"Mr. Saturn Coin",
"Charm Coin",
"Lucky Coin",
"Talisman Coin",
"Shiny Coin",
"Souvenir Coin",
"Diadem of Kings",
"Earth Pendant",
"Saturn Ribbon",
"Ribbon",
"Red Ribbon",
"Defense Ribbon",
"Talisman Ribbon",
"Goddess Ribbon",
]
char_armor_names = {
"Ness": {
"body": "tee",
"arm": "mitt",
"other": "pack"
},
"Paula": {
"body": "dress",
"arm": "ring",
"other": "ribbon"
},
"Jeff": {
"body": "tie",
"arm": "watch",
"other": "glasses"
},
}
aux_stat = {
"arm": "Luck",
"body": "Speed",
"other": "Luck"
}
armor_names = {
"body": ["pendant", "charm", "foot", "brooch", "shirt",
"amulet", "cloak", "suit", "plate", "vest", "coat", "jersey", "poncho"],
"arm": ["bracelet", "band", "bracer", "gauntlet", "sleeve", "glove", "bangle", "armlet", "sweatband"],
"other": ["cap", "hat", "coin", "crown", "diadem", "helmet", "mask", "wig", "pants", "jeans", "greaves", "boot"]
}
res_strength = [
", just a little bit",
" somewhat",
""
]
progressive_bracelets = [
]
progressive_others = [
]
world.armor_list = {
"Travel Charm": EBArmor(0x15583A, "body"),
"Great Charm": EBArmor(0x155861, "body"),
"Crystal Charm": EBArmor(0x155888, "body"),
"Rabbit's Foot": EBArmor(0x1558AF, "body"),
"Flame Pendant": EBArmor(0x1558D6, "body"),
"Rain Pendant": EBArmor(0x1558FD, "body"),
"Night Pendant": EBArmor(0x155924, "body"),
"Sea Pendant": EBArmor(0x15594B, "body"),
"Star Pendant": EBArmor(0x155972, "body"),
"Cloak of Kings": EBArmor(0x155999, "body"),
"Cheap Bracelet": EBArmor(0x1559C0, "arm"),
"Copper Bracelet": EBArmor(0x1559E7, "arm"),
"Silver Bracelet": EBArmor(0x155A0E, "arm"),
"Gold Bracelet": EBArmor(0x155A35, "arm"),
"Platinum Band": EBArmor(0x1570E8, "arm"),
"Diamond Band": EBArmor(0x15710F, "arm"),
"Pixie's Bracelet": EBArmor(0x155AAA, "arm"),
"Cherub's Band": EBArmor(0x155AD1, "arm"),
"Goddess Band": EBArmor(0x155AF8, "arm"),
"Bracer of Kings": EBArmor(0x155B1F, "arm"),
"Baseball Cap": EBArmor(0x155B46, "other"),
"Mr. Baseball Cap": EBArmor(0x155B94, "other"),
"Holmes Hat": EBArmor(0x155B6D, "other"),
"Hard Hat": EBArmor(0x155BBB, "other"),
"Ribbon": EBArmor(0x155BE2, "other"),
"Red Ribbon": EBArmor(0x155C09, "other"),
"Goddess Ribbon": EBArmor(0x155C30, "other"),
"Coin of Slumber": EBArmor(0x155C57, "other"),
"Coin of Defense": EBArmor(0x155C7E, "other"),
"Coin of Silence": EBArmor(0x1571AB, "other"),
"Mr. Saturn Coin": EBArmor(0x1575EF, "other"),
"Lucky Coin": EBArmor(0x155CA5, "other"),
"Charm Coin": EBArmor(0x1571D2, "other"),
"Talisman Coin": EBArmor(0x155CCC, "other"),
"Shiny Coin": EBArmor(0x155CF3, "other"),
"Souvenir Coin": EBArmor(0x155D1A, "other"),
"Diadem of Kings": EBArmor(0x155D41, "other"),
"Earth Pendant": EBArmor(0x156D8E, "body"),
"Defense Ribbon": EBArmor(0x157136, "other"),
"Talisman Ribbon": EBArmor(0x15715D, "other"),
"Saturn Ribbon": EBArmor(0x157184, "other"),
}
for item in all_armor:
armor = world.armor_list[item]
if world.options.armorizer == Armorizer.option_chaos:
armor.equip_type = world.random.choice(["arm", "body", "other"])
if armor.equip_type == "arm":
progressive_bracelets.append(item)
elif armor.equip_type == "other" and armor.can_equip == "All":
progressive_others.append(item)
if item in summers_addresses:
armor.double_price_item = item
armor.defense = world.random.randint(1, armor_caps[armor.equip_type])
chance = world.random.randint(0, 100)
if chance < 8:
armor.aux_stat = world.random.randint(1, 127)
else:
armor.aux_stat = 0
if armor.equip_type != "arm":
roll_resistances(world, "flash_res", armor)
roll_resistances(world, "freeze_res", armor)
roll_resistances(world, "fire_res", armor)
roll_resistances(world, "par_res", armor)
armor.sleep_res = 0
else:
armor.flash_res = 0
armor.freeze_res = 0
armor.fire_res = 0
armor.par_res = 0
# Only Arm gear can have sleep resistance; arm gear cannot have elemental resistance
roll_resistances(world, "sleep_res", armor)
if armor.flash_res + armor.freeze_res + armor.fire_res == 0:
# If no resistances are active use a normal name
front_name = world.random.choice(armor_dict[armor.equip_type])
armor_dict[armor.equip_type].remove(front_name)
elif armor.flash_res == armor.freeze_res == armor.fire_res:
# Get a combined name for the level
front_name = equalized_names[armor.flash_res - 1]
elif armor.par_res == armor.flash_res == armor.freeze_res == armor.fire_res:
# Should be used if Paralysis + the others all succeed
front_name = ult_names[armor.flash_res - 1]
else:
# If resistances are inequal, use the strongest as the name and pull a name from its strength
# If 2 are equal pick a random of them
names = ("Flash", "Freeze", "Fire")
strengths = (armor.flash_res, armor.freeze_res, armor.fire_res)
best_elements = [(name, strength) for name, strength in zip(names, strengths) if strength == max(strengths)]
best_name, best_strength = world.random.choice(best_elements)
front_name = elemental_names[best_name][best_strength - 1]
chance = world.random.randint(0, 100)
if chance < 10:
armor.can_equip = world.random.choice(["Ness", "Paula", "Jeff", "Poo"])
else:
armor.can_equip = "All"
if armor.can_equip == "Poo":
back_name = world.random.choice(royal_names)
front_name = world.random.choice(armor_names[armor.equip_type]).capitalize()
elif armor.can_equip in ["Ness", "Paula", "Jeff"]:
back_name = char_armor_names[armor.can_equip][armor.equip_type]
else:
back_name = world.random.choice(armor_names[armor.equip_type])
armor.name = front_name + " " + back_name
if armor.name in taken_names:
front_name = world.random.choice(armor_dict[armor.equip_type])
armor_dict[armor.equip_type].remove(front_name)
armor.name = front_name + " " + back_name
pixel_length = calc_pixel_width(armor.name)
first_armor = False
if armor.can_equip == "Poo":
names_to_try = royal_names.copy()
else:
names_to_try = armor_names[armor.equip_type].copy()
while pixel_length > 70 or armor.name in taken_names:
# First we replace any spaces with half-width spaces, a common tech used in vanilla to fix long names
if first_armor is False:
armor.name = armor.name.replace(" ", "")
first_armor = True
else:
if names_to_try and front_name not in plain_elemental_names:
# If it's still too long, change the second part of the name to try and roll a shorter name
back_name = world.random.choice(names_to_try)
names_to_try.remove(back_name)
else:
# If it's *STILL* too long, chop a letter off the end of the front
front_name = front_name[:-1]
if front_name == "":
# we ran out of letters rip
front_name = "Long"
first_armor = False
armor.name = front_name + " " + back_name
pixel_length = calc_pixel_width(armor.name)
taken_names.append(armor.name)
armor.total_resistance = (1 * armor.fire_res) + (4 * armor.freeze_res) + (16 * armor.flash_res) + (64 * armor.par_res)
rom.write_bytes(armor.address + 28, bytearray([usage_bytes[armor.can_equip]]))
rom.write_bytes(armor.address + 25, bytearray([type_bytes[armor.equip_type]]))
sortable_armor = copy.deepcopy(world.armor_list)
sorted_armor = sorted(sortable_armor.values(), key=attrgetter("defense"))
sorted_arm_gear = [armor for armor in sorted_armor if armor.equip_type == "arm"]
sorted_body_gear = [armor for armor in sorted_armor if armor.equip_type == "body"]
sorted_other_gear = [armor for armor in sorted_armor if armor.equip_type == "other"]
sorts = [
sorted_arm_gear,
sorted_other_gear,
sorted_body_gear,
]
prog_armors = [
progressive_bracelets,
progressive_others
]
if world.options.progressive_armor:
for i in range(2):
apply_progressive_armor(world, prog_armors[i], sorts[i], rom)
for i in range(3):
price_armors(world, sorts[i], rom)
for item in all_armor:
armor = world.armor_list[item]
if armor.can_equip != "Poo":
armor.poo_def = 216 # defense is signed, all non-kings equipment has this value
else:
armor.poo_def = armor.defense
rom.write_bytes(armor.address + 31, bytearray([armor.defense, armor.poo_def, armor.aux_stat, armor.total_resistance]))
item_name = text_encoder(armor.name, 25)
item_name.extend([0x00])
description = f"{armor.name}\n"
if armor.can_equip != "All":
description += f"@♪'s {armor.equip_type} equipment.\n"
else:
if armor.equip_type == "other":
description += "@Must be equipped as “other”.\n"
else:
description += f"@Must be equipped on your {armor.equip_type}.\n"
if armor.can_equip == "Poo":
description += f"@+{armor.poo_def} Defense.\n"
else:
description += f"@+{armor.defense} Defense.\n"
if armor.aux_stat > 0:
description += f"@+{armor.aux_stat} {aux_stat[armor.equip_type]}. \n"
if armor.flash_res > 0:
description += f"@Protects against Flash attacks{res_strength[armor.flash_res - 1]}.\n"
if armor.freeze_res > 0:
description += f"@Protects against Freeze attacks{res_strength[armor.freeze_res - 1]}.\n"
if armor.fire_res > 0:
description += f"@Protects against Fire attacks{res_strength[armor.fire_res - 1]}.\n"
if armor.par_res > 0:
description += f"@Protects against Paralysis{res_strength[armor.par_res - 1]}.\n"
if armor.sleep_res > 0:
description += f"@Protects against Sleep{res_strength[armor.sleep_res - 1]}.\n"
description = text_encoder(description, 0x100)
description = description[:-2]
description.extend([0x13, 0x02])
if armor.can_equip != "All":
index = description.index(0xAC)
description[index:index + 1] = bytearray([0x1C, 0x02, char_nums[armor.can_equip]])
rom.write_bytes(armor.address, item_name)
rom.write_bytes((0x310000 + world.description_pointer), description)
rom.write_bytes((armor.address + 35), struct.pack("I", (0xF10000 + world.description_pointer)))
if item in ["Platinum Band", "Diamond Band"]:
rom.write_bytes(summers_addresses[item] + 28, bytearray([usage_bytes[armor.can_equip]]))
rom.write_bytes(summers_addresses[item] + 31, bytearray([armor.defense, armor.poo_def, armor.aux_stat, armor.total_resistance]))
rom.write_bytes(summers_addresses[item] + 25, bytearray([type_bytes[armor.equip_type]]))
rom.write_bytes(summers_addresses[item], item_name)
rom.write_bytes((summers_addresses[item] + 35), struct.pack("I", (0xF10000 + world.description_pointer)))
world.description_pointer += len(description)
def randomize_weapons(world: "EarthBoundWorld", rom: "LocalRom") -> None:
if world.options.equipamizer_cap_stats:
weapon_cap = 120
else:
weapon_cap = 127
weapon_names = {
"Ness": ["bat", "stick", "club", "board", "racket", "cue", "pole", "paddle"],
"Paula": ["fry pan", "frypan", "skillet", "whisk", "saucepan", "pin"],
"Jeff": ["gun", "beam", "air gun", "beam gun", "cannon", "blaster", "pistol", "revolver", "shotgun", "rifle"],
"Poo": ["Sword", "Katana", "Knife", "Scissor", "Cutter", "Blade", "Chisel", "Saw", "Axe", "Scalpel", "Sabre"],
"All": ["yo-yo", "slingshot", "boomerang", "chakram", "bow"]
}
taken_names = []
miss_rates = {
"Ness": 1,
"Paula": 1,
"Jeff": 0,
"Poo": 0,
"All": 3
}
progressive_bats = [
]
progressive_pans = [
]
progressive_guns = [
]
progressive_alls = [
]
starting_weapons = {
"Ness": "Tee Ball Bat",
"Paula": "Fry Pan",
"Jeff": "Pop Gun",
"Poo": "None"
}
starting_weapon = starting_weapons[world.starting_character]
world.weapon_list = {
"Cracked Bat": EBWeapon("Ness", "Bash", 0x155297),
"Tee Ball Bat": EBWeapon("Ness", "Bash", 0x1552BE),
"Sand Lot Bat": EBWeapon("Ness", "Bash", 0x1552E5),
"Minor League Bat": EBWeapon("Ness", "Bash", 0x15530C),
"Mr. Baseball Bat": EBWeapon("Ness", "Bash", 0x155333),
"Big League Bat": EBWeapon("Ness", "Bash", 0x157073),
"Hall of Fame Bat": EBWeapon("Ness", "Bash", 0x155381),
"Magicant Bat": EBWeapon("Ness", "Bash", 0x1553A8),
"Legendary Bat": EBWeapon("Ness", "Bash", 0x1553CF),
"Gutsy Bat": EBWeapon("Ness", "Bash", 0x1553F6),
"Casey Bat": EBWeapon("Ness", "Bash", 0x15541D),
"Fry Pan": EBWeapon("Paula", "Bash", 0x155444),
"Thick Fry Pan": EBWeapon("Paula", "Bash", 0x15546B),
"Deluxe Fry Pan": EBWeapon("Paula", "Bash", 0x155492),
"Chef's Fry Pan": EBWeapon("Paula", "Bash", 0x1554B9),
"French Fry Pan": EBWeapon("Paula", "Bash", 0x1554E0),
"Magic Fry Pan": EBWeapon("Paula", "Bash", 0x155507),
"Holy Fry Pan": EBWeapon("Paula", "Bash", 0x15552E),
"Sword of Kings": EBWeapon("Poo", "Bash", 0x155555),
"Pop Gun": EBWeapon("Jeff", "Shoot", 0x15557C),
"Stun Gun": EBWeapon("Jeff", "Shoot", 0x1555A3),
"Toy Air Gun": EBWeapon("Jeff", "Shoot", 0x1555CA),
"Magnum Air Gun": EBWeapon("Jeff", "Shoot", 0x1555F1),
"Zip Gun": EBWeapon("Jeff", "Shoot", 0x155618),
"Laser Gun": EBWeapon("Jeff", "Shoot", 0x15563F),
"Hyper Beam": EBWeapon("Jeff", "Shoot", 0x155666),
"Crusher Beam": EBWeapon("Jeff", "Shoot", 0x15568D),
"Spectrum Beam": EBWeapon("Jeff", "Shoot", 0x1556B4),
"Death Ray": EBWeapon("Jeff", "Shoot", 0x1556DB),
"Baddest Beam": EBWeapon("Jeff", "Shoot", 0x155702),
"Moon Beam Gun": EBWeapon("Jeff", "Shoot", 0x155729),
"Gaia Beam": EBWeapon("Jeff", "Shoot", 0x155750),
"Yo-yo": EBWeapon("All", "Shoot", 0x155777),
"Slingshot": EBWeapon("All", "Shoot", 0x15579E),
"Bionic Slingshot": EBWeapon("All", "Shoot", 0x1557C5),
"Trick Yo-yo": EBWeapon("All", "Shoot", 0x1557EC),
"Combat Yo-yo": EBWeapon("All", "Shoot", 0x155813),
"T-Rex's Bat": EBWeapon("Ness", "Bash", 0x15704C),
"Ultimate Bat": EBWeapon("Ness", "Bash", 0x15709A),
"Double Beam": EBWeapon("Jeff", "Shoot", 0x1570C1),
"Non-stick Frypan": EBWeapon("Paula", "Bash", 0x1575C8)
}
all_weapons = [
"Cracked Bat",
"Tee Ball Bat",
"Sand Lot Bat",
"Minor League Bat",
"Mr. Baseball Bat",
"T-Rex's Bat",
"Big League Bat",
"Hall of Fame Bat",
"Ultimate Bat",
"Casey Bat",
"Magicant Bat",
"Legendary Bat",
"Gutsy Bat",
"Fry Pan",
"Thick Fry Pan",
"Deluxe Fry Pan",
"Chef's Fry Pan",
"Non-stick Frypan",
"French Fry Pan",
"Holy Fry Pan",
"Magic Fry Pan",
"Sword of Kings",
"Pop Gun",
"Stun Gun",
"Toy Air Gun",
"Magnum Air Gun",
"Zip Gun",
"Laser Gun",
"Hyper Beam",
"Double Beam",
"Crusher Beam",
"Spectrum Beam",
"Death Ray",
"Baddest Beam",
"Moon Beam Gun",
"Gaia Beam",
"Yo-yo",
"Slingshot",
"Bionic Slingshot",
"Trick Yo-yo",
"Combat Yo-yo"
]
for item in all_weapons:
weapon = world.weapon_list[item]
if world.options.weaponizer == Weaponizer.option_chaos:
chance = world.random.randint(1, 100)
if chance < 8:
weapon.can_equip = "All"
else:
weapon.can_equip = world.random.choice(["Ness", "Paula", "Jeff", "Poo"])
if item == starting_weapon:
weapon.can_equip = world.starting_character
if item in summers_addresses:
weapon.double_price_item = item
if weapon.can_equip == "Ness":
progressive_bats.append(item)
elif weapon.can_equip == "Paula":
progressive_pans.append(item)
elif weapon.can_equip == "Jeff":
progressive_guns.append(item)
if item == starting_weapon and not world.options.progressive_weapons: # Todo; remove not progressive weapons
weapon.offense = 10
else:
if world.options.progressive_weapons:
weapon.offense = world.random.randint(10, weapon_cap)
else:
weapon.offense = world.random.randint(1, weapon_cap)
if weapon.can_equip == "Poo":
front_name = world.random.choice(weapon_names[weapon.can_equip])
back_name = world.random.choice(royal_names)
else:
front_name = world.random.choice(adjectives)
back_name = world.random.choice(weapon_names[weapon.can_equip])
chance = world.random.randint(0, 100)
if chance < 8:
weapon.aux_stat = world.random.randint(1, 127)
else:
weapon.aux_stat = 0
if weapon.can_equip in ["Jeff", "All"]:
weapon.equip_type = "Shoot"
else:
weapon.equip_type = "Bash"
chance = world.random.randint(1, 100)
if chance < 4 and item != starting_weapon:
weapon.miss_rate = 12
else:
weapon.miss_rate = miss_rates[weapon.can_equip]
weapon.name = front_name + " " + back_name
pixel_length = calc_pixel_width(weapon.name)
half_space = False
if weapon.can_equip == "Poo":
names_to_try = royal_names.copy()
else:
names_to_try = weapon_names[weapon.can_equip].copy()
while pixel_length > 70 or weapon.name in taken_names:
# First we replace any spaces with half-width spaces, a common tech used in vanilla to fix long names
if half_space is False:
weapon.name = weapon.name.replace(" ", "")
half_space = True
else:
if names_to_try:
# If it's still too long, change the second part of the name to try and roll a shorter name
back_name = world.random.choice(names_to_try)
names_to_try.remove(back_name)
else:
# If it's *STILL* too long, chop a letter off the end of the front
front_name = front_name[:-1]
if front_name == "":
# we ran out of letters rip
front_name = "Long"
half_space = False
weapon.name = front_name + " " + back_name
pixel_length = calc_pixel_width(weapon.name)
rom.write_bytes(weapon.address + 28, bytearray([usage_bytes[weapon.can_equip]]))
rom.write_bytes(weapon.address + 25, bytearray([type_bytes[weapon.equip_type]]))
taken_names.append(weapon.name)
sortable_weapons = copy.deepcopy(world.weapon_list)
sorted_weapons = sorted(sortable_weapons.values(), key=attrgetter("offense"))
sorted_bats = [weapon for weapon in sorted_weapons if weapon.can_equip == "Ness"]
sorted_pans = [weapon for weapon in sorted_weapons if weapon.can_equip == "Paula"]
sorted_guns = [weapon for weapon in sorted_weapons if weapon.can_equip == "Jeff"]
sorted_swords = [weapon for weapon in sorted_weapons if weapon.can_equip == "Poo"]
sorted_alls = [weapon for weapon in sorted_weapons if weapon.can_equip == "All"]
sorts = [
sorted_bats,
sorted_pans,
sorted_guns,
sorted_alls,
sorted_swords
]
prog_weapons = [
progressive_bats,
progressive_pans,
progressive_guns,
progressive_alls
]
for i in range(5):
price_weapons(world, sorts[i], rom)
if world.options.progressive_weapons:
for i in range(4):
apply_progressive_weapons(world, prog_weapons[i], sorts[i], rom)
for item in all_weapons:
weapon = world.weapon_list[item]
if weapon.can_equip == "Poo":
weapon.poo_off = weapon.offense
else:
weapon.poo_off = 250
rom.write_bytes(weapon.address + 31, bytearray([
weapon.offense, weapon.poo_off, weapon.aux_stat, weapon.miss_rate]))
item_name = text_encoder(weapon.name, 25)
item_name.extend([0x00])
description = f"{weapon.name}\n"
if weapon.can_equip != "All":
description += f"@♪ can equip this weapon.\n"
description += f"@+{weapon.offense} Offense.\n"
if weapon.aux_stat > 0:
description += f"@+{weapon.aux_stat} Guts.\n"
if weapon.miss_rate == 12:
description += "@If you use this, you might just whiff.\n"
description = text_encoder(description, 0x100)
description = description[:-2]
description.extend([0x13, 0x02])
if weapon.can_equip != "All":
index = description.index(0xAC)
description[index:index + 1] = bytearray([0x1C, 0x02, char_nums[weapon.can_equip]])
rom.write_bytes(weapon.address, item_name)
rom.write_bytes((0x310000 + world.description_pointer), description)
rom.write_bytes((weapon.address + 35), struct.pack("I", (0xF10000 + world.description_pointer)))
if item == "Big League Bat":
rom.write_bytes(summers_addresses[item] + 28, bytearray([usage_bytes[weapon.can_equip]]))
rom.write_bytes(summers_addresses[item] + 31, bytearray([weapon.offense, weapon.poo_off, weapon.aux_stat, weapon.miss_rate]))
rom.write_bytes(summers_addresses[item] + 25, bytearray([type_bytes[weapon.equip_type]]))
rom.write_bytes(summers_addresses[item], item_name)
rom.write_bytes((summers_addresses[item] + 35), struct.pack("I", (0xF10000 + world.description_pointer)))
world.description_pointer += len(description)

View File

@@ -0,0 +1,328 @@
from ..game_data.text_data import eb_text_table
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .. import EarthBoundWorld
random_flavors = [
"Mint flavor",
"Strawberry flavor",
"Banana flavor",
"Peanut flavor",
"Crystal flavor",
"Cherry flavor",
"Grape flavor",
"Blueberry flavor",
"Handheld flavor",
"Rotten flavor",
"Golden flavor",
"Herbal flavor",
"Cherry Cola flavor",
"Salt and Pepper flavor",
"Pizza flavor",
"Cotton Candy flavor",
"Potato flavor",
"Lemon-lime flavor",
"Raisin flavor",
"Chocolate flavor",
"Ginger Ale flavor",
"Orange Cream flavor",
"Choco Cream flavor",
"Doomed flavor",
"Farm flavor",
"Negative flavor",
"Astral flavor",
"Sunflower flavor",
"Encore flavor",
"Experimental flavor",
"Blueblue flavor",
"Deeppurple flavor",
"Rivalry flavor",
"Sugar flavor",
"Faith flavor",
"Wisdom flavor",
"Hamburger flavor",
"Spearmint flavor",
"Tuna flavor",
"Milk flavor",
"Peppermint flavor",
"Toothpaste flavor",
"Barbecue flavor",
"Lavender flavor",
"Apple flavor",
"Peach flavor",
"Bubblegum flavor",
"Orange flavor",
"Toilet flavor",
"Lemonade flavor",
"Garlic flavor"
]
flavor_data = {
"Cherry flavor": [0x00, 0x00, 0xde, 0x5f, 0x9c, 0x2d, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x77, 0xf2, 0x41, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0x1b, 0x1c, 0x1b, 0x1c, 0x80, 0x3b, 0xbf, 0x57, 0xda, 0x28, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xba, 0x49, 0x3f, 0x56, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xde, 0x5f, 0x44, 0x06, 0x44, 0x2a, 0x80, 0x3b, 0x7f, 0x67, 0xda, 0x28, 0xaa, 0x28],
"Grape flavor": [0x00, 0x00, 0xde, 0x5f, 0x9c, 0x2d, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x77, 0x2f, 0x4a, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0x1b, 0x1c, 0x1b, 0x1c, 0x80, 0x3b, 0xbf, 0x57, 0x13, 0x4c, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xc8, 0x40, 0x2f, 0x55, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xde, 0x5f, 0x44, 0x30, 0x44, 0x30, 0x80, 0x3b, 0x5f, 0x63, 0xb2, 0x69, 0xc7, 0x28],
"Blueberry flavor": [0x00, 0x00, 0xde, 0x5f, 0x9c, 0x2d, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x77, 0x4d, 0x4a, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0x1b, 0x1c, 0x1b, 0x1c, 0x80, 0x3b, 0xbf, 0x57, 0x86, 0x7d, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0x04, 0x41, 0x04, 0x56, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xbd, 0x77, 0x63, 0x30, 0x63, 0x30, 0x80, 0x3b, 0xb9, 0x77, 0xa4, 0x55, 0xc4, 0x28],
"Handheld flavor": [0x00, 0x00, 0xff, 0x7f, 0xab, 0x29, 0xa3, 0x0c, 0x80, 0x3b, 0xff, 0x77, 0x6f, 0x36, 0xa3, 0x0c,
0x80, 0x3b, 0xde, 0x5f, 0x1b, 0x1c, 0x1b, 0x1c, 0x80, 0x3b, 0xd4, 0x4e, 0xeb, 0x25, 0xa3, 0x0c,
0x80, 0x3b, 0xff, 0x77, 0xec, 0x2d, 0x57, 0x5f, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xff, 0x77, 0x89, 0x25, 0xa3, 0x0c, 0x80, 0x3b, 0x50, 0x3e, 0xab, 0x29, 0x47, 0x1d],
"Rotten flavor": [0x00, 0x00, 0xbf, 0x57, 0x9c, 0x2d, 0x25, 0x14, 0x80, 0x3b, 0xa8, 0x42, 0x84, 0x31, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0x1b, 0x1c, 0x1b, 0x1c, 0x80, 0x3b, 0xbe, 0x57, 0x4f, 0x79, 0x25, 0x14,
0x80, 0x3b, 0x16, 0x56, 0x84, 0x18, 0xc7, 0x24, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0x16, 0x56, 0x10, 0x20, 0x10, 0x20, 0x80, 0x3b, 0x18, 0x42, 0x8a, 0x30, 0x44, 0x20],
"Golden flavor": [0x00, 0x00, 0xbf, 0x57, 0x9c, 0x2d, 0x25, 0x14, 0x80, 0x3b, 0xbf, 0x4b, 0x18, 0x22, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0x1b, 0x1c, 0x1b, 0x1c, 0x80, 0x3b, 0xbe, 0x57, 0x4f, 0x79, 0x25, 0x14,
0x80, 0x3b, 0xff, 0x6f, 0x50, 0x08, 0x58, 0x11, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xff, 0x6f, 0x9f, 0x30, 0x9f, 0x30, 0x80, 0x3b, 0xff, 0x63, 0xfd, 0x2a, 0xaa, 0x1c],
"Herbal flavor": [0x00, 0x00, 0xbf, 0x57, 0x9c, 0x2d, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x77, 0x6d, 0x46, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0x1b, 0x1c, 0x1b, 0x1c, 0x80, 0x3b, 0xbe, 0x57, 0x4f, 0x79, 0x25, 0x14,
0x80, 0x3b, 0xdd, 0x6f, 0x86, 0x21, 0x69, 0x22, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xdd, 0x6f, 0x48, 0x6e, 0x48, 0x6e, 0x80, 0x3b, 0xba, 0x63, 0xaa, 0x2a, 0x06, 0x29],
"Cherry Cola flavor": [0x00, 0x00, 0xde, 0x5f, 0x9c, 0x2d, 0x08, 0x14, 0x80, 0x3b, 0x9f, 0x2d, 0x76, 0x24, 0x08, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0x1b, 0x1c, 0x1b, 0x1c, 0x80, 0x3b, 0xbf, 0x57, 0x9f, 0x2d, 0x08, 0x14,
0x80, 0x3b, 0xff, 0x5e, 0x73, 0x04, 0x89, 0x00, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xff, 0x5e, 0x89, 0x00, 0x89, 0x00, 0x80, 0x3b, 0x9f, 0x2d, 0x89, 0x00, 0x08, 0x14],
"Salt and Pepper flavor": [0x00, 0x00, 0xff, 0x7f, 0x39, 0x67, 0xa5, 0x20, 0x80, 0x3b, 0xff, 0x77, 0x0d, 0x4e, 0xa5, 0x20,
0x80, 0x3b, 0xde, 0x5f, 0x1b, 0x1c, 0x1b, 0x1c, 0x80, 0x3b, 0x51, 0x4e, 0xdd, 0x7f, 0xa5, 0x20,
0x00, 0x00, 0xff, 0x7f, 0x31, 0x52, 0x59, 0x77, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xff, 0x7f, 0x00, 0x00, 0x40, 0x10, 0x80, 0x3b, 0x30, 0x46, 0x6a, 0x2d, 0x83, 0x14],
"Pizza flavor": [0x00, 0x00, 0xde, 0x5f, 0x39, 0x67, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x77, 0x1e, 0x0e, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0x1b, 0x1c, 0x1b, 0x1c, 0x80, 0x3b, 0xbf, 0x57, 0x4f, 0x79, 0x25, 0x14,
0x00, 0x00, 0x7f, 0x5b, 0xb5, 0x10, 0xbe, 0x2a, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xde, 0x5f, 0x52, 0x79, 0x52, 0x79, 0x80, 0x3b, 0x7f, 0x5b, 0xbe, 0x2a, 0xc7, 0x28],
"Cotton Candy flavor": [0x00, 0x00, 0xde, 0x5f, 0x3f, 0x7f, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x77, 0x6a, 0x56, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0x1b, 0x1c, 0x1b, 0x1c, 0x80, 0x3b, 0xbf, 0x57, 0x4f, 0x79, 0x25, 0x14,
0x00, 0x00, 0x9f, 0x73, 0xf1, 0x62, 0xfe, 0x3d, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xde, 0x5f, 0x52, 0x79, 0x52, 0x79, 0x80, 0x3b, 0x9f, 0x73, 0xfe, 0x3d, 0xc7, 0x28],
"Crystal flavor": [0x00, 0x00, 0xde, 0x5f, 0x8c, 0x31, 0x02, 0x28, 0x80, 0x3b, 0xff, 0x77, 0x6d, 0x3a, 0x02, 0x28,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x80, 0x3b, 0xbf, 0x57, 0x4f, 0x79, 0x02, 0x28,
0x80, 0x3b, 0xff, 0x7f, 0x00, 0x40, 0x00, 0x40, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xde, 0x5f, 0x52, 0x79, 0x52, 0x79, 0x80, 0x3b, 0xad, 0x35, 0x5a, 0x6b, 0xc6, 0x18],
"Potato flavor": [0x00, 0x00, 0xde, 0x5f, 0x3f, 0x67, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x7b, 0x55, 0x4e, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x80, 0x3b, 0xbf, 0x57, 0x4f, 0x79, 0x25, 0x14,
0x80, 0x3b, 0x48, 0x10, 0x57, 0x4a, 0x1b, 0x57, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0x48, 0x10, 0x18, 0x19, 0x18, 0x15, 0x80, 0x3b, 0xff, 0x73, 0x1c, 0x57, 0xca, 0x28],
"Lemon-lime flavor": [0x00, 0x00, 0xde, 0x5f, 0x3f, 0x67, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x5b, 0x9e, 0x17, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x80, 0x3b, 0xbf, 0x57, 0x25, 0x0a, 0x25, 0x14,
0x80, 0x3b, 0x25, 0x0a, 0xfd, 0x37, 0xaf, 0x17, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0x25, 0x0a, 0xcf, 0x17, 0xcf, 0x17, 0x80, 0x3b, 0x25, 0x0a, 0xcf, 0x17, 0xc7, 0x28],
"Raisin flavor": [0x00, 0x00, 0xde, 0x5f, 0x3f, 0x67, 0x25, 0x14, 0x80, 0x3b, 0x94, 0x61, 0x0a, 0x41, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x80, 0x3b, 0xbf, 0x57, 0x4f, 0x79, 0x25, 0x14,
0x80, 0x3b, 0xfb, 0x6e, 0x44, 0x18, 0xc7, 0x20, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xfb, 0x6e, 0x95, 0x20, 0x95, 0x20, 0x80, 0x3b, 0xfb, 0x6e, 0x0c, 0x55, 0x84, 0x20],
"Chocolate flavor": [0x00, 0x00, 0xde, 0x5f, 0x8e, 0x09, 0x43, 0x0c, 0x80, 0x3b, 0x37, 0x3a, 0x10, 0x21, 0x43, 0x0c,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x80, 0x3b, 0xbf, 0x57, 0x4f, 0x79, 0x43, 0x0c,
0x80, 0x3b, 0x3c, 0x57, 0x25, 0x0c, 0x8a, 0x10, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0x3c, 0x57, 0x38, 0x32, 0x38, 0x32, 0x80, 0x3b, 0xd2, 0x31, 0x88, 0x14, 0x43, 0x0c],
"Ginger Ale flavor": [0x00, 0x00, 0xde, 0x5f, 0x8e, 0x09, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x6f, 0x6d, 0x3a, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xbf, 0x57, 0xed, 0x12, 0x25, 0x14,
0x80, 0x3b, 0x9f, 0x57, 0xed, 0x12, 0x80, 0x11, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0x9f, 0x57, 0xb6, 0x09, 0xb6, 0x09, 0x80, 0x3b, 0x9f, 0x57, 0x80, 0x11, 0xe0, 0x00],
"Orange Cream flavor": [0x00, 0x00, 0xde, 0x5f, 0x8e, 0x09, 0x25, 0x14, 0x80, 0x3b, 0x9f, 0x6b, 0x9f, 0x2a, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xbf, 0x57, 0x5f, 0x12, 0x25, 0x14,
0x80, 0x3b, 0xda, 0x09, 0x3f, 0x53, 0x5f, 0x12, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xda, 0x09, 0x3f, 0x53, 0x3f, 0x53, 0x80, 0x3b, 0x9f, 0x6b, 0x5f, 0x12, 0x89, 0x00],
"Amethyst flavor": [0x00, 0x00, 0xde, 0x5f, 0x8e, 0x09, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x77, 0xf2, 0x41, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xdf, 0x67, 0x3c, 0x78, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x73, 0xaf, 0x40, 0x3c, 0x78, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xde, 0x73, 0x1f, 0x68, 0x1f, 0x68, 0x80, 0x3b, 0x7f, 0x67, 0x3c, 0x78, 0xaf, 0x40],
"Choco Cream flavor": [0x00, 0x00, 0xde, 0x5f, 0x8e, 0x09, 0x25, 0x14, 0x28, 0x21, 0xee, 0x39, 0x08, 0x25, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xdf, 0x67, 0x4f, 0x79, 0x25, 0x14,
0x80, 0x3b, 0xc8, 0x28, 0x18, 0x6b, 0xdf, 0x77, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xc8, 0x28, 0x70, 0x4b, 0x70, 0x4b, 0x80, 0x3b, 0xdf, 0x7b, 0xf8, 0x5e, 0x06, 0x29],
"Doomed flavor": [0x00, 0x00, 0x1f, 0x00, 0x8e, 0x09, 0x25, 0x14, 0x00, 0x00, 0x19, 0x00, 0x0a, 0x00, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xdf, 0x67, 0x1f, 0x00, 0x25, 0x14,
0x80, 0x3b, 0x1f, 0x00, 0x4a, 0x29, 0x2a, 0x29, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0x19, 0x18, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3b, 0xef, 0x3d, 0x6b, 0x2d, 0x84, 0x10],
"Farm flavor": [0x00, 0x00, 0xbd, 0x32, 0x8e, 0x09, 0x4a, 0x04, 0x00, 0x00, 0xde, 0x32, 0x1a, 0x1e, 0x4a, 0x04,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xdf, 0x67, 0x7f, 0x02, 0x4a, 0x04,
0x80, 0x3b, 0x4a, 0x00, 0x1f, 0x3b, 0x9d, 0x2e, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0x5f, 0x43, 0xbb, 0x3a, 0xbb, 0x3a, 0x80, 0x3b, 0x5e, 0x02, 0x36, 0x01, 0xf0, 0x04],
"Inverted flavor": [0x00, 0x00, 0x00, 0x00, 0x8e, 0x09, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x00, 0xff, 0x7f,
0x80, 0x3b, 0x00, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0x80, 0x3b, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x00],
"Negative flavor": [0x00, 0x00, 0xff, 0x7f, 0x8e, 0x09, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x00,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x00,
0x80, 0x3b, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3b, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00],
"Astral flavor": [0x00, 0x00, 0x5b, 0x7f, 0x8e, 0x09, 0x21, 0x14, 0x00, 0x00, 0xff, 0x77, 0xed, 0x58, 0x21, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xfa, 0x7a, 0x92, 0x6c, 0x21, 0x14,
0x80, 0x3b, 0x5b, 0x7f, 0xed, 0x58, 0x87, 0x38, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0x5b, 0x7f, 0xd3, 0x79, 0xd3, 0x79, 0x80, 0x3b, 0xfa, 0x7a, 0xa8, 0x38, 0x21, 0x1c],
"Sunflower flavor": [0x00, 0x00, 0xdf, 0x63, 0x8e, 0x09, 0x65, 0x04, 0x00, 0x00, 0xbf, 0x57, 0xb3, 0x15, 0x65, 0x04,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xdf, 0x57, 0x9a, 0x12, 0x65, 0x04,
0x80, 0x3b, 0xdf, 0x63, 0x1c, 0x37, 0xdf, 0x37, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xdf, 0x63, 0x70, 0x09, 0x70, 0x09, 0x80, 0x3b, 0xdf, 0x57, 0xdb, 0x26, 0x4e, 0x15],
"Encore flavor": [0x00, 0x00, 0xff, 0x7f, 0x73, 0x4e, 0x84, 0x1c, 0x00, 0x00, 0xbd, 0x7f, 0x8b, 0x45, 0x84, 0x1c,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0x5f, 0x6f, 0x4a, 0x59, 0x84, 0x1c,
0x80, 0x3b, 0xff, 0x7f, 0x12, 0x7e, 0xbf, 0x27, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xff, 0x7f, 0x7f, 0x3c, 0x7f, 0x3c, 0x80, 0x3b, 0x5f, 0x6f, 0x7e, 0x40, 0x31, 0x1c],
"Experimental flavor": [0x00, 0x00, 0x9e, 0x5f, 0x80, 0x65, 0x64, 0x08, 0x00, 0x00, 0xff, 0x77, 0x2c, 0x15, 0x64, 0x08,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0x35, 0x7f, 0x0a, 0x7a, 0x64, 0x08,
0x80, 0x3b, 0x9e, 0x5f, 0x50, 0x19, 0xec, 0x10, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0x9e, 0x5f, 0x4a, 0x7a, 0x4a, 0x7a, 0x80, 0x3b, 0x35, 0x7f, 0x47, 0x76, 0xe4, 0x38],
"Blueblue flavor": [0x00, 0x00, 0x97, 0x7b, 0x00, 0x4c, 0x21, 0x14, 0x00, 0x00, 0x0c, 0x66, 0xc3, 0x28, 0x21, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0x08, 0x6e, 0x24, 0x55, 0x21, 0x14,
0x80, 0x3b, 0x97, 0x7b, 0x82, 0x20, 0x62, 0x45, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0x97, 0x7b, 0x27, 0x71, 0x27, 0x71, 0x80, 0x3b, 0x08, 0x6e, 0x43, 0x49, 0x82, 0x1c],
"Deeppurple flavor": [0x00, 0x00, 0xfb, 0x7a, 0x13, 0x4c, 0x25, 0x10, 0x00, 0x00, 0x16, 0x69, 0x8b, 0x40, 0x25, 0x10,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0xeb, 0x33, 0x73, 0x70, 0x2b, 0x44, 0x21, 0x14,
0x80, 0x3b, 0xfb, 0x7a, 0x29, 0x30, 0x4c, 0x44, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xfb, 0x7a, 0x17, 0x6c, 0x17, 0x6c, 0x80, 0x3b, 0x73, 0x70, 0x6c, 0x48, 0x45, 0x1c],
"Rivalry flavor": [0x00, 0x00, 0xfe, 0x5e, 0x0c, 0x7c, 0x25, 0x04, 0x00, 0x00, 0x5e, 0x1d, 0x70, 0x0c, 0x25, 0x04,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0xeb, 0x33, 0x15, 0x7f, 0xb3, 0x14, 0x25, 0x04,
0x80, 0x3b, 0xfe, 0x5e, 0xea, 0x59, 0x6d, 0x6a, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xfe, 0x5e, 0xc0, 0x6d, 0xc0, 0x6d, 0x80, 0x3b, 0x15, 0x7f, 0x86, 0x55, 0xe4, 0x34],
"Sugar flavor": [0x00, 0x00, 0x9c, 0x73, 0x39, 0x67, 0x63, 0x0c, 0x00, 0x00, 0x9c, 0x73, 0x52, 0x4a, 0x63, 0x0c,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0xeb, 0x33, 0x9c, 0x73, 0x4a, 0x29, 0x63, 0x0c,
0x80, 0x3b, 0x42, 0x08, 0xff, 0x7f, 0x39, 0x67, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0x9c, 0x73, 0x42, 0x08, 0x42, 0x08, 0xe0, 0x33, 0x9c, 0x73, 0xf7, 0x5e, 0x8c, 0x31],
"Faith flavor": [0x00, 0x00, 0xfe, 0x77, 0x3f, 0x67, 0x45, 0x10, 0x00, 0x00, 0xbe, 0x63, 0x7b, 0x3e, 0x45, 0x10,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0xeb, 0x33, 0x5d, 0x43, 0x9f, 0x40, 0x45, 0x10,
0xec, 0x33, 0xfe, 0x77, 0xdd, 0x72, 0x3c, 0x66, 0x80, 0x3b, 0x4a, 0x29, 0x08, 0x21, 0xa5, 0x14,
0x00, 0x00, 0xdf, 0x77, 0x1d, 0x28, 0x1d, 0x28, 0xe0, 0x33, 0xdf, 0x6a, 0x9f, 0x51, 0x4e, 0x1c],
"Wisdom flavor": [0x00, 0x00, 0xfe, 0x77, 0x80, 0x19, 0x40, 0x04, 0x00, 0x00, 0xb3, 0x46, 0x89, 0x1d, 0x40, 0x04,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0xeb, 0x33, 0xb3, 0x46, 0x20, 0x1d, 0x40, 0x04,
0xec, 0x33, 0xc1, 0x10, 0x24, 0x2e, 0x80, 0x25, 0x80, 0x3b, 0x4a, 0x29, 0x08, 0x21, 0xa5, 0x14,
0x00, 0x00, 0xfe, 0x77, 0x29, 0x25, 0x29, 0x25, 0xe0, 0x33, 0xcc, 0x46, 0x24, 0x2e, 0x20, 0x11],
"Hamburger flavor": [0x00, 0x00, 0xbf, 0x6b, 0x3f, 0x67, 0x25, 0x14, 0x80, 0x3b, 0xdc, 0x36, 0xd1, 0x19, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x80, 0x3b, 0xbf, 0x6b, 0xfd, 0x1a, 0x25, 0x14,
0x80, 0x3b, 0x00, 0x00, 0x54, 0x08, 0x9f, 0x2f, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xff, 0x77, 0x0d, 0x1b, 0x0d, 0x1b, 0x80, 0x3b, 0x50, 0x11, 0x16, 0x2e, 0x85, 0x10],
"Spearmint flavor": [0x00, 0x00, 0xde, 0x5f, 0x07, 0x1f, 0x25, 0x14, 0x00, 0x00, 0xff, 0x77, 0x8f, 0x3e, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xbf, 0x57, 0x4e, 0x7d, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xa6, 0x1e, 0x6e, 0x3f, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xde, 0x5f, 0x50, 0x71, 0x50, 0x71, 0x80, 0x3b, 0xf9, 0x67, 0x4d, 0x3b, 0x88, 0x24],
"Tuna flavor": [0x00, 0x00, 0xde, 0x5f, 0x07, 0x1f, 0x25, 0x14, 0x00, 0x00, 0xff, 0x77, 0x93, 0x42, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xbf, 0x57, 0x4e, 0x7d, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0x58, 0x29, 0x14, 0x21, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xde, 0x5f, 0x52, 0x79, 0x52, 0x79, 0x80, 0x3b, 0x1f, 0x63, 0x9b, 0x31, 0x68, 0x14],
"Milk flavor": [0x00, 0x00, 0xde, 0x5f, 0x07, 0x1f, 0x25, 0x14, 0x00, 0x00, 0xff, 0x77, 0x73, 0x4e, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xbf, 0x57, 0x4e, 0x7d, 0x25, 0x14,
0x80, 0x3b, 0x42, 0x08, 0xb5, 0x4e, 0x7b, 0x67, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xde, 0x5f, 0x52, 0x79, 0x52, 0x79, 0x80, 0x3b, 0xbd, 0x77, 0xf7, 0x5e, 0xc7, 0x28],
"Peppermint flavor": [0x00, 0x00, 0xfe, 0x73, 0xde, 0x5f, 0x25, 0x14, 0x45, 0x1a, 0x7f, 0x4e, 0xd9, 0x00, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xff, 0x7f, 0xd9, 0x00, 0x25, 0x14,
0x80, 0x3b, 0x42, 0x08, 0xff, 0x7f, 0xd9, 0x00, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xff, 0x7f, 0x13, 0x18, 0x13, 0x00, 0x80, 0x3b, 0xff, 0x7f, 0xdf, 0x18, 0x68, 0x14],
"Toothpaste flavor": [0x00, 0x00, 0xfe, 0x73, 0xde, 0x5f, 0x25, 0x14, 0x45, 0x1a, 0xf3, 0x7f, 0x20, 0x7f, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xff, 0x7f, 0x20, 0x7f, 0x25, 0x14,
0x80, 0x3b, 0x42, 0x08, 0xec, 0x7f, 0x26, 0x7f, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xff, 0x7f, 0x66, 0x7e, 0x66, 0x7e, 0x80, 0x3b, 0xf3, 0x7f, 0x20, 0x67, 0x68, 0x14],
"Barbecue flavor": [0x00, 0x00, 0xde, 0x5f, 0x4c, 0x00, 0x06, 0x00, 0x45, 0x1a, 0xd3, 0x00, 0x4c, 0x00, 0x06, 0x00,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xd3, 0x00, 0x0c, 0x00, 0x06, 0x00,
0x80, 0x3b, 0xff, 0x7f, 0x0c, 0x00, 0x99, 0x01, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0x7f, 0x02, 0x4c, 0x00, 0x4c, 0x00, 0x80, 0x3b, 0xb0, 0x14, 0x4c, 0x00, 0x06, 0x00],
"Lavender flavor": [0x00, 0x00, 0xde, 0x5f, 0x39, 0x7f, 0x25, 0x14, 0x45, 0x1a, 0xff, 0x77, 0x93, 0x3e, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x00, 0x00, 0xbf, 0x57, 0x79, 0x7e, 0x25, 0x14,
0x80, 0x3b, 0x86, 0x10, 0x39, 0x7f, 0x3f, 0x7f, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x00, 0x00, 0xde, 0x5f, 0x73, 0x7e, 0x73, 0x7e, 0x80, 0x3b, 0x3f, 0x7f, 0x39, 0x7f, 0x25, 0x14],
"Apple flavor": [0x00, 0x00, 0xde, 0x5f, 0x1F, 0x00, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x77, 0x6d, 0x3a, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x80, 0x3b, 0xbf, 0x57, 0x4f, 0x79, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xdf, 0x18, 0x19, 0x00, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xde, 0x5f, 0x52, 0x79, 0x52, 0x79, 0x80, 0x3b, 0x3f, 0x67, 0xdf, 0x18, 0xc7, 0x28],
"Orange flavor": [0x00, 0x00, 0xde, 0x5f, 0x9F, 0x01, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x77, 0x6d, 0x3a, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x80, 0x3b, 0xbf, 0x57, 0x4f, 0x79, 0x25, 0x14,
0x80, 0x3b, 0x0c, 0x00, 0x9f, 0x19, 0x7f, 0x1a, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xde, 0x5f, 0x79, 0x02, 0x79, 0x02, 0x80, 0x3b, 0x3f, 0x67, 0xff, 0x2a, 0xc7, 0x28],
"Toilet flavor": [0x00, 0x00, 0xde, 0x5f, 0xff, 0x7f, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x67, 0x93, 0x01, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x80, 0x3b, 0xbf, 0x57, 0x4f, 0x79, 0x25, 0x14,
0x80, 0x3b, 0x32, 0x01, 0x33, 0x7f, 0x66, 0x7e, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0x8c, 0x01, 0x3f, 0x1b, 0x3f, 0x1b, 0x80, 0x3b, 0xff, 0x7f, 0x9c, 0x7f, 0xc7, 0x28],
"Peach flavor": [0x00, 0x00, 0xde, 0x5f, 0x3f, 0x1b, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x77, 0x6d, 0x3a, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x80, 0x3b, 0xbf, 0x57, 0x4f, 0x79, 0x25, 0x14,
0x80, 0x3b, 0xc6, 0x00, 0x7f, 0x32, 0x3f, 0x33, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xde, 0x5f, 0x52, 0x79, 0x52, 0x79, 0x80, 0x3b, 0x3f, 0x4f, 0x7f, 0x32, 0xc7, 0x28],
"Bubblegum flavor": [0x00, 0x00, 0xde, 0x5f, 0x3f, 0x67, 0x25, 0x14, 0x80, 0x3b, 0x3f, 0x67, 0x7f, 0x7e, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x80, 0x3b, 0xbf, 0x57, 0x4f, 0x79, 0x25, 0x14,
0x80, 0x3b, 0x86, 0x10, 0x3f, 0x7f, 0x79, 0x7e, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0x86, 0x10, 0x9f, 0x65, 0x9f, 0x65, 0x80, 0x3b, 0x3f, 0x7f, 0x7f, 0x7e, 0xc7, 0x28],
"Lemonade flavor": [0x00, 0x00, 0xde, 0x5f, 0x3f, 0x67, 0x25, 0x14, 0x80, 0x3b, 0xde, 0x63, 0x1e, 0x5f, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x80, 0x3b, 0x3f, 0x67, 0x4e, 0x7d, 0x25, 0x14,
0x80, 0x3b, 0xc6, 0x00, 0x1e, 0x5f, 0xff, 0x4f, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xde, 0x5f, 0x52, 0x79, 0x52, 0x79, 0x80, 0x3b, 0x9e, 0x5f, 0x1e, 0x5f, 0xc7, 0x28],
"Garlic flavor": [0x00, 0x00, 0xff, 0x03, 0xff, 0x03, 0x25, 0x14, 0x80, 0x3b, 0xff, 0x33, 0x19, 0x64, 0x25, 0x14,
0x80, 0x3b, 0xde, 0x5f, 0xbf, 0x4c, 0xbf, 0x4c, 0x80, 0x3b, 0xff, 0x1b, 0x13, 0x4c, 0x25, 0x14,
0x80, 0x3b, 0xc6, 0x00, 0x79, 0x02, 0x3f, 0x1b, 0x80, 0x3b, 0xe7, 0x1c, 0xa5, 0x14, 0x42, 0x08,
0x80, 0x3b, 0xde, 0x5f, 0x13, 0x4c, 0x13, 0x4c, 0x80, 0x3b, 0xff, 0x33, 0x19, 0x64, 0xc7, 0x28]
}
vanilla_flavor_pointers = {
"Mint flavor": [0x202008, 0x34B000, 0x34B110],
"Strawberry flavor": [0x202048, 0x34B040, 0x34B112],
"Banana flavor": [0x202088, 0x34B080, 0x34B114],
"Peanut flavor": [0x2020C8, 0x34B0C0, 0x34B116]
}
def create_flavors(world: "EarthBoundWorld") -> None:
"""Shuffle flavors (textbox palettes). Vanilla flavors are copied from the ROM itself and not stored here."""
world.flavor_text = []
world.flavor_pointer = [0x01F72B, 0x01F746, 0x01F761, 0x01F77C]
flavor_num = 0
for flavor in world.available_flavors:
encoded_flavor = []
for char in flavor:
encoded_flavor.extend(eb_text_table[char])
encoded_flavor.extend([0x00])
world.flavor_text.append(encoded_flavor)
flavor_num += 1

View File

@@ -0,0 +1,365 @@
from ..game_data.local_data import item_id_table, character_item_table, party_id_nums
from ..game_data.text_data import text_encoder
from ..game_data.static_location_data import location_groups
from ..modules.shopsanity import shop_locations
from ..Options import ShopRandomizer, MagicantMode
import struct
from BaseClasses import Location
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .. import EarthBoundWorld
from ..Rom import LocalRom
def setup_hints(world: "EarthBoundWorld") -> None:
hint_types = [
# gives a hint for a specific out of the way location in this player's world, regardless of what item it is
"item_at_location",
"region_progression_check", # woth or foolish hint, checks specific location groups of this world so as to be more helpful.
"hint_for_good_item", # gives the exact location and sender of a good item for the local player
"item_in_local_region", # Hints a random item that can be found in a specific local location group
"prog_item_at_region", # Hints the region that a good item can be found for this player
"joke_hint", # Doesn't hint anything
"dungeon_location" # Hints what dungeon can be found at a specific entrance
]
world.in_game_hint_types = []
world.hinted_locations = {}
world.hinted_items = {}
world.hinted_regions = {}
world.hinted_dungeons = {}
# may not need to be world.
world.local_hintable_locations = [
"Onett - Mayor Pirkle",
"Onett - South Road Present",
"Onett - Meteor Item",
"Onett - Treehouse Guy",
"Twoson - Orange Kid Donation",
"Twoson - Everdred Meeting",
"Twoson - Apple Kid Invention",
"Fourside - Post-Moonside Delivery",
"Lost Underworld - Talking Rock",
"Dungeon Man - 2F Hole Present",
"Poo - Starting Item",
"Summers - Magic Cake",
"Deep Darkness - Teleporting Monkey",
"Twoson - Insignificant Location",
"Peaceful Rest Valley - North Side Present",
"Twoson - Paula's Mother",
"Happy-Happy Village - Prisoner",
"Threed - Boogey Tent Trashcan",
"Threed - Zombie Prisoner",
"Grapefruit Falls - Saturn Cave Present",
"Saturn Valley - Saturn Coffee",
"Dusty Dunes - South Side Present",
"Stonehenge - Tony Item",
"Stonehenge - Kidnapped Mr. Saturn",
"Stonehenge - Dead End Present",
"Stonehenge - Near End of the Maze Present",
"Stonehenge - Bridge Room East Balcony Present",
"Gold Mine - B1F Isolated Present",
"Fourside - Venus Gift",
"Fourside - Bakery 2F Gift",
"Fourside - Department Store Blackout",
"Monotoli Building - Monotoli Character",
"Dungeon Man - 1F Exit Ledge Present",
"Deep Darkness - Barf Character",
"Lumine Hall - B1F West Alcove Present",
"Cave of the Present - Star Master",
"Cave of the Present - Broken Phase Distorter",
"Fire Spring - 1st Cave Present",
"Tenda Village - Tenda Tea",
"Deep Darkness - Barf Character",
"Dalaam - Trial of Mu",
"Pyramid - Northwest Door Sarcophagus"
]
world.local_hintable_items = [
"Franklin Badge",
"Key to the Shack",
"Key to the Cabin",
"Key to the Tower",
"Key to the Locker",
"Bad Key Machine",
"Pencil Eraser",
"Eraser Eraser",
"UFO Engine",
"Yogurt Dispenser",
"Zombie Paper",
"King Banana",
"Signed Banana",
"Tendakraut",
"Jar of Fly Honey",
"Wad of Bills",
"Tiny Ruby",
"Diamond",
"Meteorite Piece",
"Hieroglyph Copy",
"Piggy Nose",
"Carrot Key",
"Police Badge",
"Letter For Tony",
"Mining Permit",
"Contact Lens",
"Insignificant Item",
"Pak of Bubble Gum",
"Sea Pendant",
"Shyness Book",
"Hawk Eye",
"Ness",
"Paula",
"Jeff",
"Poo",
"Onett Teleport",
"Twoson Teleport",
"Happy-Happy Village Teleport",
"Threed Teleport",
"Saturn Valley Teleport",
"Dusty Dunes Teleport",
"Fourside Teleport",
"Winters Teleport",
"Summers Teleport",
"Scaraba Teleport",
"Dalaam Teleport",
"Deep Darkness Teleport",
"Tenda Village Teleport",
"Lost Underworld Teleport"
]
world.joke_hints = [
"you can find 6 hint shops around the world.",
"if you want to hint for an item, use !hint in your text client!",
"the mouse at the Fourside Department Store knows what the Spook has.",
"my business may be a scam.",
"there's a guy near Threed's hotel who saw the Zombies take someone away.",
"you can use the Y button to run or mash through text.",
"you can submit custom window flavors in the game's Archipelago thread.",
"you can buy a ruler at the Fourside Department Store.",
"you can store a lot of items in your backpack with the R button.",
"Giygas's guard may actually be from another seed...",
"if you have multiple of something important, you can throw away the extras.",
"the chicken crossed the road because it was trying to use Teleport α.",
"Figment's Ice Palace can probably be found in Ice Palace.",
"you can talk to the hint shop owners for a hint.",
"deliveryman are bad at using keys.",
"PK scramble is a pretty good time.",
"Apple Kid researched the Power of the Earth with Dr. Andonuts.",
"you can find beta Archipelago games on the Archipelago discord.",
"you can randomize EarthBound with Archipelago.",
"hint prices double with each one bought.",
"you probably should have kept your money.",
"there's a secret option to plando Lumine Hall's text.",
"this isn't a very good hint.",
"some hints are good hints.\n@But not this one."
]
hintable_location_groups = location_groups.copy()
if world.options.shop_randomizer != ShopRandomizer.option_shopsanity:
hintable_location_groups["Onett"] = hintable_location_groups["Onett"] - shop_locations
hintable_location_groups["Twoson"] = hintable_location_groups["Twoson"] - shop_locations
hintable_location_groups["Happy-Happy Village"] = hintable_location_groups["Happy-Happy Village"] - shop_locations
hintable_location_groups["Threed"] = hintable_location_groups["Threed"] - shop_locations
hintable_location_groups["Grapefruit Falls"] = hintable_location_groups["Grapefruit Falls"] - shop_locations
hintable_location_groups["Saturn Valley"] = hintable_location_groups["Saturn Valley"] - shop_locations
hintable_location_groups["Dusty Dunes Desert"] = hintable_location_groups["Dusty Dunes Desert"] - shop_locations
hintable_location_groups["Winters"] = hintable_location_groups["Winters"] - shop_locations
hintable_location_groups["Dr. Andonuts's Lab"] = hintable_location_groups["Dr. Andonuts's Lab"] - shop_locations
hintable_location_groups["Fourside"] = hintable_location_groups["Fourside"] - shop_locations
hintable_location_groups["Moonside"] = hintable_location_groups["Moonside"] - shop_locations
hintable_location_groups["Summers"] = hintable_location_groups["Summers"] - shop_locations
hintable_location_groups["Dalaam"] = hintable_location_groups["Dalaam"] - shop_locations
hintable_location_groups["Scaraba"] = hintable_location_groups["Scaraba"] - shop_locations
hintable_location_groups["Deep Darkness"] = hintable_location_groups["Deep Darkness"] - shop_locations
hintable_location_groups["Lost Underworld"] = hintable_location_groups["Lost Underworld"] - shop_locations
hintable_location_groups["Magicant"] = hintable_location_groups["Magicant"] - shop_locations
del hintable_location_groups["Burglin Park"]
del hintable_location_groups["the Scaraba Bazaar"]
del hintable_location_groups["the Twoson Department Store"]
del hintable_location_groups["the Fourside Department Store"]
del hintable_location_groups["the Saturn Valley Shop"]
if world.options.magicant_mode >= 2:
del hintable_location_groups["Magicant"]
if not world.options.giygas_required:
del hintable_location_groups["Cave of the Past"]
if world.options.magicant_mode.value in [0, 3]:
world.local_hintable_items.append("Magicant Teleport")
for item in world.multiworld.precollected_items[world.player]:
if item.name in world.local_hintable_items:
world.local_hintable_items.remove(item.name)
for item in world.options.start_hints.value:
if item in world.local_hintable_items:
world.local_hintable_items.remove(item)
if world.starting_area_teleport in world.local_hintable_items:
world.local_hintable_items.remove(world.starting_area_teleport)
if world.local_hintable_items == []:
hint_types.remove("hint_for_good_item")
hint_types.remove("prog_item_at_region")
if not world.options.dungeon_shuffle:
hint_types.remove("dungeon_location")
if world.options.giygas_required:
world.local_hintable_locations.append("Cave of the Past - Present")
if world.options.magicant_mode == MagicantMode.option_psi_location:
world.local_hintable_locations.append("Magicant - Ness's Nightmare")
for i in range(6):
world.in_game_hint_types.append(world.random.choice(hint_types))
for index, hint in enumerate(world.in_game_hint_types):
if hint == "item_at_location":
location = world.random.choice(world.local_hintable_locations)
world.hinted_locations[index] = location
elif hint == "region_progression_check":
group, group_locs = world.random.choice(list(hintable_location_groups.items()))
world.hinted_regions[index] = group
elif hint == "hint_for_good_item" or hint == "prog_item_at_region":
item = world.random.choice(world.local_hintable_items)
world.hinted_items[index] = item
elif hint == "item_in_local_region":
group, group_locs = world.random.choice(list(hintable_location_groups.items()))
location = world.random.choice(sorted(group_locs))
world.hinted_regions[index] = group
world.hinted_locations[index] = location
elif hint == "dungeon_location":
dungeon = world.random.choice(list(world.dungeon_connections.keys()))
world.hinted_dungeons[index] = dungeon
def parse_hint_data(world: "EarthBoundWorld", location: Location, rom: "LocalRom", hint: str, index: int) -> None:
if hint == "item_at_location":
if world.player == location.item.player and location.item.name in character_item_table and location.item.name != "Photograph":
player_text = "your friend "
item_text = bytearray([0x1C, 0x02, party_id_nums[location.item.name]]) # In-game text command to display party member names
elif world.player == location.item.player:
player_text = "your "
if location.item.name in item_id_table:
item_text = bytearray([0x1C, 0x05, item_id_table[location.item.name]]) # In-game text command to display item names
else:
# if the item doesn't have a name (e.g it's PSI)
item_text = text_encoder(location.item.name, 128)
else:
player_text = f"{world.multiworld.get_player_name(location.item.player)}'s "
item_text = text_encoder(location.item.name, 128)
player_text = text_encoder(player_text, 255)
location_text = text_encoder(f" can be found at\n@{location.name}.", 255)
text = player_text + item_text + location_text
# [player]'s [item] can be found at [location].
text.append(0x02)
elif hint == "region_progression_check":
if world.progression_count == 1:
text = f"you can find {world.progression_count} important item at {world.hinted_area}."
else:
text = f"you can find {world.progression_count} important items at {world.hinted_area}."
text = text_encoder(text, 255)
text.append(0x02)
elif hint == "hint_for_good_item" or hint == "prog_item_at_region" or hint == "item_in_local_region":
if location.item.name in character_item_table and location.item.player == world.player and location.item.name != "Photograph":
item_text = text_encoder("your friend ", 255)
item_text.extend([0x1C, 0x02, party_id_nums[location.item.name]])
elif location.item.name in item_id_table and location.item.player == world.player:
item_text = text_encoder("your ", 255)
item_text.extend([0x1C, 0x05, item_id_table[location.item.name]])
elif location.item.player == world.player:
item_text = text_encoder(f"your {location.item.name}", 128)
else:
item_text = f"{world.multiworld.get_player_name(location.item.player)}'s {location.item.name}"
item_text = text_encoder(item_text, 255)
item_text.extend(text_encoder(" can be found ", 255))
if location.player != world.player:
player_text = text_encoder(f"by {world.multiworld.get_player_name(location.player)}\n", 255)
else:
player_text = text_encoder("\n", 255)
if hint == "hint_for_good_item":
location_text = text_encoder(f"@at {location.name}.", 255)
# your [item] can be found by [player] at [location]
else:
location_name_groups = world.multiworld.worlds[location.player].location_name_groups
possible_location_groups = [
group_name for group_name, group_locations in location_name_groups.items()
if location.name in group_locations and group_name != "Everywhere"
]
if not possible_location_groups:
if location.parent_region.name == "Menu":
area = ""
else:
area = f" near {location.parent_region.name}"
else:
area = f" near {world.random.choice(possible_location_groups)}"
location_text = text_encoder(f"@somewhere{area}.", 255)
# your [item] can be found by [player] somewhere near [location group]
text = item_text + player_text + location_text
text.append(0x02)
elif hint == "joke_hint":
text = world.random.choice(world.joke_hints)
text = text_encoder(text, 255)
text.append(0x02)
elif hint == "dungeon_location":
dungeon = world.hinted_dungeons[index]
text = f"{dungeon} leads to {world.dungeon_connections[dungeon]}."
text = text_encoder(text, 255)
text.append(0x02)
else:
text = 0x00
hint_addresses = [
0x070376,
0x0703A8,
0x0703DA,
0x07040C,
0x07043E,
0x070470
]
scoutable_hint_addresses = [
0x2EF3D5,
0x2EF3EB,
0x2EF401,
0x2EF417,
0x2EF42D,
0x2EF443
]
rom.write_bytes(0x310000 + world.hint_pointer, text)
rom.write_bytes(hint_addresses[world.hint_number], struct.pack("I", 0xF10000 + world.hint_pointer))
if hint in ["item_at_location", "hint_for_good_item"]:
rom.write_bytes(scoutable_hint_addresses[world.hint_number], struct.pack("I", 0xEEF451))
rom.write_bytes(0x310252 + (world.hint_number * 3), bytearray([0x01]))
world.hint_man_hints.append((location.address, location.player))
else:
rom.write_bytes(scoutable_hint_addresses[world.hint_number], struct.pack("I", 0xEEF4B2))
world.hint_man_hints.append(("NULL", 0))
world.hint_pointer = world.hint_pointer + len(text)
world.hint_number += 1
# Word on the street is that PLAYER's ITEM can be found at LOCATION
# Word on the street is that REGION has X important items
# Word on the street is that your ITEM can be found by PLAYER at LOCATION
# Word on the street is that PLAYER's ITEM can be found somewhere near REGION...
# Word on the street is that your ITEM can be found somewhere near REGION...
# char item hint?
# That's all for today.
# Like text part 1, extend 0x1C 0x05 0xItem Item, extend (the rest of the string)

View File

@@ -0,0 +1,305 @@
from ..Options import RandomizeOverworldMusic, RandomizeFanfares
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import EarthBoundWorld
from .Rom import LocalRom
town_songs = [
0x2E,
0x2F,
0x30,
0x38,
0x3B,
0x3D,
0x41,
0x42,
0x52,
0x80,
0x81,
0x82
]
overworld_songs = [
0x1D,
0x2C,
0x33,
0x34,
0x35,
0x36,
0x3E,
0x40,
0x43,
0x44,
0x6B,
0x72,
0x79,
0x88,
0x92,
0x97,
0x99,
0x9A,
0x9F
]
dungeon_songs = [
0x28,
0x29,
0x2A,
0x2B,
0x2D,
0x31,
0x32,
0x37,
0x2D,
0x3F,
0x46,
0x6A,
0x6C,
0x75,
0x84,
0x85
]
interior_songs = [
0x10,
0x11,
0x12,
0x13,
0x14,
0x15,
0x16,
0x17,
0x18,
0x19,
0x1A,
0x1C,
0x39,
0x70,
0x78,
0x7D,
0x83,
0x8C,
0x98
]
cutscene_songs = [
0x3A,
0x3C,
0x48,
0x4C,
0x4D,
0x4E,
0x4F,
0x50,
0x51,
0x53,
0x55,
0x56,
0x57,
0x58,
0x59,
0x5C,
0x77,
0x86,
0x90,
0x9B,
0xA9,
0xAA,
0xAB,
0xAC,
0xBB
]
wakeup_songs = [
0x1B,
0x5A,
0xAD,
0xB2,
0xB5,
0xBC,
]
rare_songs = [
0x1E,
0x1F,
0x45,
0x47,
0x70,
0x74,
0x76,
0x96,
]
non_randomized = [
0x01,
0x02,
0x03,
0x04,
0x06,
0x07,
0x0A,
0x0D,
0x0E,
0x20,
0x21,
0x22,
0x23,
0x24,
0x25,
0x26,
0x27,
0x4B,
0x5D,
0x5E,
0x5F,
0x73,
0x7A,
0x87,
0x89,
0x8A,
0x8B,
0x9D,
0xAE,
0xAF,
0xB6,
0xBE,
0xBF
]
battle_songs = [
0x49,
0x4A,
0x60,
0x61,
0x62,
0x63,
0x64,
0x65,
0x66,
0x67,
0x68,
0x69,
0x8D,
0x94,
0x95,
0xB9,
0xBA
]
def music_randomizer(world: "EarthBoundWorld", rom: "LocalRom") -> None:
fanfares = [
0x05,
0x08,
0x09,
0x0B,
0x0C,
0x0F,
0x54,
0x5B,
0x6D,
0x6E,
0x6F,
0x71,
0x7B,
0x7C,
0x7E,
0x7F,
0xB7,
0xB8,
0x8E,
0x8F,
0x91,
0x93,
0x9C,
0x9E,
0xA0,
0xA1,
0xA2,
0xA3,
0xA4,
0xA5,
0xA6,
0xA7,
0xA8,
0xB0,
0xB1,
0xB3,
0xB4,
0xB7,
0xB8,
0xBD
]
# Todo; options here
global_tracklist = list(range(0xC0)) # Initialize the list; this ideally should be vanilla
if world.options.randomize_fanfares == RandomizeFanfares.option_on_no_sound_stone_fanfares:
for i in range(9):
fanfares.remove(0xA0 + i)
if world.options.randomize_fanfares:
shuffled_fanfares = fanfares.copy()
world.random.shuffle(shuffled_fanfares)
for track_id, song in enumerate(fanfares):
global_tracklist[song] = shuffled_fanfares[track_id]
if world.options.randomize_battle_music:
shuffled_battle_songs = battle_songs.copy()
world.random.shuffle(shuffled_battle_songs)
for track_id, song in enumerate(battle_songs):
global_tracklist[song] = shuffled_battle_songs[track_id]
if world.options.randomize_overworld_music == RandomizeOverworldMusic.option_match_type:
shuffled_town_songs = town_songs.copy()
shuffled_overworld_songs = overworld_songs.copy()
shuffled_interior_songs = interior_songs.copy()
shuffled_dungeon_songs = dungeon_songs.copy()
shuffled_cutscene_songs = cutscene_songs.copy()
shuffled_rare_songs = rare_songs.copy()
shuffled_wakeup_songs = wakeup_songs.copy()
world.random.shuffle(shuffled_town_songs)
world.random.shuffle(shuffled_overworld_songs)
world.random.shuffle(shuffled_interior_songs)
world.random.shuffle(shuffled_dungeon_songs)
world.random.shuffle(shuffled_cutscene_songs)
world.random.shuffle(shuffled_rare_songs)
world.random.shuffle(shuffled_wakeup_songs)
for track_id, song in enumerate(town_songs):
global_tracklist[song] = shuffled_town_songs[track_id]
for track_id, song in enumerate(overworld_songs):
global_tracklist[song] = shuffled_overworld_songs[track_id]
for track_id, song in enumerate(interior_songs):
global_tracklist[song] = shuffled_interior_songs[track_id]
for track_id, song in enumerate(dungeon_songs):
global_tracklist[song] = shuffled_dungeon_songs[track_id]
for track_id, song in enumerate(cutscene_songs):
global_tracklist[song] = shuffled_cutscene_songs[track_id]
for track_id, song in enumerate(rare_songs):
global_tracklist[song] = shuffled_rare_songs[track_id]
for track_id, song in enumerate(wakeup_songs):
global_tracklist[song] = shuffled_wakeup_songs[track_id]
elif world.options.randomize_overworld_music == RandomizeOverworldMusic.option_full:
all_overworld_songs = (town_songs + overworld_songs + interior_songs +
dungeon_songs + cutscene_songs + rare_songs + wakeup_songs)
shuffled_overworld_songs = all_overworld_songs.copy()
world.random.shuffle(shuffled_overworld_songs)
for track_id, song in enumerate(all_overworld_songs):
global_tracklist[song] = shuffled_overworld_songs[track_id]
rom.write_bytes(0x17FDA0, bytearray(global_tracklist))
# Should the Melodies be fanfares?

View File

@@ -0,0 +1,52 @@
from ..game_data.palettes_organized import map_palettes, nice_palettes, ugly_palettes, nonsense_palettes
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import EarthBoundWorld
from .Rom import LocalRom
event_palettes = {
"Happy-Happy Village": 0x8367,
"Threed": 0x85A7,
"Deep Darkness": 0x8F67
}
def randomize_psi_palettes(world: "EarthBoundWorld", rom: "LocalRom") -> None:
spell_palettes = []
for i in range(34):
spell_palettes.append(0x0CF47F + (i * 8))
for i in range(7):
spell_palettes.append(0x360710 + (i * 8))
shuffled_palettes = spell_palettes.copy()
if world.options.randomize_psi_palettes == 1:
world.random.shuffle(shuffled_palettes)
elif world.options.randomize_psi_palettes == 2:
for i in range(0x010E):
rom.write_bytes(0x0CF47F + i, bytearray([world.random.randint(0x00, 0xFF)]))
for i in range(0x50):
rom.write_bytes(0x36F710 + i, bytearray([world.random.randint(0x00, 0xFF)]))
for index, pointer in enumerate(spell_palettes):
rom.copy_bytes(pointer, 8, shuffled_palettes[index])
def map_palette_shuffle(world: "EarthBoundWorld", rom: "LocalRom") -> None:
for i in range(168):
rom.copy_bytes(0x1A7CA7 + (i * 192), 191, 0x381000 + (i * 192))
for item in map_palettes:
choosable_palettes = nice_palettes[item]
if world.options.map_palette_shuffle > 1:
choosable_palettes += ugly_palettes[item]
if world.options.map_palette_shuffle > 2:
choosable_palettes += nonsense_palettes[item]
chosen_palette = world.random.choice(choosable_palettes)
rom.copy_bytes(0x381002 + (chosen_palette * 192), 29, 0x1A7CA9 + (map_palettes[item] * 192))
rom.copy_bytes(0x381022 + (chosen_palette * 192), 157, 0x1A7CC9 + (map_palettes[item] * 192)) # The event palette pointer is between these 2 blocks

View File

@@ -0,0 +1,515 @@
import struct
from ..Options import PSIShuffle
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import EarthBoundWorld
from .Rom import LocalRom
def shuffle_psi(world: "EarthBoundWorld") -> None:
world.offensive_psi_slots = [
"Special",
"Flash",
"Fire",
"Freeze",
"Thunder",
"Starstorm",
"Blast",
"Missile"
]
world.assist_psi_slots = [
"Hypnosis",
"Paralysis",
"Offense Up",
"Defense Down",
"Brainshock",
"Defense up",
"Drain",
"Disable",
"Stop",
"Neutralize"
]
world.shield_slots = [
"Shield",
"PSI Shield"
]
world.jeff_offense_items = []
world.jeff_assist_items = []
world.psi_address = {
"Special": [0x158A5F, 4],
"Flash": [0x158B4F, 4],
"Fire": [0x158A9B, 4],
"Freeze": [0x158AD7, 4],
"Thunder": [0x158B13, 4],
"Starstorm": [0x3503FC, 4],
"Shield": [0x158C21, 4],
"PSI Shield": [0x158C5D, 4],
"Hypnosis": [0x158CD5, 2],
"Paralysis": [0x158D11, 2],
"Offense Up": [0x158C99, 2],
"Defense Down": [0x158CB7, 2],
"Brainshock": [0x158D2F, 2],
"Blast": [0x35041A, 4],
"Missile": [0x350456, 4],
"Defense up": [0x350492, 2],
"Drain": [0x3504B0, 2],
"Disable": [0x3504Ce, 2],
"Stop": [0x3504EC, 2],
"Neutralize": [0x35050A, 2],
}
if world.options.psi_shuffle:
world.random.shuffle(world.offensive_psi_slots)
if world.options.psi_shuffle != PSIShuffle.option_extended:
adjust_psi_list(world.offensive_psi_slots, "Blast", 7)
adjust_psi_list(world.offensive_psi_slots, "Missile", 7)
if not world.options.allow_flash_as_favorite_thing:
if world.offensive_psi_slots[0] == "Flash":
adjust_psi_list(world.offensive_psi_slots, "Flash", world.random.randint(1, 5)) # Randomize which slot gets Flash
world.random.shuffle(world.assist_psi_slots)
if world.options.psi_shuffle != PSIShuffle.option_extended:
adjust_psi_list(world.assist_psi_slots, "Defense up", 10)
adjust_psi_list(world.assist_psi_slots, "Drain", 10)
adjust_psi_list(world.assist_psi_slots, "Disable", 10)
adjust_psi_list(world.assist_psi_slots, "Stop", 10)
adjust_psi_list(world.assist_psi_slots, "Neutralize", 10)
world.jeff_offense_items.extend(world.offensive_psi_slots[-2:])
world.jeff_assist_items.extend(world.assist_psi_slots[-5:])
world.offensive_psi_slots = world.offensive_psi_slots[:-2]
world.assist_psi_slots = world.assist_psi_slots[:-5]
world.random.shuffle(world.shield_slots)
shield_data = {key: world.psi_address[key] for key in world.shield_slots}
assist_data = {key: world.psi_address[key] for key in world.assist_psi_slots}
assist_data_plus = {key: world.psi_address[key] for key in world.jeff_assist_items}
offense_data_plus = {key: world.psi_address[key] for key in world.jeff_offense_items}
world.psi_address = {key: world.psi_address[key] for key in world.offensive_psi_slots}
world.psi_address.update(shield_data)
world.psi_address.update(assist_data)
world.psi_address.update(offense_data_plus)
world.psi_address.update(assist_data_plus)
world.psi_slot_data = [
[[0x09, 0x00], [0x0B, 0x00], [0x0D, 0x00], [0x0F, 0x00]], # Special
[[0x09, 0x01], [0x0B, 0x01], [0x0D, 0x01], [0x0F, 0x01]], # Flash
[[0x09, 0x00], [0x0B, 0x00], [0x0D, 0x00], [0x0F, 0x00]], # Fire
[[0x09, 0x01], [0x0B, 0x01], [0x0D, 0x01], [0x0F, 0x01]], # Freeze
[[0x09, 0x02], [0x0B, 0x02], [0x0D, 0x02], [0x0F, 0x02]], # Thunder
[[0x09, 0x00], [0x0B, 0x00], [0x0D, 0x00], [0x0F, 0x00]], # Starstorm
[[0x09, 0x00], [0x0B, 0x00], [0x0D, 0x00], [0x0F, 0x00]], # Shield
[[0x09, 0x00], [0x0B, 0x00], [0x0D, 0x00], [0x0F, 0x00]], # PSI Shield
[[0x09, 0x01], [0x0B, 0x01]], # Hypnosis
[[0x09, 0x02], [0x0B, 0x02]], # Paralysis
[[0x09, 0x01], [0x0B, 0x01]], # Offense Up
[[0x09, 0x02], [0x0B, 0x02]], # Defense Down
[[0x09, 0x01], [0x0B, 0x01]], # Brainshock
[[0x09, 0x00], [0x0B, 0x00], [0x0D, 0x00], [0x0F, 0x00]], # Blast
[[0x09, 0x00], [0x0B, 0x00], [0x0D, 0x00], [0x0F, 0x00]], # Missile
[[0x09, 0x01], [0x0B, 0x01]], # Defense up
[[0x09, 0x02], [0x0B, 0x02]], # Drain
[[0x09, 0x01], [0x0B, 0x01]], # Disable
[[0x09, 0x02], [0x0B, 0x02]], # Stop
[[0x09, 0x01], [0x0B, 0x01]], # Neutralize
]
world.psi_level_data = [
[[0x08, 0x00, 0x00], [0x16, 0x00, 0x00], [0x31, 0x00, 0x00], [0x4B, 0x00, 0x00]], # Special
[[0x12, 0x00, 0x00], [0x26, 0x00, 0x00], [0x3D, 0x00, 0x00], [0x43, 0x00, 0x00]], # Flash
[[0x00, 0x03, 0x00], [0x00, 0x13, 0x00], [0x00, 0x25, 0x00], [0x00, 0x40, 0x00]], # Fire
[[0x00, 0x01, 0x01], [0x00, 0x0B, 0x01], [0x00, 0x1F, 0x21], [0x00, 0x2E, 0x00]], # Freeze
[[0x00, 0x08, 0x01], [0x00, 0x19, 0x01], [0x00, 0x39, 0x29], [0x00, 0x00, 0x37]], # Thunder
[[0x00, 0x00, 0x00], [0x00, 0x00, 0x00], [0x00, 0x00, 0x00], [0x00, 0x00, 0x00]], # Starstorm
[[0x0C, 0x00, 0x0E], [0x00, 0x00, 0x0F], [0x22, 0x00, 0x10], [0x00, 0x00, 0x33]], # Shield
[[0x00, 0x06, 0x00], [0x00, 0x1B, 0x00], [0x00, 0x33, 0x00], [0x00, 0x3C, 0x00]], # PSI Shield
[[0x04, 0x00, 0x00], [0x1B, 0x00, 0x00]], # Hypnosis
[[0x0E, 0x00, 0x00], [0x1D, 0x00, 0x00]], # Paralysis
[[0x00, 0x15, 0x00], [0x00, 0x28, 0x00]], # Offense Up
[[0x00, 0x1D, 0x00], [0x00, 0x36, 0x00]], # Defense Down
[[0x00, 0x00, 0x18], [0x00, 0x00, 0x2C]], # Brainshock
# Level-up data needs to be zeroed out for these slots
[[0x00, 0x00, 0x00], [0x00, 0x00, 0x00], [0x00, 0x00, 0x00], [0x00, 0x00, 0x00]], # Blast
[[0x00, 0x00, 0x00], [0x00, 0x00, 0x00], [0x00, 0x00, 0x00], [0x00, 0x00, 0x00]], # Missile
[[0x00, 0x00, 0x00], [0x00, 0x00, 0x00]], # Defense up
[[0x00, 0x00, 0x00], [0x00, 0x00, 0x00]], # Drain
[[0x00, 0x00, 0x00], [0x00, 0x00, 0x00]], # Disable
[[0x00, 0x00, 0x00], [0x00, 0x00, 0x00]], # Stop
[[0x00, 0x00, 0x00], [0x00, 0x00, 0x00]] # Neutralize
]
world.bomb_names = {
"Special": ["Psycho bomb", "Mad psycho bomb", "Psywave emitter", "Psywave blaster", "Broken radio"],
"Flash": ["Smoke bomb", "Flashbang", "Flash pan", "Digital camera", "Broken camera"],
"Freeze": ["Ice bomb", "Dry ice bomb", "Frost ray", "Freeze ray", "Broken minifridge"],
"Fire": ["Fire bomb", "Napalm bomb", "Blowtorch", "Flamethrower", "Broken grill"],
"Thunder": ["Electric bomb", "EMP bomb", "Shock coil", "Tesla coil", "Broken spring"],
"Starstorm": ["Comet bomb", "Nova bomb", "Meteor radar", "Star radar", "Broken radar"],
"Blast": ["Bomb", "Super bomb", "Bazooka", "Heavy bazooka", "Broken bazooka"],
"Missile": ["Rocket", "Nitro rocket", "Missile launcher", "Nitro launcher", "Broken vacuum"]
}
world.rocket_names = {
"Special": ["Psionic shard", "Psionic orb", "Psionic crystal"],
"Flash": ["Flashbulb", "Bright bulb", "Magna bulb"],
"Freeze": ["LN2 bottle", "LN2 jug", "LN2 bucket"],
"Fire": ["Flare", "Big flare", "Blitz flare"],
"Thunder": ["Sparkler", "Big sparkler", "Mega sparkler"],
"Starstorm": ["Meteor missile", "Star missile", "Nova missile"],
"Blast": ["Firecracker", "Big firecracker", "Super firecracker"],
"Missile": ["Bottle rocket", "Big bottle rocket", "Multibottle rocket"]
}
world.spray_names = {
"Hypnosis": "Chloroform spray",
"Paralysis": "Nerve spray",
"Offense Up": "Offense spray",
"Defense Down": "Weakness spray",
"Brainshock": "Confusion spray",
"Defense up": "Defense spray",
"Drain": "HP-straw",
"Disable": "Distraction spray",
"Stop": "Slime spray",
"Neutralize": "Shield-off spray"
}
world.broken_gadgets = {
"Hypnosis": ["Broken watch", "Broken screen"],
"Paralysis": ["Broken razor", "Broken fan"],
"Offense Up": ["Broken faucet", "Broken tuba"],
"Defense Down": ["Broken sprinkler", "Broken trombone"],
"Brainshock": ["Broken toaster", "Broken fryer"],
"Defense up": ["Broken nozzle", "Broken trumpet"],
"Drain": ["Broken hose", "Broken tube"],
"Disable": ["Broken machine", "Broken device"],
"Stop": ["Broken iron", "Broken steamer"],
"Neutralize": ["Broken pipe", "Broken board"]
}
world.broken_desc = {
"Hypnosis": [0x00EEEBA4, 0x00EEEBCC],
"Paralysis": [0x00EEEC06, 0x00EEEC28],
"Offense Up": [0x00EEEC4D, 0x00EEEC75],
"Defense Down": [0x00EEECA5, 0x00EEECCB],
"Brainshock": [0x00EEED00, 0x00EEED2B],
"Defense up": [0x00EEED5A, 0x00C53929],
"Drain": [0x00EEED8A, 0x00C538E0],
"Disable": [0x00C53772, 0x00EEEDB2],
"Stop": [0x00C53870, 0x00EEEE03],
"Neutralize": [0x00C53897, 0x00EEEE2F]
}
world.gadget_names = {
"Hypnosis": ["Hypno pendulum", "Hypno screen"],
"Paralysis": ["Nerve taser", "Nerve ray"],
"Offense Up": ["Offensalizer", "Offense shower"],
"Defense Down": ["Weakalizer", "Weakness shower"],
"Brainshock": ["Mind jammer", "Mind fryer"],
"Defense up": ["Defensalizer", "Defense shower"],
"Drain": ["HP-sucker", "Hungry HP-sucker"],
"Disable": ["Counter-PSI unit", "PSI-nullifier unit"],
"Stop": ["Slime generator", "Slime blaster"],
"Neutralize": ["Shield killer", "Neutralizer"]
}
world.starstorm_address = {
"Special": [0x002D, 0x003C],
"Flash": [0x011D, 0x012C],
"Fire": [0x0069, 0x0078],
"Freeze": [0x00A5, 0x00B4],
"Thunder": [0x00E1, 0x00F0],
"Starstorm": [0x013B, 0x014A],
"Blast": [0x0438, 0x0447],
"Missile": [0x0474, 0x0483]
}
world.starstorm_spell_id = {
"Special": [0x03, 0x04],
"Flash": [0x13, 0x14],
"Fire": [0x07, 0x08],
"Freeze": [0x0B, 0x0C],
"Thunder": [0x0F, 0x10],
"Starstorm": [0x15, 0x16],
"Blast": [0x48, 0x49],
"Missile": [0x4C, 0x4D]
}
world.jeff_addresses = [
0x156665, # Bomb
0x15668C, # Super Bomb
0x156443, # Bazooka
0x15646A, # Heavy bazooka
0x1551FB, # broken bazooka
0x1565F0, # Bottle Rocket
0x156617, # Big Bottle Rocket
0x15663E, # multi Bottle Rocket
]
world.gadget_addresses = [
[0, 0x1567EB], # defense shower
[0x156491, 0x1564B8], # HP-Sucker
[0x1563F5], # Counter-PSI Unit
[0x156506], # Slime Generator
[0x15641C, 0x156DB5], # Shield Killer
]
world.broken_gadget_addresses = [
0x155222,
0x1551D4,
0x15509C,
0x15515F,
0x155186
]
world.bomb_desc = {
"Special": [0x00EED4D8, 0x00EED521, 0x00EEDA4F, 0x00EEDA6B, 0x00EEDCFB],
"Flash": [0x00EED538, 0x00EED5AB, 0x00EEDA87, 0x00EEDAD7, 0x00EEDD1D],
"Fire": [0x00EED659, 0x00EED6CC, 0x00EEDB37, 0x00EEDB68, 0x00EEDD40],
"Freeze": [0x00EED744, 0x00EED79D, 0x00EEDB85, 0x00EEDBA2, 0x00EEDD64],
"Thunder": [0x00EED822, 0x00EED88E, 0x00EEDBBF, 0x00EEDBE4, 0x00EEDDA0],
"Starstorm": [0x00EED4D8, 0x00EED521, 0x00EEDA4F, 0x00EEDCD2, 0x00EEDDC3],
"Blast": [0x00C54EA7, 0x00C54EF7, 0x00C54AA1, 0x00C54AF5, 0x00C53908],
"Missile": [0x00EED9A5, 0x00EED9FF, 0x00EEDC09, 0x00EEDC40, 0x00EEDDEE]
}
world.rocket_desc = {
"Special": [0x00EEDE25, 0x00EEDE41, 0x00EEDE80],
"Flash": [0x00EEDEDC, 0x00EEDF37, 0x00EEDFAE],
"Fire": [0x00EEE013, 0x00EEE031, 0x00EEE061],
"Freeze": [0x00EEE098, 0x00EEE0B5, 0x00EEE11E],
"Thunder": [0x00EEE190, 0x00EEE1B6, 0x00EEE220],
"Starstorm": [0x00EEE28B, 0x00EEE2A7, 0x00EEE2DB],
"Blast": [0x00EEE344, 0x00EEE35B, 0x00EEE36E],
"Missile": [0x00C54E01, 0x00C54E20, 0x00C54E54]
}
world.spray_desc = {
"Hypnosis": 0x00EEE653,
"Paralysis": 0x00EEE688,
"Offense Up": 0x00EEE6B9,
"Defense Down": 0x00EEE705,
"Brainshock": 0x00EEE751,
"Defense up": 0x00C2558D,
"Drain": 0x00EEE802,
"Disable": 0x00EEE838,
"Stop": 0x00EEE7D0,
"Neutralize": 0x00EEE78F
}
world.gadget_desc = {
"Hypnosis": [0x00EEE87B, 0x00EEE8AA],
"Paralysis": [0x00EEE8DA, 0x00EEE905],
"Offense Up": [0x00EEE933, 0x00EEE97F],
"Defense Down": [0x00EEE9C2, 0x00EEEA0E],
"Brainshock": [0x00EEEA9D, 0x00EEEAD2],
"Defense up": [0x00EEEA51, 0x00C5519F],
"Drain": [0x00C54B67, 0x00C54BB0],
"Disable": [0x00C54A3A, 0x00EEEB5E],
"Stop": [0x00C54C32, 0x00EEEB09],
"Neutralize": [0x00C54A6C, 0x00C559D9]
}
world.bomb_actions = {
"Special": [0x01AC, 0x01AD, 0x01CF, 0x01D0],
"Flash": [0x01AE, 0x01AF, 0x01D1, 0x01D2],
"Fire": [0x01B0, 0x01B1, 0x01D3, 0x01D4],
"Freeze": [0x01B2, 0x01B3, 0x01D5, 0x01D6],
"Thunder": [0x01B4, 0x01B5, 0x01D7, 0x01D8],
"Starstorm": [0x01B6, 0x01B7, 0x01D9, 0x01DA],
"Blast": [0x00A7, 0x00A8, 0x0136, 0x0137],
"Missile": [0x01B8, 0x01B9, 0x01DB, 0x01DC]
}
world.missile_actions = {
"Special": [0x01BA, 0x01BB, 0x01BC],
"Flash": [0x01BD, 0x01BE, 0x01BF],
"Fire": [0x01C0, 0x01C1, 0x01C2],
"Freeze": [0x01C3, 0x01C4, 0x01C5],
"Thunder": [0x01C6, 0x01C7, 0x01C8],
"Starstorm": [0x01C9, 0x01CA, 0x01CB],
"Blast": [0x01CC, 0x01CD, 0x01CE],
"Missile": [0x00A3, 0x00A4, 0x00A5]
}
world.gadget_actions = {
"Hypnosis": [0x01E7, 0x01E8],
"Paralysis": [0x01E9, 0x01EA],
"Offense Up": [0x01EB, 0x01EC],
"Defense Down": [0x01ED, 0x01EE],
"Brainshock": [0x01EF, 0x01F0],
"Defense up": [0x00B7, 0x00B8],
"Drain": [0x00A1, 0x00B0],
"Disable": [0x009F, 0x01F1],
"Stop": [0x00A9, 0x01F2],
"Neutralize": [0x00A0, 0x00F7]
}
world.jeff_item_counts = [
5, # Bomb
3 # Bottle Rocket
]
world.gadget_counts = [
1, # defense shower
2, # HP-Sucker
1, # Counter PSI unit
1, # Slime generator
2 # Neutralizer
]
world.gadget_ids = [
[0x00, 0x9D],
[0x87, 0x88],
[0x83],
[0x8A],
[0x84, 0xC3]
]
world.broken_gadget_ids = [
0x0E, # Broken trumpet
0x0C, # Broken tube
0x04, # Broken machine
0x09, # Broken iron
0x0A # Broken pipe
]
world.jeff_item_names = [
world.bomb_names,
world.rocket_names
]
world.jeff_help_text = [
world.bomb_desc,
world.rocket_desc
]
world.attack_types = [
world.bomb_actions,
world.missile_actions
]
def write_psi(world: "EarthBoundWorld", rom: "LocalRom") -> None:
from ..game_data.text_data import text_encoder
psi_num = 0
for spell, (address, levels) in world.psi_address.items():
for i in range(levels):
rom.write_bytes(address + 9, bytearray(world.psi_slot_data[psi_num][i]))
rom.write_bytes(address + 6, bytearray(world.psi_level_data[psi_num][i]))
if psi_num == 0:
rom.write_bytes(address, bytearray([0x01]))
elif psi_num == 5 and i > 1:
rom.write_bytes(0x01C4AB + (0x9E * (i - 2)), struct.pack("H", world.starstorm_address[spell][i - 2])) # Menu spells
rom.write_bytes(0x01C536 + (0x78 * (i - 2)), bytearray([world.starstorm_spell_id[spell][i - 2]])) # What spell is controlled by the PSI flags
rom.write_bytes(0x2E957F + (0x11 * (i - 2)), bytearray([world.starstorm_spell_id[spell][i - 2]])) # The global Poo PSI text
rom.write_bytes(address + 9, bytearray(world.psi_slot_data[psi_num][i - 2]))
if spell == "Special" and psi_num != 0:
rom.write_bytes(address, bytearray([0x12]))
address += 15
if spell == "Starstorm" and i == 1:
address = 0x158B8B
# todo; expanded psi
# todo; animation for Starstorm L/D
# todo; swap enemy actions for Special?
# todo; cleanup stuff
psi_num += 1
# rom.write_bytes(0x2E957F + (0x11 * (i - 2)), bytearray([world.starstorm_spell_id[spell][i - 2]]))
# Starstorm spell for the item locally
rom.write_bytes(0x2EAE2E, bytearray([world.starstorm_spell_id[world.offensive_psi_slots[5]][0]]))
rom.write_bytes(0x2EAE38, bytearray([world.starstorm_spell_id[world.offensive_psi_slots[5]][1]]))
jeff_item_num = 0
jeff_item_index = 0
for item in world.jeff_offense_items:
for i in range(world.jeff_item_counts[jeff_item_num]):
address = world.jeff_addresses[jeff_item_index]
jeff_item_index += 1
name = world.jeff_item_names[jeff_item_num][item][i]
description = world.jeff_help_text[jeff_item_num][item][i]
name_encoded = text_encoder(name, 22)
name_encoded.extend(([0x00]))
rom.write_bytes(address, name_encoded)
rom.write_bytes(address + 35, struct.pack("I", description))
if "Broken" not in name: # broken items don't need attack data
attack = world.attack_types[jeff_item_num][item][i]
rom.write_bytes(address + 29, struct.pack("H", attack))
jeff_item_num += 1
jeff_item_num = 0
name = world.spray_names[world.jeff_assist_items[0]]
name_encoded = text_encoder(name, 22)
name_encoded.extend(([0x00]))
description = world.spray_desc[world.jeff_assist_items[0]]
address = 0x156887
action = world.gadget_actions[world.jeff_assist_items[0]][0]
rom.write_bytes(address, name_encoded)
rom.write_bytes(address + 35, struct.pack("I", description))
rom.write_bytes(address + 29, struct.pack("H", action))
for item in world.jeff_assist_items:
for i in range(world.gadget_counts[jeff_item_num]):
if jeff_item_num == 0:
i = 1
name = world.gadget_names[item][i]
name_encoded = text_encoder(name, 22)
name_encoded.extend(([0x00]))
description = world.gadget_desc[item][i]
action = world.gadget_actions[world.jeff_assist_items[jeff_item_num]][i]
address = world.gadget_addresses[jeff_item_num][i]
rom.write_bytes(address, name_encoded)
rom.write_bytes(address + 35, struct.pack("I", description))
rom.write_bytes(address + 29, struct.pack("H", action))
rom.write_bytes(description - 0xC00000 + 5, bytearray([world.gadget_ids[jeff_item_num][i]]))
jeff_item_num += 1
for i in range(5):
item = world.jeff_assist_items[i]
if i < 2:
level = 1
else:
level = 0
address = world.broken_gadget_addresses[i]
name = world.broken_gadgets[item][level]
description = world.broken_desc[item][level]
name_encoded = text_encoder(name, 22)
name_encoded.extend(([0x00]))
rom.write_bytes(address, name_encoded)
rom.write_bytes(address + 35, struct.pack("I", description))
rom.write_bytes(description - 0xC00000 + 5, bytearray([world.broken_gadget_ids[i]]))
rom.write_bytes(0x15A8EB, bytearray(struct.pack("H", world.gadget_actions[world.jeff_assist_items[4]][1])))
rom.write_bytes(0x15BB45, bytearray(struct.pack("H", world.gadget_actions[world.jeff_assist_items[0]][0])))
rom.write_bytes(0x15BBA5, bytearray(struct.pack("H", world.gadget_actions[world.jeff_assist_items[4]][0])))
rom.write_bytes(0x15DF41, bytearray(struct.pack("H", world.gadget_actions[world.jeff_assist_items[4]][0])))
rom.write_bytes(0x15DF3F, bytearray(struct.pack("H", world.gadget_actions[world.jeff_assist_items[4]][1])))
rom.write_bytes(0x15BA2B, bytearray(struct.pack("H", world.gadget_actions[world.jeff_assist_items[1]][0])))
rom.write_bytes(0x15C06D, bytearray(struct.pack("H", world.gadget_actions[world.jeff_assist_items[1]][1])))
def adjust_psi_list(psi_input: list[str], spell: str, index: int) -> None:
"""Move a spell in the PSI table to a different entry/slot"""
psi_input.insert(index, (psi_input.pop(psi_input.index(spell))))

View File

@@ -0,0 +1,449 @@
from ..game_data.local_data import psi_item_table, character_item_table, special_name_table, item_id_table, money_item_table
from ..game_data.text_data import calc_pixel_width, text_encoder
from ..game_data.static_location_data import location_ids
from ..Options import ShopRandomizer, MagicantMode, MonkeyCavesMode
from BaseClasses import ItemClassification, Location
import struct
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import EarthBoundWorld
from .Rom import LocalRom
high_purchase_areas = {
"Twoson",
"Fourside",
"Scaraba",
"Andonuts Lab Area" # long walk with no atm
}
shop_locations = {
"Onett Drugstore - Right Counter Slot 1",
"Onett Drugstore - Right Counter Slot 2",
"Onett Drugstore - Right Counter Slot 3",
"Onett Drugstore - Right Counter Slot 4",
"Onett Drugstore - Right Counter Slot 5",
"Onett Drugstore - Left Counter",
"Summers - Beach Cart",
"Onett Burger Shop - Slot 1",
"Onett Burger Shop - Slot 2",
"Onett Burger Shop - Slot 3",
"Onett Burger Shop - Slot 4",
"Onett Bakery - Slot 1",
"Onett Bakery - Slot 2",
"Onett Bakery - Slot 3",
"Onett Bakery - Slot 4",
"Twoson Department Store Burger Shop - Slot 1",
"Twoson Department Store Burger Shop - Slot 2",
"Twoson Department Store Burger Shop - Slot 3",
"Twoson Department Store Burger Shop - Slot 4",
"Twoson Department Store Bakery - Slot 1",
"Twoson Department Store Bakery - Slot 2",
"Twoson Department Store Bakery - Slot 3",
"Twoson Department Store Bakery - Slot 4",
"Twoson Department Store Top Floor - Right Counter Slot 1",
"Twoson Department Store Top Floor - Right Counter Slot 2",
"Twoson Department Store Top Floor - Right Counter Slot 3",
"Twoson Department Store Top Floor - Right Counter Slot 4",
"Twoson Department Store Top Floor - Right Counter Slot 5",
"Twoson Department Store Top Floor - Right Counter Slot 6",
"Twoson Department Store Top Floor - Left Counter Slot 1",
"Summers - Magic Cake Cart Shop Slot",
"Burglin Park Junk Shop - Slot 1",
"Burglin Park Junk Shop - Slot 2",
"Burglin Park Junk Shop - Slot 3",
"Burglin Park Junk Shop - Slot 4",
"Burglin Park Junk Shop - Slot 5",
"Burglin Park Junk Shop - Slot 6",
"Burglin Park Bread Stand - Slot 1",
"Burglin Park Bread Stand - Slot 2",
"Burglin Park Bread Stand - Slot 3",
"Burglin Park Bread Stand - Slot 4",
"Burglin Park Bread Stand - Slot 5",
"Burglin Park Bread Stand - Slot 6",
"Burglin Park - Banana Stand",
"Happy-Happy Village Drugstore - Right Counter Slot 1",
"Happy-Happy Village Drugstore - Right Counter Slot 2",
"Happy-Happy Village Drugstore - Right Counter Slot 3",
"Happy-Happy Village Drugstore - Right Counter Slot 4",
"Happy-Happy Village Drugstore - Right Counter Slot 5",
"Threed Drugstore - Right Counter Slot 1",
"Threed Drugstore - Right Counter Slot 2",
"Threed Drugstore - Right Counter Slot 3",
"Threed Drugstore - Right Counter Slot 4",
"Threed Drugstore - Right Counter Slot 5",
"Threed Drugstore - Left Counter Slot 1",
"Threed Drugstore - Left Counter Slot 2",
"Threed Drugstore - Left Counter Slot 3",
"Threed Drugstore - Left Counter Slot 4",
"Threed Drugstore - Left Counter Slot 5",
"Threed - Arms Dealer Slot 1",
"Threed - Arms Dealer Slot 2",
"Threed - Arms Dealer Slot 3",
"Threed - Arms Dealer Slot 4",
"Threed Bakery - Slot 1",
"Threed Bakery - Slot 2",
"Threed Bakery - Slot 3",
"Threed Bakery - Slot 4",
"Threed Bakery - Slot 5",
"Threed Bakery - Slot 6",
"Threed Bakery - Slot 7",
"Scaraba - Expensive Water Guy",
"Winters Drugstore - Slot 1",
"Winters Drugstore - Slot 2",
"Winters Drugstore - Slot 3",
"Winters Drugstore - Slot 4",
"Winters Drugstore - Slot 5",
"Winters Drugstore - Slot 6",
"Winters Drugstore - Slot 7",
"Saturn Valley Shop - Center Saturn Slot 1",
"Saturn Valley Shop - Center Saturn Slot 2",
"Saturn Valley Shop - Center Saturn Slot 3",
"Saturn Valley Shop - Center Saturn Slot 4",
"Saturn Valley Shop - Center Saturn Slot 5",
"Dusty Dunes Drugstore - Counter Slot 1",
"Dusty Dunes Drugstore - Counter Slot 2",
"Dusty Dunes Drugstore - Counter Slot 3",
"Dusty Dunes Drugstore - Counter Slot 4",
"Dusty Dunes Drugstore - Counter Slot 5",
"Dusty Dunes - Arms Dealer Slot 1",
"Dusty Dunes - Arms Dealer Slot 2",
"Dusty Dunes - Arms Dealer Slot 3",
"Dusty Dunes - Arms Dealer Slot 4",
"Fourside Bakery - Slot 1",
"Fourside Bakery - Slot 2",
"Fourside Bakery - Slot 3",
"Fourside Bakery - Slot 4",
"Fourside Bakery - Slot 5",
"Fourside Bakery - Slot 6",
"Fourside Department Store - Tool Shop Slot 1",
"Fourside Department Store - Tool Shop Slot 2",
"Fourside Department Store - Tool Shop Slot 3",
"Fourside Department Store - Tool Shop Slot 4",
"Fourside Department Store - Tool Shop Slot 5",
"Fourside Department Store - Tool Shop Slot 6",
"Fourside Department Store - Tool Shop Slot 7",
"Fourside Department Store - Shop Shop Slot 1",
"Fourside Department Store - Shop Shop Slot 2",
"Fourside Department Store - Shop Shop Slot 3",
"Fourside Department Store - Shop Shop Slot 4",
"Fourside Department Store - Food Shop Slot 1",
"Fourside Department Store - Food Shop Slot 2",
"Fourside Department Store - Food Shop Slot 3",
"Fourside Department Store - Food Shop Slot 4",
"Fourside Department Store - Food Shop Slot 5",
"Fourside Department Store - 2F Cart Slot 1",
"Fourside Department Store - 2F Cart Slot 2",
"Fourside Department Store - 2F Cart Slot 3",
"Fourside Department Store - 2F Cart Slot 4",
"Fourside Department Store - 2F Cart Slot 5",
"Fourside Department Store - 2F Cart Slot 6",
"Fourside Department Store - 2F Cart Slot 7",
"Fourside Department Store - Toys Shop Slot 1",
"Fourside Department Store - Toys Shop Slot 2",
"Fourside Department Store - Toys Shop Slot 3",
"Fourside Department Store - Toys Shop Slot 4",
"Fourside Department Store - Toys Shop Slot 5",
"Fourside Department Store - Toys Shop Slot 6",
"Fourside Department Store - Sports Shop Slot 1",
"Fourside Department Store - Sports Shop Slot 2",
"Fourside Department Store - Sports Shop Slot 3",
"Fourside Department Store - Sports Shop Slot 4",
"Fourside Department Store - Burger Shop Slot 1",
"Fourside Department Store - Burger Shop Slot 2",
"Fourside Department Store - Burger Shop Slot 3",
"Fourside Department Store - Burger Shop Slot 4",
"Fourside Department Store - Burger Shop Slot 5",
"Fourside Department Store - Arms Dealer Slot 1",
"Fourside Department Store - Arms Dealer Slot 2",
"Fourside Department Store - Arms Dealer Slot 3",
"Fourside Department Store - Arms Dealer Slot 4",
"Fourside Department Store - Arms Dealer Slot 5",
"Fourside - Northeast Alley Junk Shop Slot 1",
"Fourside - Northeast Alley Junk Shop Slot 2",
"Fourside - Northeast Alley Junk Shop Slot 3",
"Fourside - Northeast Alley Junk Shop Slot 4",
"Magicant - Shop Slot 1",
"Magicant - Shop Slot 2",
"Summers - Scam Shop Slot 1",
"Summers - Scam Shop Slot 2",
"Summers - Scam Shop Slot 3",
"Summers - Scam Shop Slot 4",
"Summers - Scam Shop Slot 5",
"Summers - Scam Shop Slot 6",
"Summers - Scam Shop Slot 7",
"Summers Harbor - Shop Slot 1",
"Summers Harbor - Shop Slot 2",
"Summers Harbor - Shop Slot 3",
"Summers Harbor - Shop Slot 4",
"Summers Harbor - Shop Slot 5",
"Summers Harbor - Shop Slot 6",
"Summers Harbor - Shop Slot 7",
"Summers Restaurant - Slot 1",
"Summers Restaurant - Slot 2",
"Summers Restaurant - Slot 3",
"Summers Restaurant - Slot 4",
"Summers Restaurant - Slot 5",
"Summers Restaurant - Slot 6",
"Scaraba - Indoors Shop Slot 1",
"Scaraba - Indoors Shop Slot 2",
"Scaraba - Indoors Shop Slot 3",
"Scaraba - Indoors Shop Slot 4",
"Scaraba - Indoors Shop Slot 5",
"Scaraba - Indoors Shop Slot 6",
"Scaraba Bazaar - Red Snake Carpet Slot 1",
"Scaraba Bazaar - Red Snake Carpet Slot 2",
"Scaraba Bazaar - Red Snake Carpet Slot 3",
"Scaraba Bazaar - Bottom Left Carpet Slot 1",
"Scaraba Bazaar - Bottom Left Carpet Slot 2",
"Scaraba Bazaar - Bottom Left Carpet Slot 3",
"Scaraba Bazaar - Bottom Left Carpet Slot 4",
"Scaraba Bazaar - Bottom Left Carpet Slot 5",
"Scaraba Bazaar - Bottom Left Carpet Slot 6",
"Scaraba Hotel - Arms Dealer Slot 1",
"Scaraba Hotel - Arms Dealer Slot 2",
"Scaraba Hotel - Arms Dealer Slot 3",
"Scaraba Hotel - Arms Dealer Slot 4",
"Deep Darkness - Businessman Slot 1",
"Deep Darkness - Businessman Slot 2",
"Deep Darkness - Businessman Slot 3",
"Deep Darkness - Businessman Slot 4",
"Deep Darkness - Businessman Slot 5",
"Deep Darkness - Businessman Slot 6",
"Deep Darkness - Businessman Slot 7",
"Happy-Happy Village - Trust Shop Slot 1",
"Happy-Happy Village - Trust Shop Slot 2",
"Saturn Valley Shop - Post-Belch Saturn Slot 1",
"Saturn Valley Shop - Post-Belch Saturn Slot 2",
"Saturn Valley Shop - Post-Belch Saturn Slot 3",
"Saturn Valley Shop - Post-Belch Saturn Slot 4",
"Scaraba - Southern Camel Shop Slot 1",
"Scaraba - Southern Camel Shop Slot 2",
"Scaraba - Southern Camel Shop Slot 3",
"Scaraba - Southern Camel Shop Slot 4",
"Scaraba - Southern Camel Shop Slot 5",
"Scaraba - Southern Camel Shop Slot 6",
"Scaraba - Southern Camel Shop Slot 7",
"Deep Darkness - Arms Dealer Slot 1",
"Deep Darkness - Arms Dealer Slot 2",
"Deep Darkness - Arms Dealer Slot 3",
"Deep Darkness - Arms Dealer Slot 4",
"Lost Underworld - Tenda Camp Shop Slot 1",
"Lost Underworld - Tenda Camp Shop Slot 2",
"Lost Underworld - Tenda Camp Shop Slot 3",
"Lost Underworld - Tenda Camp Shop Slot 4",
"Lost Underworld - Tenda Camp Shop Slot 5",
"Lost Underworld - Tenda Camp Shop Slot 6",
"Lost Underworld - Tenda Camp Shop Slot 7",
"Happy-Happy Village Drugstore - Left Counter Slot 1",
"Happy-Happy Village Drugstore - Left Counter Slot 2",
"Happy-Happy Village Drugstore - Left Counter Slot 3",
"Happy-Happy Village Drugstore - Left Counter Slot 4",
"Happy-Happy Village Drugstore - Left Counter Slot 5",
"Happy-Happy Village Drugstore - Left Counter Slot 6",
"Happy-Happy Village Drugstore - Left Counter Slot 7",
"Grapefruit Falls - Hiker Shop Slot 1",
"Grapefruit Falls - Hiker Shop Slot 2",
"Grapefruit Falls - Hiker Shop Slot 3",
"Saturn Valley Shop - Top Saturn Slot 1",
"Saturn Valley Shop - Top Saturn Slot 2",
"Saturn Valley Shop - Top Saturn Slot 3",
"Saturn Valley Shop - Top Saturn Slot 4",
"Saturn Valley Shop - Top Saturn Slot 5",
"Saturn Valley Shop - Top Saturn Slot 6",
"Saturn Valley Shop - Top Saturn Slot 7",
"Dusty Dunes Drugstore - Left Shop Slot 1",
"Dusty Dunes Drugstore - Left Shop Slot 2",
"Dusty Dunes Drugstore - Left Shop Slot 3",
"Dusty Dunes Drugstore - Left Shop Slot 4",
"Dusty Dunes Drugstore - Left Shop Slot 5",
"Dusty Dunes Drugstore - Left Shop Slot 6",
"Dusty Dunes Drugstore - Left Shop Slot 7",
"Dusty Dunes - Mine Food Cart Slot 1",
"Dusty Dunes - Mine Food Cart Slot 2",
"Dusty Dunes - Mine Food Cart Slot 3",
"Dusty Dunes - Mine Food Cart Slot 4",
"Dusty Dunes - Mine Food Cart Slot 5",
"Dusty Dunes - Mine Food Cart Slot 6",
"Dusty Dunes - Mine Food Cart Slot 7",
"Moonside Hotel - Shop Slot 1",
"Moonside Hotel - Shop Slot 2",
"Moonside Hotel - Shop Slot 3",
"Moonside Hotel - Shop Slot 4",
"Moonside Hotel - Shop Slot 5",
"Dalaam Restaurant - Slot 1",
"Dalaam Restaurant - Slot 2",
"Dalaam Restaurant - Slot 3",
"Dalaam Restaurant - Slot 4",
"Scaraba Bazaar - Delicacy Shop Slot 1",
"Scaraba Bazaar - Delicacy Shop Slot 2",
"Scaraba Bazaar - Delicacy Shop Slot 3",
"Scaraba Bazaar - Delicacy Shop Slot 4",
"Scaraba Bazaar - Delicacy Shop Slot 5",
"Scaraba Bazaar - Delicacy Shop Slot 6",
"Scaraba Bazaar - Delicacy Shop Slot 7",
"Twoson/Scaraba - Shared Condiment Shop Slot 1",
"Twoson/Scaraba - Shared Condiment Shop Slot 2",
"Twoson/Scaraba - Shared Condiment Shop Slot 3",
"Twoson/Scaraba - Shared Condiment Shop Slot 4",
"Twoson/Scaraba - Shared Condiment Shop Slot 5",
"Twoson/Scaraba - Shared Condiment Shop Slot 6",
"Twoson/Scaraba - Shared Condiment Shop Slot 7",
"Andonuts Lab - Caveman Shop Slot 1",
"Andonuts Lab - Caveman Shop Slot 2",
"Andonuts Lab - Caveman Shop Slot 3",
"Andonuts Lab - Caveman Shop Slot 4",
"Andonuts Lab - Caveman Shop Slot 5"
}
def write_shop_checks(world: "EarthBoundWorld", rom: "LocalRom", shop_checks: list[Location]) -> None:
unsellable_filler_prices = {
"Broken Machine": 150,
"Broken Air Gun": 110,
"Broken Laser": 250,
"Broken Pipe": 250,
"Broken Tube": 800,
"Broken Bazooka": 900,
"Broken Trumpet": 500,
"Broken Harmonica": 1500,
"Counter-PSI Unit": 300,
"Shield Killer": 500,
"Heavy Bazooka": 1800,
"Hungry HP-Sucker": 1600,
"Defense Shower": 1000,
"Neutralizer": 5000,
"Brain Stone": 2,
"Monkey's Love": 2
}
# Unique non-progression items that have no price by default. If they're on a shop,
# give them a base price. (prog items are handled by the "price" variable)
# Equipamizer already assigns prices to Equipment with no price, so they can be exempt.
if not world.options.armorizer:
unsellable_filler_prices["Cloak of Kings"] = 2000
unsellable_filler_prices["Diadem of Kings"] = 1500
unsellable_filler_prices["Bracer of Kings"] = 3500
if not world.options.weaponizer:
unsellable_filler_prices["Magicant Bat"] = 3000
unsellable_filler_prices["Legendary Bat"] = 5000
unsellable_filler_prices["Magnum Air Gun"] = 220
unsellable_filler_prices["Laser Gun"] = 500
unsellable_filler_prices["Baddest Beam"] = 3000
if world.options.shop_randomizer == ShopRandomizer.option_shopsanity:
rom.write_bytes(0x04FD77, bytearray([world.options.scout_shop_checks]))
for location in shop_checks:
flag = location.address - 0xEB1000
if location.item.player == world.player:
if world.options.remote_items:
if location.item.name in special_name_table or location.item.name in money_item_table:
item_type = 0x04
item_id = 0xAD
else:
item_type = 0x05
item_id = item_id_table[location.item.name]
else:
if location.item.name in psi_item_table:
item_type = 0x01
item_id = psi_item_table[location.item.name]
elif location.item.name == "Photograph" and location.item.player == world.player:
item_type = 0x06
item_id = 0xAD
elif location.item.name in character_item_table:
item_type = 0x02
item_id = character_item_table[location.item.name][0]
elif location.item.name in money_item_table:
item_type = 0x07
item_id = list(money_item_table).index(location.item.name) + 1
else:
item_type = 0x00
item_id = item_id_table[location.item.name]
if location.item.name in unsellable_filler_prices and location.item.player == world.player:
rom.write_bytes(0x15501A + (item_id_table[location.item.name] * 39),
struct.pack("H", unsellable_filler_prices[location.item.name]))
else:
item_type = 0x04
item_id = 0xAD
if ItemClassification.trap in location.item.classification:
price = 0
else:
price = world.random.randint(1, (75 * world.area_levels[location.parent_region.name]))
if location.parent_region.name in high_purchase_areas:
price = int(price / 1.5)
item_struct = struct.pack('<BHBH', item_id, price, item_type, flag)
rom.write_bytes(0x34002A + (0x06 * flag), item_struct)
menu_long_name = text_encoder(location.item.name, 127)
menu_item_name = location.item.name[:0x30]
menu_item_name = menu_item_name.replace(" ", "")
pixel_length = calc_pixel_width(menu_item_name)
while pixel_length > 78:
menu_item_name = menu_item_name[:-1]
pixel_length = calc_pixel_width(menu_item_name)
menu_item_name = text_encoder(menu_item_name, 0x30)
player_name = text_encoder(world.multiworld.get_player_name(location.item.player), 16)
player_name.append(0x00)
rom.write_bytes(0x341190 + (flag * 0x30), menu_item_name)
rom.write_bytes(0x3466D0 + (flag * 0x11), player_name)
rom.write_bytes(0x351100 + (flag * 127), menu_long_name)
rom.write_bytes(0x019DE5, struct.pack("I", 0xF007805C)) # Build the shop menus
rom.write_bytes(0x019E23, struct.pack("I", 0xF008465C)) # Display the item name
rom.write_bytes(0x019E8F, struct.pack("I", 0xF0094E5C)) # Display the item price
rom.write_bytes(0x011AC6, struct.pack("I", 0xF009985C)) # display the player name
rom.write_bytes(0x019EDD, struct.pack("I", 0xF00AA45C)) # Transfer the used data and player selection into a script for processing
rom.write_bytes(0x019ED3, struct.pack("I", 0xF00ADD5C)) # Display SOLD OUT
rom.write_bytes(0x019B66, struct.pack("I", 0xF00B0B5C)) # Prevent items for other players flashing the "you can equip this"
rom.write_bytes(0x019DA0, struct.pack("I", 0xF00B275C)) # Preserve the greyed out HP/PP palette
rom.write_bytes(0x05E0A9, struct.pack("I", 0xF4900008)) # Compare the price of the item with money on hand
rom.write_bytes(0x05E0B6, struct.pack("I", 0xF4905808)) # Display the item we bought and ask to confirm
# The player bought the item; set a flag and give it to them
rom.write_bytes(0x05E0CE, struct.pack("I", 0xF493C30A))
rom.write_bytes(0x05E0C8, struct.pack("I", 0xF493C3))
rom.write_bytes(0x05DF1E, struct.pack("I", 0xF496140A))
# Prevent the game from checking inventory space if not needed
rom.write_bytes(0x05E029, struct.pack("I", 0xF496340A))
rom.write_bytes(0x05E04C, struct.pack("I", 0xF496590A))
rom.write_bytes(0x05E1AE, struct.pack("I", 0xF00EB8)) # Post-shop cleanup
rom.write_bytes(0x05E1A5, struct.pack("I", 0xF00EAA))
rom.write_bytes(0x05E119, struct.pack("I", 0xF00EB1))
rom.write_bytes(0x05E0F2, struct.pack("I", 0xF00EC0))
rom.write_bytes(0x3407E0, bytearray([item_id_table[world.filler_shop[0]], 0x00, 0x00, 0x00, 0x49, 0x01]))
rom.write_bytes(0x3407E6, bytearray([item_id_table[world.filler_shop[1]], 0x00, 0x00, 0x00, 0x4A, 0x01]))
rom.write_bytes(0x3408DC, bytearray([0xE0, 0x00, 0x00, 0x00, 0xFF, 0xFF]))
rom.write_bytes(0x3408E2, bytearray([0x5D, 0x00, 0x00, 0x00, 0xFF, 0xFF]))
rom.write_bytes(0x3408E8, bytearray([0x5A, 0x00, 0x00, 0x00, 0xFF, 0xFF]))
rom.write_bytes(0x3408EE, bytearray([0x7F, 0x00, 0x00, 0x00, 0xFF, 0xFF]))
rom.write_bytes(0x3408F4, bytearray([0x5F, 0x00, 0x00, 0x00, 0xFF, 0xFF]))
rom.write_bytes(0x3408FA, bytearray([0x6C, 0x00, 0x00, 0x00, 0xFF, 0xFF]))
rom.write_bytes(0x340900, bytearray([0x8C, 0x00, 0x00, 0x00, 0xFF, 0xFF]))
if world.options.magicant_mode >= MagicantMode.option_alternate_goal: # 2
rom.write_bytes(0x3405E8, bytearray([item_id_table[world.magicant_junk[6]], 0x00, 0x00, 0x00, 0xF5, 0x00]))
rom.write_bytes(0x3405EE, bytearray([item_id_table[world.magicant_junk[7]], 0x00, 0x00, 0x00, 0xF6, 0x00]))
else:
filler_shop_items = world.filler_drops.copy()
filler_shop_items = [x for x in filler_shop_items if x not in [227, 228, 229, 230, 231, 0]]
for location in location_ids:
if location_ids[location] >= 0xEB1000:
slot = location_ids[location] - 0xEB1000
rom.write_bytes(0x1576B9 + slot, bytearray([world.random.choice(filler_shop_items)]))
rom.write_bytes(0x1576e3, bytearray([0xEF]))
rom.write_bytes(0x15779c, bytearray([0x5A]))
if world.options.monkey_caves_mode < MonkeyCavesMode.option_shop: # 2
rom.write_bytes(0x15776b, bytearray([0xE0]))
rom.write_bytes(0x157775, bytearray([0x8C]))
rom.write_bytes(0x157778, bytearray([0x6C]))
rom.write_bytes(0x157781, bytearray([0x5D]))
rom.write_bytes(0x157848, bytearray([0x7F])) # DD Drugstore left counter 1
rom.write_bytes(0x157802, bytearray([world.random.choice(filler_shop_items)]))
rom.write_bytes(0x157803, bytearray([world.random.choice(filler_shop_items)]))

View File

@@ -0,0 +1,501 @@
from .modules.flavor_data import random_flavors
from .game_data.text_data import lumine_hall_text, eb_text_table, text_encoder
from .game_data.local_data import item_id_table
from .modules.psi_shuffle import shuffle_psi
from .modules.boss_shuffle import initialize_bosses
from .modules.enemy_shuffler import shuffle_enemies
from .modules.dungeon_er import shuffle_dungeons
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import EarthBoundWorld
def setup_gamevars(world: "EarthBoundWorld") -> None:
"""Initialize or roll most world variables"""
world.slime_pile_wanted_item = world.random.choice([
"Cookie",
"Bag of Fries",
"Hamburger",
"Boiled Egg",
"Fresh Egg",
"Picnic Lunch",
"Pasta di Summers",
"Pizza",
"Chef's Special",
"Large Pizza",
"PSI Caramel",
"Magic Truffle",
"Brain Food Lunch",
"Rock Candy",
"Croissant",
"Bread Roll",
"Can of Fruit Juice",
"Royal Iced Tea",
"Protein Drink",
"Kraken Soup",
"Bottle of Water",
"Cold Remedy",
"Vial of Serum",
"IQ Capsule",
"Guts Capsule",
"Speed Capsule",
"Vital Capsule",
"Luck Capsule",
"Ketchup Packet",
"Sugar Packet",
"Tin of Cocoa",
"Carton of Cream",
"Sprig of Parsley",
"Jar of Hot Sauce",
"Salt Packet",
"Jar of Delisauce",
"Trout Yogurt",
"Banana",
"Calorie Stick",
"Gelato de Resort",
"Magic Tart",
"Cup of Noodles",
"Repel Sandwich",
"Repel Superwich",
"Lucky Sandwich",
"Cup of Coffee",
"Double Burger",
"Mammoth Burger",
"Peanut Cheese Bar",
"Piggy Jelly",
"Bowl of Rice Gruel",
"Bean Croquette",
"Molokheiya Soup",
"Plain Roll",
"Kabob",
"Plain Yogurt",
"Beef Jerky",
"Spicy Jerky",
"Luxury Jerky",
"Bottle of DXwater",
"Magic Pudding",
"Popsicle"])
if world.options.progressive_weapons:
for i in range(3):
world.common_gear.append("Progressive Bat")
world.common_gear.append("Progressive Gun")
world.common_gear.append("Progressive Fry Pan")
for i in range(3):
world.uncommon_gear.append("Progressive Bat")
world.uncommon_gear.append("Progressive Gun")
world.uncommon_gear.append("Progressive Fry Pan")
world.rare_gear.append("Progressive Bat")
world.rare_gear.append("Progressive Gun")
world.rare_gear.append("Progressive Fry Pan")
else:
world.common_gear.extend([
"Cracked Bat",
"Tee Ball Bat",
"Sand Lot Bat",
"Minor League Bat",
"Fry Pan",
"Thick Fry Pan",
"Deluxe Fry Pan",
"Toy Air Gun",
"Zip Gun"
])
world.uncommon_gear.extend([
"Mr. Baseball Bat",
"T-Rex's Bat",
"Big League Bat",
"Chef's Fry Pan",
"Non-Stick Frypan",
"French Fry Pan",
"Hyper Beam",
"Crusher Beam"
])
world.rare_gear.extend([
"Hall of Fame Bat",
"Ultimate Bat",
"Gutsy Bat",
"Casey Bat",
"Holy Fry Pan",
"Magic Fry Pan"
])
if world.options.progressive_armor:
for i in range(3):
world.common_gear.append("Progressive Bracelet")
world.common_gear.append("Progressive Other")
for i in range(3):
world.uncommon_gear.append("Progressive Bracelet")
world.uncommon_gear.append("Progressive Other")
world.rare_gear.append("Progressive Bracelet")
world.rare_gear.append("Progressive Other")
else:
world.common_gear.extend([
"Cheap Bracelet",
"Copper Bracelet",
"Baseball Cap",
"Mr. Baseball Cap",
"Holmes Hat",
"Hard Hat",
"Coin of Defense"
])
world.uncommon_gear.extend([
"Platinum Band",
"Diamond Band",
"Lucky Coin",
"Silver Bracelet",
"Gold Bracelet",
"Coin of Slumber",
"Coin of Silence"
])
world.rare_gear.extend([
"Talisman Coin",
"Shiny Coin",
"Charm Coin"
])
world.multiworld.push_precollected(world.create_item(world.starting_character))
valid_starts = 14
if world.options.magicant_mode != 00:
valid_starts -= 1
if world.options.random_start_location:
world.start_location = world.random.randint(1, valid_starts)
else:
world.start_location = 0
if world.options.prefixed_items:
world.multiworld.itempool.append(world.create_item("Counter-PSI Unit"))
world.multiworld.itempool.append(world.create_item("Shield Killer"))
world.multiworld.itempool.append(world.create_item("Hungry HP-Sucker"))
world.multiworld.itempool.append(world.create_item("Defense Shower"))
world.multiworld.itempool.append(world.create_item("Heavy Bazooka"))
world.common_items.append("Defense Spray")
world.uncommon_items.append("Slime Generator")
if world.options.progressive_weapons:
for i in range(3):
world.multiworld.itempool.append(world.create_item("Progressive Gun"))
world.common_gear.append("Progressive Gun")
world.uncommon_gear.append("Progressive Gun")
world.rare_gear.append("Progressive Gun")
else:
world.multiworld.itempool.append(world.create_item("Magnum Air Gun"))
world.multiworld.itempool.append(world.create_item("Laser Gun"))
world.multiworld.itempool.append(world.create_item("Baddest Beam"))
world.uncommon_gear.append("Spectrum Beam")
world.rare_gear.append("Gaia Beam")
world.common_gear.append("Double Beam")
else:
world.multiworld.itempool.append(world.create_item("Broken Machine"))
world.multiworld.itempool.append(world.create_item("Broken Pipe"))
world.multiworld.itempool.append(world.create_item("Broken Tube"))
world.multiworld.itempool.append(world.create_item("Broken Trumpet"))
world.multiworld.itempool.append(world.create_item("Broken Bazooka"))
world.common_items.append("Broken Spray Can")
world.uncommon_items.append("Broken Iron")
if world.options.progressive_weapons:
for i in range(3):
world.multiworld.itempool.append(world.create_item("Progressive Gun"))
world.common_gear.append("Progressive Gun")
world.uncommon_gear.append("Progressive Gun")
world.rare_gear.append("Progressive Gun")
else:
world.multiworld.itempool.append(world.create_item("Broken Air Gun"))
world.multiworld.itempool.append(world.create_item("Broken Laser"))
world.multiworld.itempool.append(world.create_item("Broken Harmonica"))
world.common_gear.append("Broken Gadget")
world.uncommon_gear.append("Broken Cannon")
world.rare_gear.append("Broken Antenna")
for i in range(world.options.total_photos):
world.multiworld.itempool.append(world.create_item("Photograph"))
world.event_count += 1
world.franklinbadge_elements = [
"thunder",
"fire",
"freeze",
"flash",
"starstorm",
"special",
"explosive"
]
world.starting_progressive_bats = 0
world.starting_progressive_pans = 0
world.starting_progressive_guns = 0
world.starting_progressive_bracelets = 0
world.starting_progressive_others = 0
if world.options.prefixed_items:
world.broken_guns = [
"Magnum Air Gun",
"Laser Gun",
"Double Beam",
"Spectrum Beam",
"Baddest Beam",
"Gaia Beam"
]
else:
world.broken_guns = [
"Broken Air Gun",
"Broken Laser",
"Broken Gadget",
"Broken Cannon",
"Broken Harmonica",
"Broken Antenna"
]
world.bats = [
"Sand Lot Bat",
"Minor League Bat",
"Mr. Baseball Bat",
"T-Rex's Bat",
"Big League Bat",
"Hall of Fame Bat",
"Casey Bat",
"Magicant Bat",
"Legendary Bat"
]
world.pans = [
"Fry Pan",
"Thick Fry Pan",
"Deluxe Fry Pan",
"Chef's Fry Pan",
"Non-Stick Fry Pan",
"French Fry Pan",
"Holy Fry Pan",
"Magic Fry Pan"
]
world.guns = [
"Pop Gun",
"Stun Gun",
"Toy Air Gun",
world.broken_guns[0],
"Zip Gun",
world.broken_guns[1],
"Hyper Beam",
world.broken_guns[2],
"Crusher Beam",
world.broken_guns[3],
"Death Ray",
world.broken_guns[4],
"Moon Beam Gun",
world.broken_guns[5]
]
world.bracelets = [
"Cheap Bracelet",
"Copper Bracelet",
"Silver Bracelet",
"Gold Bracelet",
"Platinum Band",
"Diamond Band",
"Pixie's Bracelet",
"Cherub's Band",
"Goddess Band"
]
world.others = [
"Baseball Cap",
"Mr. Baseball Cap",
"Holmes Hat",
"Hard Hat",
"Coin of Slumber",
"Coin of Defense",
"Coin of Slience"
"Mr. Saturn Coin",
"Charm Coin",
"Lucky Coin",
"Talisman Coin",
"Shiny Coin",
"Souvenir Coin"
]
world.progressive_item_groups = {
"Progressive Bat": world.bats,
"Progressive Fry Pan": world.pans,
"Progressive Gun": world.guns,
"Progressive Bracelet": world.bracelets,
"Progressive Other": world.others
}
world.start_prog_counts = {
"Progressive Bat": world.starting_progressive_bats,
"Progressive Fry Pan": world.starting_progressive_pans,
"Progressive Gun": world.starting_progressive_guns,
"Progressive Bracelet": world.starting_progressive_bracelets,
"Progressive Other": world.starting_progressive_others
}
if world.options.randomize_franklinbadge_protection:
world.franklin_protection = world.random.choice(world.franklinbadge_elements)
else:
world.franklin_protection = "thunder"
if world.options.random_start_location:
world.valid_teleports = [
"Onett Teleport",
"Twoson Teleport",
"Happy-Happy Village Teleport",
"Threed Teleport",
"Saturn Valley Teleport",
"Fourside Teleport",
"Winters Teleport",
"Summers Teleport",
"Dalaam Teleport",
"Scaraba Teleport",
"Deep Darkness Teleport",
"Tenda Village Teleport",
"Lost Underworld Teleport"
]
if world.options.magicant_mode == 0:
world.valid_teleports.append("Magicant Teleport")
del world.valid_teleports[world.start_location - 1]
world.starting_teleport = world.random.choice(world.valid_teleports)
world.multiworld.push_precollected(world.create_item(world.starting_teleport))
filler_items = (world.common_items + world.uncommon_items + world.rare_items + world.common_gear +
world.uncommon_gear + world.rare_gear)
if world.options.progressive_weapons:
remove_items = {"Progressive Bat", "Progressive Fry Pan", "Progressive Gun"}
filler_items = [item for item in filler_items if item not in remove_items]
if world.options.progressive_armor:
remove_items = {"Progressive Bracelet", "Progressive Other"}
filler_items = [item for item in filler_items if item not in remove_items]
world.filler_drops = [item_id_table[i] for i in filler_items if i in item_id_table]
world.filler_drops.append(0x00)
if world.options.prefixed_items:
world.filler_drops.extend([0xA1, 0xD7, 0x8A, 0x2C, 0x30])
else:
world.filler_drops.extend([0x07, 0x05, 0x09, 0x0B, 0x10])
world.filler_shop = []
if world.options.magicant_mode.value >= 2:
world.magicant_junk = []
for i in range(8):
world.magicant_junk.append(world.random.choice(filler_items))
for i in range(2):
world.filler_shop.append(world.random.choice(filler_items))
world.available_flavors = []
if world.options.random_flavors:
for i in range(4):
world.available_flavors = world.random.sample(random_flavors, 4)
else:
world.available_flavors = [
"Mint flavor",
"Strawberry flavor",
"Banana flavor",
"Peanut flavor"
]
world.lumine_text = []
world.prayer_player = []
if world.options.plando_lumine_hall_text == "":
lumine_str = world.random.choice(lumine_hall_text)
else:
lumine_str = world.options.plando_lumine_hall_text.value
for char in lumine_str[:213]:
world.lumine_text.extend(eb_text_table[char])
world.lumine_text.extend([0x00])
world.starting_money = world.options.starting_money.value
# todo; move to text converter
prayer_player = world.multiworld.get_player_name(world.random.randint(1, world.multiworld.players))
for char in prayer_player[:24]:
if char in eb_text_table:
world.prayer_player.extend(eb_text_table[char])
else:
world.prayer_player.extend([0x6F])
world.prayer_player.extend([0x00])
world.credits_player = world.multiworld.get_player_name(world.player)
world.credits_player = text_encoder(world.credits_player, 16)
world.credits_player.extend([0x00])
shuffle_psi(world)
initialize_bosses(world)
shuffle_enemies(world)
shuffle_dungeons(world)
def place_static_items(world: "EarthBoundWorld") -> None:
"""Places all locked items. Some are events. Some are filler items that
need to be placed depending on certain settings."""
world.get_location("Belch Defeated").place_locked_item(world.create_item("Threed Tunnels Clear"))
world.get_location("Dungeon Man Submarine").place_locked_item(world.create_item("Submarine to Deep Darkness"))
world.get_location("Any ATM").place_locked_item(world.create_item("ATM Access"))
world.get_location("Giant Step Sanctuary").place_locked_item(world.create_item("Melody"))
world.get_location("Lilliput Steps Sanctuary").place_locked_item(world.create_item("Melody"))
world.get_location("Milky Well Sanctuary").place_locked_item(world.create_item("Melody"))
world.get_location("Rainy Circle Sanctuary").place_locked_item(world.create_item("Melody"))
world.get_location("Magnet Hill Sanctuary").place_locked_item(world.create_item("Melody"))
world.get_location("Pink Cloud Sanctuary").place_locked_item(world.create_item("Melody"))
world.get_location("Lumine Hall Sanctuary").place_locked_item(world.create_item("Melody"))
world.get_location("Fire Spring Sanctuary").place_locked_item(world.create_item("Melody"))
world.get_location("Carpainter Defeated").place_locked_item(world.create_item("Valley Bridge Repair"))
if world.options.giygas_required:
world.get_location("Giygas").place_locked_item(world.create_item("Saved Earth")) # Normal final boss
if world.options.magicant_mode == 1:
# If required magicant
world.get_location("Magicant - Ness's Nightmare").place_locked_item(world.create_item("Power of the Earth"))
world.get_location("Sanctuary Goal").place_locked_item(world.create_item("Magicant Unlock"))
else:
# If not required, place this condition on sanctuary goal
world.get_location("Sanctuary Goal").place_locked_item(world.create_item("Power of the Earth"))
else:
if world.options.magicant_mode == 1:
# If Magicant required but not Giygas, place goal
world.get_location("Magicant - Ness's Nightmare").place_locked_item(world.create_item("Saved Earth"))
world.get_location("Sanctuary Goal").place_locked_item(world.create_item("Magicant Unlock"))
else:
# If neither final boss, place goal
world.get_location("Sanctuary Goal").place_locked_item(world.create_item("Saved Earth"))
if world.options.alternate_sanctuary_goal:
world.get_location("+2 Sanctuaries").place_locked_item(world.create_item("Alternate Goal"))
if world.options.magicant_mode == 2:
world.get_location("+1 Sanctuary").place_locked_item(world.create_item("Magicant Unlock"))
world.get_location("Magicant - Ness's Nightmare").place_locked_item(world.create_item("Alternate Goal"))
if not world.options.monkey_caves_mode:
world.get_location("Monkey Caves - 1F Right Chest").place_locked_item(world.create_item("Wet Towel"))
world.get_location("Monkey Caves - 1F Left Chest").place_locked_item(world.create_item("Pizza"))
world.get_location("Monkey Caves - West 2F Left Chest").place_locked_item(world.create_item("Pizza"))
world.get_location("Monkey Caves - West 2F Right Chest #1").place_locked_item(world.create_item("Hamburger"))
world.get_location("Monkey Caves - West 2F Right Chest #2").place_locked_item(world.create_item("Ruler"))
world.get_location("Monkey Caves - East 2F Left Chest").place_locked_item(world.create_item("Protein Drink"))
world.get_location("Monkey Caves - East 2F Right Chest").place_locked_item(world.create_item("Hamburger"))
world.get_location("Monkey Caves - East West 3F Right Chest #1").place_locked_item(world.create_item("Hamburger"))
world.get_location("Monkey Caves - East West 3F Right Chest #2").place_locked_item(world.create_item("Picnic Lunch"))
if world.options.shop_randomizer == 2:
world.get_location("Twoson Department Store Bakery - Slot 1").place_locked_item(world.create_item("Plain Roll"))
world.get_location("Fourside Department Store - Burger Shop Slot 4").place_locked_item(world.create_item("Hamburger"))
if world.options.monkey_caves_mode < 2:
world.get_location("Fourside Bakery - Slot 4").place_locked_item(world.create_item("Repel Sandwich"))
world.get_location("Fourside Department Store - Tool Shop Slot 7").place_locked_item(world.create_item("Ruler"))
world.get_location("Fourside Department Store - Shop Shop Slot 3").place_locked_item(world.create_item("Protein Drink"))
world.get_location("Fourside Department Store - Food Shop Slot 5").place_locked_item(world.create_item("Picnic Lunch"))
world.get_location("Dusty Dunes Drugstore - Left Shop Slot 1").place_locked_item(world.create_item("Wet Towel"))

Binary file not shown.

18902
worlds/earthbound/src/eb.asm Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -0,0 +1,6 @@
from test.bases import WorldTestBase
class EarthBoundTestBase(WorldTestBase):
game = "EarthBound"
player = 1

View File

@@ -0,0 +1,66 @@
from . import EarthBoundTestBase
class TestPSIShuffle(EarthBoundTestBase):
options = {
"PSIShuffle": 1
}
class TestExtendedPSIShuffle(EarthBoundTestBase):
options = {
"PSIShuffle": 2
}
class TestBossShuffle(EarthBoundTestBase):
options = {
"BossShuffle": 1
}
class TestBossShuffleWithDD(EarthBoundTestBase):
options = {
"BossShuffle": 1,
"DecoupleDiamondDog": 1
}
class TestBossShuffleGiygas(EarthBoundTestBase):
options = {
"BossShuffle": 1,
"ShuffleGiygas": 1
}
class TestBossShuffleFull(EarthBoundTestBase):
options = {
"BossShuffle": 1,
"ShuffleGiygas": 1,
"DecoupleDiamondDog": 1
}
class TestShopChecks(EarthBoundTestBase):
options = {
"ShopRandomizer": 2,
}
class TestDungeons(EarthBoundTestBase):
options = {
"DungeonShuffle": True,
}
class TestEnemizer(EarthBoundTestBase):
options = {
"EnemizerStats": True,
"EnemizerAttacks": True,
"EnemizerAttributes": True,
}
class TestMapPalettes(EarthBoundTestBase):
options = {
"RandomMapColors": 3,
}