Files
dockipelago/worlds/dk64/randomizer/Patching/PuzzleRando.py
Jonathan Tinney 7971961166
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
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

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)