lufia2ac: add shops to the cave (#2103)
This PR adds a new, optional aspect to the Ancient Cave experience: During their run, players can have the opportunity to purchase some additional items or spells to improve their party. If enabled, a shop will appear everytime a certain (configurable) number of floors in the dungeon has been completed. The shop inventories are generated randomly (taking into account player preference as well as a system to ensure that more expensive items can only become available deeper into the run). For customization, 3 new options are introduced: - `shop_interval`: Determines by how many floors the shops are separated (or keeps them turned off entirely) - `shop_inventory`: Determines what's possible to be for sale. (Players can specify weights for general categories of things such as "weapon" or "spell" or even adjust the probabilities of individual items) - `gold_modifier`: Determines how much gold is dropped by enemies. This is the player's only source of income and thus controls how much money they will have available to spend in shops
This commit is contained in:
parent
7c2cb34b45
commit
1c4303cce6
|
@ -2,9 +2,8 @@ from enum import auto, Enum
|
|||
from typing import Dict, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from . import Locations
|
||||
|
||||
start_id: int = Locations.start_id
|
||||
start_id: int = 0xAC0000
|
||||
|
||||
|
||||
class ItemType(Enum):
|
||||
|
@ -500,7 +499,7 @@ l2ac_item_table: Dict[str, ItemData] = {
|
|||
# 0x01C8: "Key28"
|
||||
# 0x01C9: "Key29"
|
||||
# 0x01CA: "AP item" # replaces "Key30"
|
||||
# 0x01CB: "Crown"
|
||||
# 0x01CB: "SOLD OUT" # replaces "Crown"
|
||||
# 0x01CC: "Ruby apple"
|
||||
# 0x01CD: "PURIFIA"
|
||||
# 0x01CE: "Tag ring"
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import numbers
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from itertools import accumulate, chain, combinations
|
||||
from typing import Any, cast, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Type, TYPE_CHECKING, Union
|
||||
|
||||
from Options import AssembleOptions, Choice, DeathLink, ItemDict, PerGameCommonOptions, Range, SpecialRange, \
|
||||
TextChoice, Toggle
|
||||
from Options import AssembleOptions, Choice, DeathLink, ItemDict, OptionDict, PerGameCommonOptions, Range, \
|
||||
SpecialRange, TextChoice, Toggle
|
||||
from .Enemies import enemy_name_to_sprite
|
||||
from .Items import ItemType, l2ac_item_table
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from BaseClasses import PlandoOptions
|
||||
|
@ -558,6 +561,25 @@ class Goal(Choice):
|
|||
default = option_boss
|
||||
|
||||
|
||||
class GoldModifier(Range):
|
||||
"""Percentage modifier for gold gained from enemies.
|
||||
|
||||
Supported values: 25 – 400
|
||||
Default value: 100 (same as in an unmodified game)
|
||||
"""
|
||||
|
||||
display_name = "Gold modifier"
|
||||
range_start = 25
|
||||
range_end = 400
|
||||
default = 100
|
||||
|
||||
def __call__(self, gold: bytes) -> bytes:
|
||||
try:
|
||||
return (int.from_bytes(gold, "little") * self.value // 100).to_bytes(2, "little")
|
||||
except OverflowError:
|
||||
return b"\xFF\xFF"
|
||||
|
||||
|
||||
class HealingFloorChance(Range):
|
||||
"""The chance of a floor having a healing tile hidden under a bush.
|
||||
|
||||
|
@ -662,6 +684,105 @@ class RunSpeed(Choice):
|
|||
default = option_disabled
|
||||
|
||||
|
||||
class ShopInterval(SpecialRange):
|
||||
"""Place shops after a certain number of floors.
|
||||
|
||||
E.g., if you set this to 5, then you will be given the opportunity to shop after completing B5, B10, B15, etc.,
|
||||
whereas if you set it to 1, then there will be a shop after every single completed floor.
|
||||
Shops will offer a random selection of wares; on deeper floors, more expensive items might appear.
|
||||
You can customize the stock that can appear in shops using the shop_inventory option.
|
||||
You can control how much gold you will be obtaining from enemies using the gold_multiplier option.
|
||||
Supported values: disabled, 1 – 10
|
||||
Default value: disabled (same as in an unmodified game)
|
||||
"""
|
||||
|
||||
display_name = "Shop interval"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 0
|
||||
special_range_cutoff = 1
|
||||
special_range_names = {
|
||||
"disabled": 0,
|
||||
}
|
||||
|
||||
|
||||
class ShopInventory(OptionDict):
|
||||
"""Determine the item types that can appear in shops.
|
||||
|
||||
The value of this option should be a mapping of item categories (or individual items) to weights (non-negative
|
||||
integers), which are used as relative probabilities when it comes to including these things in shops. (The actual
|
||||
contents of the generated shops are selected randomly and are subject to additional constraints such as more
|
||||
expensive things being allowed only on later floors.)
|
||||
Supported keys:
|
||||
non_restorative — a selection of mostly non-restorative red chest consumables
|
||||
restorative — all HP- or MP-restoring red chest consumables
|
||||
blue_chest — all blue chest items
|
||||
spell — all red chest spells
|
||||
gear — all red chest armors, shields, headgear, rings, and rocks (this respects the gear_variety_after_b9 option,
|
||||
meaning that you will not encounter any shields, headgear, rings, or rocks in shops from B10 onward unless you
|
||||
also enabled that option)
|
||||
weapon — all red chest weapons
|
||||
Additionally, you can also add extra weights for any specific cave item. If you want your shops to have a
|
||||
higher than normal chance of selling a Dekar blade, you can, e.g., add "Dekar blade: 5".
|
||||
You can even forego the predefined categories entirely and design a custom shop pool from scratch by providing
|
||||
separate weights for each item you want to include.
|
||||
(Spells, however, cannot be weighted individually and are only available as part of the "spell" category.)
|
||||
Default value: {spell: 30, gear: 45, weapon: 82}
|
||||
"""
|
||||
|
||||
display_name = "Shop inventory"
|
||||
_special_keys = {"non_restorative", "restorative", "blue_chest", "spell", "gear", "weapon"}
|
||||
valid_keys = _special_keys | {item for item, data in l2ac_item_table.items()
|
||||
if data.type in {ItemType.BLUE_CHEST, ItemType.ENEMY_DROP, ItemType.ENTRANCE_CHEST,
|
||||
ItemType.RED_CHEST, ItemType.RED_CHEST_PATCH}}
|
||||
default: Dict[str, int] = {
|
||||
"spell": 30,
|
||||
"gear": 45,
|
||||
"weapon": 82,
|
||||
}
|
||||
value: Dict[str, int]
|
||||
|
||||
def verify(self, world: Type[World], player_name: str, plando_options: PlandoOptions) -> None:
|
||||
super().verify(world, player_name, plando_options)
|
||||
for item, weight in self.value.items():
|
||||
if not isinstance(weight, numbers.Integral) or weight < 0:
|
||||
raise Exception(f"Weight for item \"{item}\" from option {self} must be a non-negative integer, "
|
||||
f"but was \"{weight}\".")
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return sum(self.value.values())
|
||||
|
||||
@property
|
||||
def non_restorative(self) -> int:
|
||||
return self.value.get("non_restorative", 0)
|
||||
|
||||
@property
|
||||
def restorative(self) -> int:
|
||||
return self.value.get("restorative", 0)
|
||||
|
||||
@property
|
||||
def blue_chest(self) -> int:
|
||||
return self.value.get("blue_chest", 0)
|
||||
|
||||
@property
|
||||
def spell(self) -> int:
|
||||
return self.value.get("spell", 0)
|
||||
|
||||
@property
|
||||
def gear(self) -> int:
|
||||
return self.value.get("gear", 0)
|
||||
|
||||
@property
|
||||
def weapon(self) -> int:
|
||||
return self.value.get("weapon", 0)
|
||||
|
||||
@functools.cached_property
|
||||
def custom(self) -> Dict[int, int]:
|
||||
return {l2ac_item_table[item].code & 0x01FF: weight for item, weight in self.value.items()
|
||||
if item not in self._special_keys}
|
||||
|
||||
|
||||
class ShuffleCapsuleMonsters(Toggle):
|
||||
"""Shuffle the capsule monsters into the multiworld.
|
||||
|
||||
|
@ -717,6 +838,7 @@ class L2ACOptions(PerGameCommonOptions):
|
|||
final_floor: FinalFloor
|
||||
gear_variety_after_b9: GearVarietyAfterB9
|
||||
goal: Goal
|
||||
gold_modifier: GoldModifier
|
||||
healing_floor_chance: HealingFloorChance
|
||||
initial_floor: InitialFloor
|
||||
iris_floor_chance: IrisFloorChance
|
||||
|
@ -724,5 +846,7 @@ class L2ACOptions(PerGameCommonOptions):
|
|||
master_hp: MasterHp
|
||||
party_starting_level: PartyStartingLevel
|
||||
run_speed: RunSpeed
|
||||
shop_interval: ShopInterval
|
||||
shop_inventory: ShopInventory
|
||||
shuffle_capsule_monsters: ShuffleCapsuleMonsters
|
||||
shuffle_party_members: ShufflePartyMembers
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import itertools
|
||||
from operator import itemgetter
|
||||
from random import Random
|
||||
from typing import Dict, List, MutableSequence, Sequence, Set, Tuple
|
||||
from typing import Dict, Iterable, List, MutableSequence, Sequence, Set, Tuple
|
||||
|
||||
|
||||
def constrained_choices(population: Sequence[int], d: int, *, k: int, random: Random) -> List[int]:
|
||||
|
@ -19,3 +21,10 @@ def constrained_shuffle(x: MutableSequence[int], d: int, random: Random) -> None
|
|||
i, j = random.randrange(n), random.randrange(n)
|
||||
if x[i] in constraints[j] and x[j] in constraints[i]:
|
||||
x[i], x[j] = x[j], x[i]
|
||||
|
||||
|
||||
def weighted_sample(population: Iterable[int], weights: Iterable[float], k: int, *, random: Random) -> List[int]:
|
||||
population, keys = zip(*((item, pow(random.random(), 1 / group_weight))
|
||||
for item, group in itertools.groupby(sorted(zip(population, weights)), key=itemgetter(0))
|
||||
if (group_weight := sum(weight for _, weight in group))))
|
||||
return sorted(population, key=dict(zip(population, keys)).__getitem__)[-k:]
|
||||
|
|
|
@ -14,9 +14,9 @@ from .Client import L2ACSNIClient # noqa: F401
|
|||
from .Items import ItemData, ItemType, l2ac_item_name_to_id, l2ac_item_table, L2ACItem, start_id as items_start_id
|
||||
from .Locations import l2ac_location_name_to_id, L2ACLocation
|
||||
from .Options import CapsuleStartingLevel, DefaultParty, EnemyFloorNumbers, EnemyMovementPatterns, EnemySprites, \
|
||||
ExpModifier, Goal, L2ACOptions
|
||||
Goal, L2ACOptions
|
||||
from .Rom import get_base_rom_bytes, get_base_rom_path, L2ACDeltaPatch
|
||||
from .Utils import constrained_choices, constrained_shuffle
|
||||
from .Utils import constrained_choices, constrained_shuffle, weighted_sample
|
||||
from .basepatch import apply_basepatch
|
||||
|
||||
CHESTS_PER_SPHERE: int = 5
|
||||
|
@ -222,6 +222,7 @@ class L2ACWorld(World):
|
|||
rom_bytearray[0x09D59B:0x09D59B + 256] = self.get_node_connection_table()
|
||||
rom_bytearray[0x0B05C0:0x0B05C0 + 18843] = self.get_enemy_stats()
|
||||
rom_bytearray[0x0B4F02:0x0B4F02 + 2] = self.o.master_hp.value.to_bytes(2, "little")
|
||||
rom_bytearray[0x0BEE9F:0x0BEE9F + 1948] = self.get_shops()
|
||||
rom_bytearray[0x280010:0x280010 + 2] = self.o.blue_chest_count.value.to_bytes(2, "little")
|
||||
rom_bytearray[0x280012:0x280012 + 3] = self.o.capsule_starting_level.xp.to_bytes(3, "little")
|
||||
rom_bytearray[0x280015:0x280015 + 1] = self.o.initial_floor.value.to_bytes(1, "little")
|
||||
|
@ -229,6 +230,7 @@ class L2ACWorld(World):
|
|||
rom_bytearray[0x280017:0x280017 + 1] = self.o.iris_treasures_required.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x280018:0x280018 + 1] = self.o.shuffle_party_members.unlock.to_bytes(1, "little")
|
||||
rom_bytearray[0x280019:0x280019 + 1] = self.o.shuffle_capsule_monsters.unlock.to_bytes(1, "little")
|
||||
rom_bytearray[0x28001A:0x28001A + 1] = self.o.shop_interval.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x280030:0x280030 + 1] = self.o.goal.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x28003D:0x28003D + 1] = self.o.death_link.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x281200:0x281200 + 470] = self.get_capsule_cravings_table()
|
||||
|
@ -357,7 +359,7 @@ class L2ACWorld(World):
|
|||
def get_enemy_stats(self) -> bytes:
|
||||
rom: bytes = get_base_rom_bytes()
|
||||
|
||||
if self.o.exp_modifier == ExpModifier.default:
|
||||
if self.o.exp_modifier == 100 and self.o.gold_modifier == 100:
|
||||
return rom[0x0B05C0:0x0B05C0 + 18843]
|
||||
|
||||
number_of_enemies: int = 224
|
||||
|
@ -366,6 +368,7 @@ class L2ACWorld(World):
|
|||
for enemy_id in range(number_of_enemies):
|
||||
pointer: int = int.from_bytes(enemy_stats[2 * enemy_id:2 * enemy_id + 2], "little")
|
||||
enemy_stats[pointer + 29:pointer + 31] = self.o.exp_modifier(enemy_stats[pointer + 29:pointer + 31])
|
||||
enemy_stats[pointer + 31:pointer + 33] = self.o.gold_modifier(enemy_stats[pointer + 31:pointer + 33])
|
||||
return enemy_stats
|
||||
|
||||
def get_goal_text_bytes(self) -> bytes:
|
||||
|
@ -383,6 +386,90 @@ class L2ACWorld(World):
|
|||
goal_text_bytes = bytes((0x08, *b"\x03".join(line.encode("ascii") for line in goal_text), 0x00))
|
||||
return goal_text_bytes + b"\x00" * (147 - len(goal_text_bytes))
|
||||
|
||||
def get_shops(self) -> bytes:
|
||||
rom: bytes = get_base_rom_bytes()
|
||||
|
||||
if not self.o.shop_interval:
|
||||
return rom[0x0BEE9F:0x0BEE9F + 1948]
|
||||
|
||||
non_restorative_ids = {int.from_bytes(rom[0x0A713D + 2 * i:0x0A713D + 2 * i + 2], "little") for i in range(31)}
|
||||
restorative_ids = {int.from_bytes(rom[0x08FFDC + 2 * i:0x08FFDC + 2 * i + 2], "little") for i in range(9)}
|
||||
blue_ids = {int.from_bytes(rom[0x0A6EA0 + 2 * i:0x0A6EA0 + 2 * i + 2], "little") for i in range(41)}
|
||||
number_of_spells: int = 35
|
||||
number_of_items: int = 467
|
||||
spells_offset: int = 0x0AFA5B
|
||||
items_offset: int = 0x0B4F69
|
||||
non_restorative_list: List[List[int]] = [list() for _ in range(99)]
|
||||
restorative_list: List[List[int]] = [list() for _ in range(99)]
|
||||
blue_list: List[List[int]] = [list() for _ in range(99)]
|
||||
spell_list: List[List[int]] = [list() for _ in range(99)]
|
||||
gear_list: List[List[int]] = [list() for _ in range(99)]
|
||||
weapon_list: List[List[int]] = [list() for _ in range(99)]
|
||||
custom_list: List[List[int]] = [list() for _ in range(99)]
|
||||
|
||||
for spell_id in range(number_of_spells):
|
||||
pointer: int = int.from_bytes(rom[spells_offset + 2 * spell_id:spells_offset + 2 * spell_id + 2], "little")
|
||||
value: int = int.from_bytes(rom[spells_offset + pointer + 15:spells_offset + pointer + 17], "little")
|
||||
for f in range(value // 1000, 99):
|
||||
spell_list[f].append(spell_id)
|
||||
for item_id in range(number_of_items):
|
||||
pointer = int.from_bytes(rom[items_offset + 2 * item_id:items_offset + 2 * item_id + 2], "little")
|
||||
buckets: List[List[List[int]]] = list()
|
||||
if item_id in non_restorative_ids:
|
||||
buckets.append(non_restorative_list)
|
||||
if item_id in restorative_ids:
|
||||
buckets.append(restorative_list)
|
||||
if item_id in blue_ids:
|
||||
buckets.append(blue_list)
|
||||
if not rom[items_offset + pointer] & 0x20 and not rom[items_offset + pointer + 1] & 0x20:
|
||||
category: int = rom[items_offset + pointer + 7]
|
||||
if category >= 0x02:
|
||||
buckets.append(gear_list)
|
||||
elif category == 0x01:
|
||||
buckets.append(weapon_list)
|
||||
if item_id in self.o.shop_inventory.custom:
|
||||
buckets.append(custom_list)
|
||||
value = int.from_bytes(rom[items_offset + pointer + 5:items_offset + pointer + 7], "little")
|
||||
for bucket in buckets:
|
||||
for f in range(value // 1000, 99):
|
||||
bucket[f].append(item_id)
|
||||
|
||||
if not self.o.gear_variety_after_b9:
|
||||
for f in range(99):
|
||||
del gear_list[f][len(gear_list[f]) % 128:]
|
||||
|
||||
def create_shop(floor: int) -> Tuple[int, ...]:
|
||||
if self.random.randrange(self.o.shop_inventory.total) < self.o.shop_inventory.spell:
|
||||
return create_spell_shop(floor)
|
||||
else:
|
||||
return create_item_shop(floor)
|
||||
|
||||
def create_spell_shop(floor: int) -> Tuple[int, ...]:
|
||||
spells = self.random.sample(spell_list[floor], 3)
|
||||
return 0x03, 0x20, 0x00, *spells, 0xFF
|
||||
|
||||
def create_item_shop(floor: int) -> Tuple[int, ...]:
|
||||
population = non_restorative_list[floor] + restorative_list[floor] + blue_list[floor] \
|
||||
+ gear_list[floor] + weapon_list[floor] + custom_list[floor]
|
||||
weights = itertools.chain(*([weight / len_] * len_ if (len_ := len(list_)) else [] for weight, list_ in
|
||||
[(self.o.shop_inventory.non_restorative, non_restorative_list[floor]),
|
||||
(self.o.shop_inventory.restorative, restorative_list[floor]),
|
||||
(self.o.shop_inventory.blue_chest, blue_list[floor]),
|
||||
(self.o.shop_inventory.gear, gear_list[floor]),
|
||||
(self.o.shop_inventory.weapon, weapon_list[floor])]),
|
||||
(self.o.shop_inventory.custom[item] for item in custom_list[floor]))
|
||||
items = weighted_sample(population, weights, 5, random=self.random)
|
||||
return 0x01, 0x04, 0x00, *(b for item in items for b in item.to_bytes(2, "little")), 0x00, 0x00
|
||||
|
||||
shops = [create_shop(floor)
|
||||
for floor in range(self.o.shop_interval, 99, self.o.shop_interval)
|
||||
for _ in range(self.o.shop_interval)]
|
||||
shop_pointers = itertools.accumulate((len(shop) for shop in shops[:-1]), initial=2 * len(shops))
|
||||
shop_bytes = bytes(itertools.chain(*(p.to_bytes(2, "little") for p in shop_pointers), *shops))
|
||||
|
||||
assert len(shop_bytes) <= 1948, shop_bytes
|
||||
return shop_bytes.ljust(1948, b"\x00")
|
||||
|
||||
@staticmethod
|
||||
def get_node_connection_table() -> bytes:
|
||||
class Connect(IntFlag):
|
||||
|
|
|
@ -71,6 +71,11 @@ org $9EDD60 ; name
|
|||
org $9FA900 ; sprite
|
||||
incbin "ap_logo/ap_logo.bin"
|
||||
warnpc $9FA980
|
||||
; sold out item
|
||||
org $96F9BA ; properties
|
||||
DB $00,$00,$00,$10,$00,$00,$00,$00,$00,$00,$00,$00,$00
|
||||
org $9EDD6C ; name
|
||||
DB "SOLD OUT " ; overwrites "Crown "
|
||||
|
||||
|
||||
org $D08000 ; signature, start of expanded data area
|
||||
|
@ -825,6 +830,119 @@ SpellRNG:
|
|||
|
||||
|
||||
|
||||
; shops
|
||||
pushpc
|
||||
org $83B442
|
||||
; DB=$83, x=1, m=1
|
||||
JSL Shop ; overwrites STA $7FD0BF
|
||||
pullpc
|
||||
|
||||
Shop:
|
||||
STA $7FD0BF ; (overwritten instruction)
|
||||
LDY $05AC ; load map number
|
||||
CPY.b #$F0 ; check if ancient cave
|
||||
BCC +
|
||||
LDA $05B4 ; check if going to ancient cave entrance
|
||||
BEQ +
|
||||
LDA $7FE696 ; load next to next floor number
|
||||
DEC
|
||||
CPY.b #$F1 ; check if going to final floor
|
||||
BCS ++ ; skip a decrement because next floor number is not incremented on final floor
|
||||
DEC
|
||||
++: CMP $D08015 ; check if past initial floor
|
||||
BCC +
|
||||
STA $4204 ; WRDIVL; dividend = floor number
|
||||
STZ $4205 ; WRDIVH
|
||||
TAX
|
||||
LDA $D0801A
|
||||
STA $4206 ; WRDIVB; divisor = shop_interval
|
||||
STA $211C ; M7B; second factor = shop_interval
|
||||
JSL $8082C7 ; advance RNG (while waiting for division to complete)
|
||||
LDY $4216 ; RDMPYL; skip if remainder (i.e., floor number mod shop_interval) is not 0
|
||||
BNE +
|
||||
STA $211B
|
||||
STZ $211B ; M7A; first factor = random number from 0 to 255
|
||||
TXA
|
||||
CLC
|
||||
SBC $2135 ; MPYM; calculate (floor number) - (random number from 0 to shop_interval-1) - 1
|
||||
STA $30 ; set shop id
|
||||
STZ $05A8 ; initialize variable for sold out item tracking
|
||||
STZ $05A9
|
||||
PHB
|
||||
PHP
|
||||
JML $80A33A ; open shop menu
|
||||
+: RTL
|
||||
|
||||
; shop item select
|
||||
pushpc
|
||||
org $82DF50
|
||||
; DB=$83, x=0, m=1
|
||||
JML ShopItemSelected ; overwrites JSR $8B08 : CMP.b #$01
|
||||
pullpc
|
||||
|
||||
ShopItemSelected:
|
||||
LDA $1548 ; check inventory free space
|
||||
BEQ +
|
||||
JSR LoadShopSlotAsFlag
|
||||
BIT $05A8 ; test item not already sold
|
||||
BNE +
|
||||
JML $82DF79 ; skip quantity selection and go directly to buy/equip
|
||||
+: JML $82DF80 ; abort and go back to item selection
|
||||
|
||||
; track bought shop items
|
||||
pushpc
|
||||
org $82E084
|
||||
; DB=$83, x=0, m=1
|
||||
JSL ShopBuy ; overwrites LDA.b #$05 : LDX.w #$0007
|
||||
NOP
|
||||
org $82E10E
|
||||
; DB=$83, x=0, m=1
|
||||
JSL ShopEquip ; overwrites SEP #$10 : LDX $14DC
|
||||
NOP
|
||||
pullpc
|
||||
|
||||
ShopBuy:
|
||||
JSR LoadShopSlotAsFlag
|
||||
TSB $05A8 ; mark item as sold
|
||||
LDA.b #$05 ; (overwritten instruction)
|
||||
LDX.w #$0007 ; (overwritten instruction)
|
||||
RTL
|
||||
|
||||
ShopEquip:
|
||||
JSR LoadShopSlotAsFlag
|
||||
TSB $05A8 ; mark item as sold
|
||||
SEP #$10 ; (overwritten instruction)
|
||||
LDX $14DC ; (overwritten instruction)
|
||||
RTL
|
||||
|
||||
LoadShopSlotAsFlag:
|
||||
TDC
|
||||
LDA $14EC ; load currently selected shop slot number
|
||||
ASL
|
||||
TAX
|
||||
LDA $8ED8C3,X ; load predefined bitmask with a single bit set
|
||||
RTS
|
||||
|
||||
; mark bought items as sold out
|
||||
pushpc
|
||||
org $8285EA
|
||||
; DB=$83, x=0, m=0
|
||||
JSL SoldOut ; overwrites LDA [$FC],Y : AND #$01FF
|
||||
NOP
|
||||
pullpc
|
||||
|
||||
SoldOut:
|
||||
LDA $8ED8C3,X ; load predefined bitmask with a single bit set
|
||||
BIT $05A8 ; test sold items
|
||||
BEQ +
|
||||
LDA.w #$01CB ; load sold out item id
|
||||
BRA ++
|
||||
+: LDA [$FC],Y ; (overwritten instruction)
|
||||
AND #$01FF ; (overwritten instruction)
|
||||
++: RTL
|
||||
|
||||
|
||||
|
||||
; increase variety of red chest gear after B9
|
||||
pushpc
|
||||
org $839176
|
||||
|
@ -1054,6 +1172,7 @@ pullpc
|
|||
; $F02017 1 iris treasures required
|
||||
; $F02018 1 party members available
|
||||
; $F02019 1 capsule monsters available
|
||||
; $F0201A 1 shop interval
|
||||
; $F02030 1 selected goal
|
||||
; $F02031 1 goal completion: boss
|
||||
; $F02032 1 goal completion: iris_treasure_hunt
|
||||
|
|
Binary file not shown.
|
@ -49,8 +49,9 @@ Your Party Leader will hold up the item they received when not in a fight or in
|
|||
- Customize the multiworld item pool. (By default, your pool is filled with random blue chest items, but you can place
|
||||
any cave item you want instead)
|
||||
- Customize start inventory, i.e., begin every run with certain items or spells of your choice
|
||||
- Adjust how much EXP is gained from enemies
|
||||
- Adjust how much EXP and gold is gained from enemies
|
||||
- Randomize enemy movement patterns, enemy sprites, and which enemy types can appear at which floor numbers
|
||||
- Option to make shops appear in the cave so that you have a way to spend your hard-earned gold
|
||||
- Option to shuffle your party members and/or capsule monsters into the multiworld, meaning that someone will have to
|
||||
find them in order to unlock them for you to use. While cave diving, you can add newly unlocked members to your party
|
||||
by using the character items from your inventory
|
||||
|
|
Loading…
Reference in New Issue