forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
615 lines
26 KiB
Python
615 lines
26 KiB
Python
"""Randomize puzzles."""
|
|
|
|
import math
|
|
from enum import IntEnum, auto
|
|
from randomizer.Enums.Maps import Maps
|
|
from randomizer.Patching.Patcher import LocalROM
|
|
from randomizer.Patching.Library.Generic import IsItemSelected
|
|
from randomizer.Patching.Library.DataTypes import float_to_hex
|
|
from randomizer.Patching.Library.Assets import getPointerLocation, TableNames
|
|
from randomizer.Enums.Settings import FasterChecksSelected, PuzzleRando
|
|
|
|
|
|
def chooseSFX(rando):
|
|
"""Choose random SFX from bank of acceptable SFX."""
|
|
banks = [[98, 138], [166, 247], [249, 252], [398, 411], [471, 476], [519, 535], [547, 575], [614, 631], [644, 650]]
|
|
bank = rando.choice(banks)
|
|
return rando.randint(bank[0], bank[1])
|
|
|
|
|
|
def shiftCastleMinecartRewardZones(ROM_COPY: LocalROM):
|
|
"""Shifts the triggers for the reward point in castle minecart."""
|
|
cont_map_lzs_address = getPointerLocation(TableNames.Triggers, Maps.CastleMinecarts)
|
|
ROM_COPY.seek(cont_map_lzs_address)
|
|
lz_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
for lz_id in range(lz_count):
|
|
start = (lz_id * 0x38) + 2
|
|
ROM_COPY.seek(cont_map_lzs_address + start + 0x10)
|
|
lz_type = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
lz_extra_data = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
if lz_type == 0xA and lz_extra_data == 4:
|
|
# Turn around zone
|
|
offsets = [2, 6, 8]
|
|
for offset in offsets:
|
|
ROM_COPY.seek(cont_map_lzs_address + start + offset)
|
|
ROM_COPY.writeMultipleBytes(0, 2)
|
|
elif lz_type == 0x0 and lz_extra_data == 5:
|
|
new_location = (3232, 482, 693)
|
|
for c in range(3):
|
|
ROM_COPY.seek(cont_map_lzs_address + start + (c * 2))
|
|
ROM_COPY.writeMultipleBytes(new_location[c], 2)
|
|
ROM_COPY.seek(cont_map_lzs_address + start + 6)
|
|
ROM_COPY.writeMultipleBytes(40, 2)
|
|
|
|
|
|
class CarRaceArea(IntEnum):
|
|
"""Car Race Area enum."""
|
|
|
|
null = auto()
|
|
# Castle Car Race
|
|
castle_car_start_finish = auto()
|
|
ramp_up = auto()
|
|
ramp_down = auto()
|
|
tunnel_in = auto()
|
|
tunnel_out = auto()
|
|
around_dartboard_boxes = auto() # Used for enemy AI pathing
|
|
around_arcade_stairs = auto() # Used for enemy AI pathing
|
|
high_turn_around = auto() # Used for enemy AI Pathing
|
|
|
|
|
|
class RaceBound:
|
|
"""Class to store information regarding a race bound."""
|
|
|
|
def __init__(
|
|
self,
|
|
start_x: int,
|
|
start_z: int,
|
|
end_x: int,
|
|
end_z: int,
|
|
direction_is_x: bool,
|
|
checkpoint_count: int,
|
|
area: CarRaceArea = CarRaceArea.null,
|
|
):
|
|
"""Initialize with given parameters."""
|
|
self.start_x = start_x
|
|
self.end_x = end_x
|
|
self.start_z = start_z
|
|
self.end_z = end_z
|
|
self.direction_is_x = direction_is_x
|
|
self.checkpoint_count = checkpoint_count
|
|
self.area = area
|
|
self.placement_radius = 80
|
|
|
|
def getPoints(self, random, y_level: int, placement_bubbles: list) -> list:
|
|
"""Get list of points."""
|
|
if self.area == CarRaceArea.null:
|
|
arr = []
|
|
i = 0
|
|
lower_x = min(self.start_x, self.end_x)
|
|
upper_x = max(self.start_x, self.end_x)
|
|
lower_z = min(self.start_z, self.end_z)
|
|
upper_z = max(self.start_z, self.end_z)
|
|
while i < self.checkpoint_count:
|
|
x = random.randint(lower_x, upper_x)
|
|
z = random.randint(lower_z, upper_z)
|
|
allowed = True
|
|
for bubble in placement_bubbles:
|
|
dx = bubble[0] - x
|
|
dz = bubble[1] - z
|
|
dxz2 = (dx * dx) + (dz * dz)
|
|
if dxz2 < (bubble[2] * bubble[2]):
|
|
allowed = False
|
|
break
|
|
if allowed:
|
|
arr.append((x, y_level, z))
|
|
placement_bubbles.append([x, z, self.placement_radius])
|
|
i += 1
|
|
return arr.copy()
|
|
elif self.area == CarRaceArea.ramp_up:
|
|
return [(2106, 1026, 1122), (1685, 1113, 735)]
|
|
elif self.area == CarRaceArea.ramp_down:
|
|
return [(1738, 1113, 731), (2128, 1026, 1119)]
|
|
elif self.area == CarRaceArea.tunnel_in:
|
|
return [(2779, 1026, 1138), (3045, 1026, 1085)]
|
|
elif self.area == CarRaceArea.tunnel_out:
|
|
return [(3045, 1026, 1085), (2779, 1026, 1138)]
|
|
elif self.area == CarRaceArea.castle_car_start_finish:
|
|
return [(2733, 1026, 1308)]
|
|
elif self.area == CarRaceArea.around_dartboard_boxes:
|
|
return [(2431, 1026, 1166), (2567, 1026, 1157)]
|
|
elif self.area == CarRaceArea.around_arcade_stairs:
|
|
return [(2061, 1026, 1446), (2095, 1026, 1299)]
|
|
elif self.area == CarRaceArea.high_turn_around:
|
|
return [(1565, 1113, 625)]
|
|
return []
|
|
|
|
def getAngle(self, random) -> int:
|
|
"""Get angle for a checkpoint."""
|
|
if self.area == CarRaceArea.castle_car_start_finish:
|
|
return 0
|
|
angle_offset = random.randint(-300, 300)
|
|
if self.direction_is_x:
|
|
if self.start_x > self.end_x:
|
|
return angle_offset + 3072
|
|
else:
|
|
return angle_offset + 1024
|
|
else:
|
|
if self.start_z > self.end_z:
|
|
return angle_offset + 2048
|
|
else:
|
|
new_angle = angle_offset
|
|
if new_angle < 0:
|
|
new_angle += 4096
|
|
return new_angle
|
|
|
|
|
|
def writeRandomCastleCarRace(random, ROM_COPY: LocalROM):
|
|
"""Write random castle car race pathing."""
|
|
# Castle Car Race
|
|
placement_bubbles = []
|
|
bounds = [
|
|
RaceBound(2641, 1419, 2517, 1581, True, 1),
|
|
# RaceBound(2517, 1581, 2352, 1402, True, 1),
|
|
RaceBound(2346, 1450, 2003, 1554, True, 1),
|
|
RaceBound(0, 0, 0, 0, False, 0, CarRaceArea.around_arcade_stairs),
|
|
RaceBound(2073, 1307, 2307, 1194, False, 1),
|
|
RaceBound(0, 0, 0, 0, False, 0, CarRaceArea.ramp_up), # Ramp Up
|
|
RaceBound(1698, 778, 1625, 579, True, 1), # Forward
|
|
RaceBound(0, 0, 0, 0, False, 0, CarRaceArea.high_turn_around),
|
|
RaceBound(1625, 579, 1698, 778, True, 1), # Back
|
|
RaceBound(0, 0, 0, 0, False, 0, CarRaceArea.ramp_down), # Ramp Down
|
|
RaceBound(2189, 1111, 2380, 1010, True, 1),
|
|
RaceBound(0, 0, 0, 0, False, 0, CarRaceArea.around_dartboard_boxes),
|
|
RaceBound(2560, 990, 2692, 1183, True, 1),
|
|
RaceBound(0, 0, 0, 0, False, 0, CarRaceArea.tunnel_in), # Tunnel In
|
|
RaceBound(3070, 1090, 3205, 987, True, 1),
|
|
RaceBound(3205, 1090, 3070, 1162, True, 1),
|
|
RaceBound(0, 0, 0, 0, False, 0, CarRaceArea.tunnel_out), # Tunnel Out
|
|
RaceBound(0, 0, 0, 0, False, 1, CarRaceArea.castle_car_start_finish), # Start/Finish Line
|
|
]
|
|
enemy_car_checkpoints = []
|
|
y_level = 1026
|
|
checkpoint_bytes_order = []
|
|
for bound in bounds:
|
|
if bound.area == CarRaceArea.ramp_up:
|
|
y_level = 1113
|
|
elif bound.area == CarRaceArea.ramp_down:
|
|
y_level = 1026
|
|
new_points = bound.getPoints(random, y_level, placement_bubbles)
|
|
if bound.area in (CarRaceArea.null, CarRaceArea.castle_car_start_finish):
|
|
for i in range(bound.checkpoint_count):
|
|
local_bytes = []
|
|
for j in range(3):
|
|
local_bytes.extend([(new_points[i][j] & 0xFF00) >> 8, new_points[i][j] & 0xFF])
|
|
angle = bound.getAngle(random)
|
|
angle_rad = (angle / 2048) * math.pi
|
|
local_bytes.extend([(angle & 0xFF00) >> 8, angle & 0xFF])
|
|
s_angle = int(float_to_hex(math.sin(angle_rad)), 16)
|
|
c_angle = int(float_to_hex(math.cos(angle_rad)), 16)
|
|
for strength in [s_angle, c_angle]:
|
|
strength_arr = [0, 0, 0, 0]
|
|
val = strength
|
|
for j in range(4):
|
|
strength_arr[3 - j] = val & 0xFF
|
|
val >>= 8
|
|
local_bytes.extend(strength_arr)
|
|
local_bytes.append(2) # Goal
|
|
local_bytes.append(0) # unk11 - always 0
|
|
local_bytes.extend([0, 0]) # unk12 - always 0
|
|
if bound.area == CarRaceArea.castle_car_start_finish:
|
|
local_bytes.extend([0x00, 0x00, 0x00, 0x00]) # Scale
|
|
else:
|
|
local_bytes.extend([0x3F, 0x80, 0x00, 0x00]) # Scale
|
|
local_bytes.extend([2, 0]) # Always 512
|
|
check_type = 0x27 if bound.area == CarRaceArea.null else 0x1A
|
|
local_bytes.extend([0, check_type]) # Seen values of 26,39,42,43,44,47,48,49,50,53,55,65,89,90,110,124
|
|
checkpoint_bytes_order.append(local_bytes)
|
|
enemy_car_checkpoints.extend(new_points)
|
|
will_reverse = random.randint(0, 3) == 0
|
|
if will_reverse and False:
|
|
temp_checkpoints = enemy_car_checkpoints[:-1]
|
|
temp_check_bytes = checkpoint_bytes_order[:-1]
|
|
sf_checkpoint = enemy_car_checkpoints[-1]
|
|
sf_check_bytes = checkpoint_bytes_order[-1]
|
|
temp_checkpoints.reverse()
|
|
temp_check_bytes.reverse()
|
|
temp_checkpoints.append(sf_checkpoint)
|
|
temp_check_bytes.append(sf_check_bytes)
|
|
enemy_car_checkpoints = temp_checkpoints.copy()
|
|
checkpoint_bytes_order = temp_check_bytes.copy()
|
|
# Write Enemy Car AI
|
|
checkpoint_ai_mapping = [
|
|
0xF,
|
|
0x0,
|
|
0x01,
|
|
0x1D,
|
|
0x02,
|
|
0x19,
|
|
0x03,
|
|
0x18,
|
|
0x04,
|
|
0x06,
|
|
0x07,
|
|
0x08,
|
|
0x09,
|
|
0x15,
|
|
0x1C,
|
|
0x12,
|
|
0x10,
|
|
0x05,
|
|
0x0A,
|
|
0x0B,
|
|
0x0C,
|
|
0x0D,
|
|
0x13,
|
|
0x0E,
|
|
0x14,
|
|
0x1B,
|
|
0x1A,
|
|
0x11,
|
|
]
|
|
map_spawners = getPointerLocation(TableNames.Spawners, Maps.CastleTinyRace)
|
|
for point in range(len(checkpoint_ai_mapping)):
|
|
slot = checkpoint_ai_mapping[point]
|
|
ROM_COPY.seek(map_spawners + 36 + (slot * 0xA))
|
|
point_filtered = point
|
|
if point_filtered >= len(enemy_car_checkpoints):
|
|
point_filtered = len(enemy_car_checkpoints) - 1
|
|
coords = enemy_car_checkpoints[point_filtered]
|
|
if len(coords) != 3:
|
|
raise Exception("Invalid tuple size for Castle Car Race.")
|
|
if slot in (22, 23): # Positions used for the end of Castle Car Race
|
|
raise Exception("Invalid slot for Castle Car Race.")
|
|
for item in coords:
|
|
ROM_COPY.writeMultipleBytes(item, 2)
|
|
# Write checkpoint file
|
|
checkpoint_raw_bytes = []
|
|
for check in checkpoint_bytes_order:
|
|
checkpoint_raw_bytes.extend(check)
|
|
start_bytes = [1, 0, len(checkpoint_bytes_order), 0, len(checkpoint_bytes_order)]
|
|
for x in range(len(checkpoint_bytes_order)):
|
|
start_bytes.extend([0, x]) # Add Checkpoint mapping
|
|
start_bytes.extend(checkpoint_raw_bytes)
|
|
if (len(start_bytes) & 0xF) != 0:
|
|
diff = 0x10 - (len(start_bytes) & 0xF)
|
|
for _ in range(diff):
|
|
start_bytes.append(0)
|
|
map_checkpoints = getPointerLocation(TableNames.RaceCheckpoints, Maps.CastleTinyRace)
|
|
ROM_COPY.seek(map_checkpoints)
|
|
ROM_COPY.writeBytes(bytearray(start_bytes))
|
|
|
|
|
|
def shortenCastleMinecart(spoiler, ROM_COPY: LocalROM):
|
|
"""Shorten Castle Minecart to end at the u-turn point."""
|
|
if not IsItemSelected(
|
|
spoiler.settings.faster_checks_enabled,
|
|
spoiler.settings.faster_checks_selected,
|
|
FasterChecksSelected.castle_minecart,
|
|
):
|
|
return
|
|
shiftCastleMinecartRewardZones(ROM_COPY)
|
|
new_squawks_coords = (3232, 482, 693)
|
|
old_squawks_coords = (619, 690, 4134)
|
|
cont_map_spawner_address = getPointerLocation(TableNames.Spawners, Maps.CastleMinecarts)
|
|
ROM_COPY.seek(cont_map_spawner_address)
|
|
fence_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
offset = 2
|
|
fence_bytes = []
|
|
used_fence_ids = []
|
|
fence_4_data = {"fence_6": [], "fence_A": [], "footer": 1}
|
|
if fence_count > 0:
|
|
for x in range(fence_count):
|
|
fence = []
|
|
fence_start = cont_map_spawner_address + offset
|
|
ROM_COPY.seek(cont_map_spawner_address + offset)
|
|
point_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
point_6_offset = offset + 2
|
|
offset += (point_count * 6) + 2
|
|
ROM_COPY.seek(cont_map_spawner_address + offset)
|
|
point0_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
offset + 2
|
|
offset += (point0_count * 10) + 6
|
|
fence_finish = cont_map_spawner_address + offset
|
|
fence_size = fence_finish - fence_start
|
|
ROM_COPY.seek(fence_finish - 4)
|
|
fence_id = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
used_fence_ids.append(fence_id)
|
|
ROM_COPY.seek(fence_start)
|
|
for y in range(int(fence_size / 2)):
|
|
fence.append(int.from_bytes(ROM_COPY.readBytes(2), "big"))
|
|
fence_bytes.append(fence)
|
|
if fence_id == 4:
|
|
# Vanilla Squawks Fence
|
|
for p in range(point_count):
|
|
ROM_COPY.seek(cont_map_spawner_address + point_6_offset + (p * 6))
|
|
local_coords = []
|
|
for c in range(3):
|
|
local_coords.append(int.from_bytes(ROM_COPY.readBytes(2), "big"))
|
|
fence_4_data["fence_6"].append(local_coords)
|
|
# for p in range(point0_count):
|
|
# ROM_COPY.seek(cont_map_spawner_address + point_A_offset + (p * 10))
|
|
# local_coords = []
|
|
# for c in range(5):
|
|
# local_coords.append(int.from_bytes(ROM_COPY.readBytes(2), "big"))
|
|
# fence_4_data["fence_A"].append(local_coords)
|
|
ROM_COPY.seek(fence_finish)
|
|
spawner_count_location = cont_map_spawner_address + offset
|
|
ROM_COPY.seek(spawner_count_location)
|
|
spawner_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
offset += 2
|
|
spawner_bytes = []
|
|
used_enemy_indexes = []
|
|
# Get new fence index
|
|
fence_index = 1
|
|
if fence_index in used_fence_ids:
|
|
while fence_index in used_fence_ids:
|
|
fence_index += 1
|
|
used_fence_ids.append(fence_index)
|
|
# Read Spawners
|
|
for x in range(spawner_count):
|
|
ROM_COPY.seek(cont_map_spawner_address + offset)
|
|
enemy_id = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
ROM_COPY.seek(cont_map_spawner_address + offset + 0x4)
|
|
enemy_coords = []
|
|
for y in range(3):
|
|
coord = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
if coord > 32767:
|
|
coord -= 65536
|
|
enemy_coords.append(coord)
|
|
ROM_COPY.seek(cont_map_spawner_address + offset + 0x13)
|
|
enemy_index = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
used_enemy_indexes.append(enemy_index)
|
|
init_offset = offset
|
|
ROM_COPY.seek(cont_map_spawner_address + offset + 0x11)
|
|
extra_count = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
offset += 0x16 + (extra_count * 2)
|
|
end_offset = offset
|
|
# Get New Spawner Bytes
|
|
data_bytes = []
|
|
spawner_size = end_offset - init_offset
|
|
ROM_COPY.seek(cont_map_spawner_address + init_offset)
|
|
for x in range(spawner_size):
|
|
value = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
if enemy_id == 0x35 and enemy_index == 5:
|
|
if x >= 4 and x < 10:
|
|
coord_slot = int((x - 4) / 2)
|
|
coord_top = (x - 4) % 2
|
|
coord_val = new_squawks_coords[coord_slot]
|
|
write_val = coord_val & 0xFF
|
|
if coord_top == 0:
|
|
write_val = (coord_val >> 8) & 0xFF
|
|
value = write_val
|
|
elif x == 0xE:
|
|
value = fence_index
|
|
data_bytes.append(value)
|
|
spawner_bytes.append(data_bytes)
|
|
# Create new fence
|
|
new_fence_bytes = []
|
|
new_fence_bytes.append(len(fence_4_data["fence_6"])) # 0: Fence Block 0x6 Count, 1: Fence Block 0xA Count
|
|
for point in fence_4_data["fence_6"]:
|
|
for yi, y in enumerate(point):
|
|
diff = y - old_squawks_coords[yi]
|
|
new_fence_bytes.append(new_squawks_coords[yi] + diff)
|
|
new_fence_bytes.append(0)
|
|
new_fence_bytes.append(fence_index)
|
|
new_fence_bytes.append(1)
|
|
fence_bytes.append(new_fence_bytes)
|
|
ROM_COPY.seek(cont_map_spawner_address)
|
|
ROM_COPY.writeMultipleBytes(len(fence_bytes), 2)
|
|
for x in fence_bytes:
|
|
for y in x:
|
|
ROM_COPY.writeMultipleBytes(y, 2)
|
|
ROM_COPY.writeMultipleBytes(len(spawner_bytes), 2)
|
|
for x in spawner_bytes:
|
|
for y in x:
|
|
ROM_COPY.writeMultipleBytes(y, 1)
|
|
|
|
|
|
class PuzzleRandoBound:
|
|
"""Class to store information regarding the bounds of a puzzle requirement."""
|
|
|
|
def __init__(self, lower: int, upper: int):
|
|
"""Initialize with given parameters."""
|
|
self.lower = lower
|
|
self.upper = upper
|
|
self.selected = None
|
|
|
|
def generateRequirement(self, spoiler) -> int:
|
|
"""Generate random requirement between the upper and lower bounds."""
|
|
lower = self.lower
|
|
lower_mid = int((((self.upper - self.lower) / 3) * 1) + self.lower)
|
|
upper_mid = int((((self.upper - self.lower) / 3) * 2) + self.lower)
|
|
upper = self.upper
|
|
selected_upper = upper
|
|
selected_lower = lower
|
|
puzzle_setting = spoiler.settings.puzzle_rando_difficulty
|
|
if puzzle_setting == PuzzleRando.easy:
|
|
selected_lower = lower
|
|
selected_upper = lower_mid
|
|
elif puzzle_setting == PuzzleRando.medium:
|
|
selected_lower = lower_mid
|
|
selected_upper = upper_mid
|
|
elif puzzle_setting == PuzzleRando.hard:
|
|
selected_lower = upper_mid
|
|
selected_upper = upper
|
|
self.selected = spoiler.settings.random.randint(selected_lower, selected_upper)
|
|
return self.selected
|
|
|
|
|
|
class PuzzleItem:
|
|
"""Class to store information regarding a puzzle requirement."""
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
tied_map: Maps,
|
|
offset: int,
|
|
normal_bound: PuzzleRandoBound,
|
|
fast_bound: PuzzleRandoBound = None,
|
|
fast_check_setting: FasterChecksSelected = None,
|
|
):
|
|
"""Initialize with given parameters."""
|
|
self.name = name
|
|
self.tied_map = tied_map
|
|
self.offset = offset
|
|
self.normal_bound = normal_bound
|
|
self.fast_bound = fast_bound
|
|
self.fast_check_setting = fast_check_setting
|
|
self.selected_bound = self.normal_bound
|
|
|
|
def updateBoundSetting(self, spoiler):
|
|
"""Update the settings regarding bounds depending on selected settings."""
|
|
self.selected_bound = self.normal_bound
|
|
if self.fast_check_setting is not None and self.fast_bound is not None:
|
|
if IsItemSelected(spoiler.settings.faster_checks_enabled, spoiler.settings.faster_checks_selected, self.fast_check_setting):
|
|
self.selected_bound = self.fast_bound
|
|
|
|
|
|
def randomize_puzzles(spoiler, ROM_COPY: LocalROM):
|
|
"""Shuffle elements of puzzles. Currently limited to coin challenge requirements but will be extended in future."""
|
|
sav = spoiler.settings.rom_data
|
|
spoiler.coin_requirements = {}
|
|
if spoiler.settings.puzzle_rando_difficulty != PuzzleRando.off:
|
|
coin_req_info = [
|
|
PuzzleItem("Caves Beetle Race", Maps.CavesLankyRace, 0x13C, PuzzleRandoBound(10, 60)),
|
|
PuzzleItem("Aztec Beetle Race", Maps.AztecTinyRace, 0x13D, PuzzleRandoBound(20, 60)),
|
|
PuzzleItem(
|
|
"Factory Car Race",
|
|
Maps.FactoryTinyRace,
|
|
0x13E,
|
|
PuzzleRandoBound(5, 18),
|
|
PuzzleRandoBound(3, 12),
|
|
FasterChecksSelected.factory_car_race,
|
|
),
|
|
PuzzleItem(
|
|
"Galleon Seal Race",
|
|
Maps.GalleonSealRace,
|
|
0x13F,
|
|
PuzzleRandoBound(5, 12),
|
|
PuzzleRandoBound(5, 10),
|
|
FasterChecksSelected.galleon_seal_race,
|
|
),
|
|
PuzzleItem(
|
|
"Castle Car Race",
|
|
Maps.CastleTinyRace,
|
|
0x140,
|
|
PuzzleRandoBound(5, 15),
|
|
PuzzleRandoBound(5, 12),
|
|
FasterChecksSelected.castle_car_race,
|
|
),
|
|
PuzzleItem("Japes Minecart", Maps.JapesMinecarts, 0x141, PuzzleRandoBound(40, 70)),
|
|
PuzzleItem("Forest Minecart", Maps.ForestMinecarts, 0x142, PuzzleRandoBound(25, 60)),
|
|
PuzzleItem(
|
|
"Castle Minecart",
|
|
Maps.CastleMinecarts,
|
|
0x143,
|
|
PuzzleRandoBound(10, 45),
|
|
PuzzleRandoBound(5, 30),
|
|
FasterChecksSelected.castle_minecart,
|
|
),
|
|
]
|
|
for coinreq in coin_req_info:
|
|
coinreq.updateBoundSetting(spoiler)
|
|
ROM_COPY.seek(sav + coinreq.offset)
|
|
selected_requirement = coinreq.selected_bound.generateRequirement(spoiler)
|
|
spoiler.coin_requirements[coinreq.tied_map] = selected_requirement
|
|
ROM_COPY.writeMultipleBytes(selected_requirement, 1)
|
|
chosen_sounds = []
|
|
for matching_head in range(8):
|
|
ROM_COPY.seek(sav + 0x15C + (2 * matching_head))
|
|
sfx = chooseSFX(spoiler.settings.random)
|
|
while sfx in chosen_sounds:
|
|
sfx = chooseSFX(spoiler.settings.random)
|
|
chosen_sounds.append(sfx)
|
|
ROM_COPY.writeMultipleBytes(sfx, 2)
|
|
for piano_item in range(7):
|
|
ROM_COPY.seek(sav + 0x16C + piano_item)
|
|
key = spoiler.settings.random.randint(0, 5)
|
|
ROM_COPY.writeMultipleBytes(key, 1)
|
|
spoiler.dk_face_puzzle = [None] * 9
|
|
spoiler.chunky_face_puzzle = [None] * 9
|
|
for face_puzzle_square in range(9):
|
|
value = spoiler.settings.random.randint(0, 3)
|
|
if face_puzzle_square == 8:
|
|
value = spoiler.settings.random.choice([0, 1, 3]) # Lanky for this square glitches out the puzzle. Nice going Loser kong
|
|
spoiler.dk_face_puzzle[face_puzzle_square] = value
|
|
value = spoiler.settings.random.randint(0, 3)
|
|
if face_puzzle_square == 2:
|
|
value = spoiler.settings.random.choice([0, 1, 3]) # Lanky for this square glitches out the puzzle. Nice going Loser kong again
|
|
spoiler.chunky_face_puzzle[face_puzzle_square] = value
|
|
# Arcade Level Order Rando
|
|
arcade_levels = ["25m", "50m", "75m", "100m"]
|
|
arcade_level_data = {
|
|
"25m": 1,
|
|
"50m": 4,
|
|
"75m": 3,
|
|
"100m": 2,
|
|
}
|
|
spoiler.settings.random.shuffle(arcade_levels)
|
|
# Make sure 75m isn't in the first 2 levels if faster arcade is enabled because 75m is hard
|
|
if IsItemSelected(spoiler.settings.faster_checks_enabled, spoiler.settings.faster_checks_selected, FasterChecksSelected.arcade):
|
|
for x in range(2):
|
|
if arcade_levels[x] == "75m":
|
|
temp_level = arcade_levels[2]
|
|
arcade_levels[2] = arcade_levels[x]
|
|
arcade_levels[x] = temp_level
|
|
spoiler.arcade_order = [0] * 4
|
|
for lvl_index, lvl in enumerate(arcade_levels):
|
|
spoiler.arcade_order[lvl_index] = arcade_level_data[lvl]
|
|
if spoiler.settings.puzzle_rando_difficulty in (PuzzleRando.hard, PuzzleRando.chaos):
|
|
# Random Race Paths
|
|
race_data = {
|
|
# Maps.AngryAztec: {
|
|
# "offset": 0x21E,
|
|
# "center_x": 3280,
|
|
# "center_z": 3829,
|
|
# "radius": [70, 719],
|
|
# "y": [190, 530],
|
|
# "count": 16,
|
|
# "size": 10,
|
|
# "start_angle": None,
|
|
# },
|
|
# Maps.FungiForest: {
|
|
# "offset": 0xC2,
|
|
# "center_x": 1276,
|
|
# "center_z": 3825,
|
|
# "radius": [246, 587],
|
|
# "y": [231, 650],
|
|
# "count": 32,
|
|
# "size": 10,
|
|
# "start_angle": 0,
|
|
# },
|
|
}
|
|
for map_index in race_data:
|
|
map_spawners = getPointerLocation(TableNames.Spawners, map_index)
|
|
map_data = race_data[map_index]
|
|
if map_data["start_angle"] is None:
|
|
initial_angle = spoiler.settings.random.randint(0, 359)
|
|
else:
|
|
initial_angle = map_data["start_angle"]
|
|
previous_offset = None
|
|
for point in range(map_data["count"]):
|
|
ROM_COPY.seek(map_spawners + map_data["offset"] + (point * 0xA))
|
|
if previous_offset is None:
|
|
angle_offset = spoiler.settings.random.randint(-90, 90)
|
|
else:
|
|
angle_magnitude = spoiler.settings.random.randint(0, 90)
|
|
direction = -1
|
|
if previous_offset > 0:
|
|
direction = 1
|
|
change_direction = spoiler.settings.random.randint(0, 3) == 0
|
|
if change_direction:
|
|
direction *= -1
|
|
angle_offset = direction * angle_magnitude
|
|
previous_offset = angle_offset
|
|
initial_angle += angle_offset
|
|
radius = spoiler.settings.random.randint(map_data["radius"][0], map_data["radius"][1])
|
|
angle_rad = (initial_angle / 180) * math.pi
|
|
x = int(map_data["center_x"] + (radius * math.sin(angle_rad)))
|
|
y = spoiler.settings.random.randint(map_data["y"][0], map_data["y"][1])
|
|
z = int(map_data["center_z"] + (radius * math.cos(angle_rad)))
|
|
ROM_COPY.writeMultipleBytes(x, 2)
|
|
ROM_COPY.writeMultipleBytes(y, 2)
|
|
ROM_COPY.writeMultipleBytes(z, 2)
|
|
writeRandomCastleCarRace(spoiler.settings.random, ROM_COPY)
|