Files
dockipelago/worlds/papermario/modules/random_formations.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

430 lines
15 KiB
Python

from ..data.actor_data import actor_table
from ..data.formations_meta import (
front_row_enemies,
chapter_battle_mapping,
dont_randomize_formations,
dont_randomize_enemies,
special_random_formations,
random_actor_vars,
gulpit_rocks,
flying_enemies,
battlestage_ceilings,
battlestage_ceiling_formations,
ceiling_enemies
)
def _get_random_formationsize(
chapter_difficulty:int,
do_progressive_scaling:bool,
random
):
"""
Choose the size of the formation from 1-4. This is a function of
chapter difficulty, with later chapters having a higher likelihood for
more enemies.
"""
# Setup formation size chances:
# formations in vanilla game
# enemy count
# 1 2 3 4
# chapter 0 25% 60% 15% 0%
# chapter 1 15% 55% 20% 10%
# chapter 2 10% 45% 30% 15%
# chapter 3 10% 35% 35% 20%
# chapter 4 5% 35% 35% 25%
# chapter 5 0% 35% 40% 25%
# chapter 6 0% 25% 45% 30%
# chapter 7 0% 15% 55% 30%
# chapter 8 0% 10% 50% 40%
if do_progressive_scaling:
# during prog scaling always do 3/4 enemies, since the rom itself
# deletes an enemy if scale is low, resulting in 2/3
size_chances = {
0: [ 0, 0, 60, 40],
1: [ 0, 0, 60, 40],
2: [ 0, 0, 60, 40],
3: [ 0, 0, 60, 40],
4: [ 0, 0, 60, 40],
5: [ 0, 0, 60, 40],
6: [ 0, 0, 60, 40],
7: [ 0, 0, 60, 40],
8: [ 0, 0, 60, 40],
}
else:
size_chances = {
0: [25, 60, 15, 0],
1: [15, 55, 20, 10],
2: [10, 45, 30, 15],
3: [10, 35, 35, 20],
4: [ 5, 35, 35, 25],
5: [ 0, 35, 40, 25],
6: [ 0, 25, 45, 30],
7: [ 0, 15, 55, 30],
8: [ 0, 10, 50, 40],
}
rnd_value = random.random() * 100
probability_count = 0
for size, size_probability in enumerate(size_chances[chapter_difficulty]):
probability_count += size_probability
if rnd_value <= probability_count:
rnd_number_of_enemies = size + 1
break
return rnd_number_of_enemies
def _get_new_formation(
actor_pointers:dict,
area_id:str,
formation_id:str,
enemylist:list,
random
):
formation = []
int_area_id = int(area_id, 16)
int_formation_id = int(formation_id, 16)
# Setup enemy formation file occupancy:
# The number of enemies determines the 'shape' of the formation -- which
# of the four files are occupied by an enemy.
# file a file b file c file d
# # enemies 1 x
# 2 x x
# 3 x x x
# 4 x x x x
enemycount_occupancy_map = {
1: [2],
2: [1,2],
3: [1,2,3],
4: [0,1,2,3]
}
gulpit_in_formation = ("1D_Gulpit" in enemylist)
if gulpit_in_formation:
cur_turn_order = 0x14
else:
cur_turn_order = 0x0A
for i, enemy_pos in enumerate(enemycount_occupancy_map[len(enemylist)]):
# formation setup on example:
# -> 0000010A 8021B0AC 00000000 00000000
# word 1:
# battleID:position:turn order
# 0000 01 0A
# word 2:
# enemy pointer:
# 8021B0AC
# word 3:
# posX:poxY
# 0000 0000
# word 4:
# posZ:var0:var1
# 0000 00 00
current_enemy = enemylist[i]
enemy_on_ceiling = False
battle_string = f"{area_id}-{formation_id}"
if current_enemy in ceiling_enemies:
for stage, battle_list in battlestage_ceiling_formations.items():
if battle_string in battle_list:
# If enemy can be stuck to ceiling, do so
enemy_on_ceiling = True
cur_stage = stage
break
# Each enemy has four home positions (one in each file), usually
# either 0/1/2/3 (ground) or 4/5/6/7 (flying), but certain
# enemies on certain stages can be attached to the ceiling instead.
actual_enemy_pos = enemy_pos
if enemy_on_ceiling:
actual_enemy_pos = 0xFF
elif current_enemy in flying_enemies:
actual_enemy_pos += 4
formation_word_1 = (
(int_area_id << 24)
| (int_formation_id << 16)
| (actual_enemy_pos << 8)
| cur_turn_order
)
formation.append(formation_word_1)
formation_word_2 = actor_pointers.get(current_enemy)
formation.append(formation_word_2)
# If the current battle map has a usable ceiling and we have enemies
# that can roost on the ceiling, put them there
formation_word_3 = 0
xpos = 0
ypos = 0
zpos = 0
if enemy_on_ceiling:
xpos_per_file = [0xF, 0x37, 0x5F, 0x87] # static 15, 55, 95, 135
xpos = xpos_per_file[enemy_pos]
ypos = battlestage_ceilings.get(cur_stage)
zpos = 0xFFDB # static -25
formation_word_3 = (xpos << 16) | ypos
formation.append(formation_word_3)
# Choose actor vars if applicable.
# (certain enemies may have randomized initial values for their
# actor values (ex: spear guy holding spear upward or forward))
actor_var0 = 0
actor_var1 = 0
if current_enemy in random_actor_vars:
if "Var0" in random_actor_vars[current_enemy]:
if current_enemy == "0B_BuzzyBeetle":
if enemy_on_ceiling:
actor_var0 = 1
else:
actor_var0 = random.choice(random_actor_vars[current_enemy]["Var0"])
if "Var1" in random_actor_vars[current_enemy]:
actor_var1 = random.choice(random_actor_vars[current_enemy]["Var1"])
elif current_enemy == "14_SpearGuy":
# first spearguy points spear forward, all others up
actor_var0 = 0 if i == 0 else 1
formation_word_4 = (zpos << 16) | (actor_var0 << 8) | actor_var1
formation.append(formation_word_4)
cur_turn_order -= 1
# And if an enemy is a Gulpit, spawn a bunch of rocks in too.
# They always have the same number and positions.
if gulpit_in_formation:
for i, rocks_hex in enumerate(gulpit_rocks):
updated_rocks_hex = rocks_hex
# Insert area id and battle id into each rock's first word
if i % 4 == 0:
updated_rocks_hex = (
(int_area_id << 24)
| (int_formation_id << 16)
| rocks_hex
)
formation.append(updated_rocks_hex)
return formation
def _get_new_special_formation(
actor_pointers:dict,
area_id:str,
formation_id:str,
chapter_difficulty:int,
do_progressive_scaling:bool,
available_enemies:list,
random
):
special_formation = []
battle_id = f"{area_id}-{formation_id}"
if battle_id == "06-17":
# Billblaster x3 -> add one extra random enemy
cur_enemylist = ["06_BillBlaster", "06_BillBlaster", "06_BillBlaster"]
max_number_of_enemies = 4
elif battle_id == "0B-07":
# StoneChomp x2 -> add one extra random enemy
cur_enemylist = ["0B_StoneChomp", "0B_StoneChomp"]
available_enemies.remove("0B_Swooper")
max_number_of_enemies = 3
elif battle_id == "18-1C":
# AmazyDayzee -> add one extra random enemy
cur_enemylist = ["18_AmazyDayzee"]
max_number_of_enemies = 2
elif battle_id == "25-02":
# BombshellBlaster x2 -> add one extra random enemy
cur_enemylist = ["25_BombshellBlaster", "25_BombshellBlaster"]
max_number_of_enemies = 3
elif battle_id == "25-03":
# BombshellBlaster x2 + Koopatrol -> replace Koopatrol with random enemy
cur_enemylist = ["25_BombshellBlaster", "25_BombshellBlaster"]
max_number_of_enemies = 3
elif battle_id == "25-04":
# BombshellBlaster x2 + MagiKoopa-> replace Koopatrol with random enemy
cur_enemylist = ["25_BombshellBlaster", "25_BombshellBlaster"]
max_number_of_enemies = 3
else:
raise(KeyError)
base_number_of_enemies = len(cur_enemylist)
while True:
if battle_id == "18-1C":
rnd_number_of_enemies = 2
rnd_number_of_enemies = _get_random_formationsize(
chapter_difficulty,
do_progressive_scaling,
random
)
if rnd_number_of_enemies >= base_number_of_enemies:
break
if rnd_number_of_enemies > max_number_of_enemies:
rnd_number_of_enemies = max_number_of_enemies
while rnd_number_of_enemies > len(cur_enemylist):
cur_enemylist.append(random.choice(available_enemies))
special_formation = _get_new_formation(
actor_pointers,
area_id,
formation_id,
cur_enemylist,
random
)
return special_formation
def get_random_formations(
chapter_changes:dict,
do_progressive_scaling:bool,
random
):
battle_formations = []
unused_mediguys = [
"14_MediGuy",
"16_MediGuy",
"18_MediGuy",
]
# Fetch dict of actors and their ROM pointers
actor_pointers = actor_table.copy()
actor_areas = {}
for name, pointer in actor_pointers.items():
# Build list of areas the formations fall into. This requires the actor
# names to be prefixed by the area byte, i.e. "06_KoopaTroopa"
area_id = name[:2]
if not area_id in actor_areas:
actor_areas[area_id] = []
if not name in actor_areas[area_id]:
# Only allow unused MediGuys during ProgressiveScaling
if ( do_progressive_scaling
or not name in unused_mediguys
):
actor_areas[area_id].append(name)
# Loop over all battle formation to be randomized
for battle, front_row_enemy in front_row_enemies.items():
#NYI if formation in vanilla_used_battles:
area_id = battle[:2]
formation_id = battle[3:]
do_randomize_battle = True
for forbidden_formation in dont_randomize_formations:
if ( forbidden_formation[:2] == area_id
and forbidden_formation[3:] in [formation_id, "XX"]
):
do_randomize_battle = False
break
if do_randomize_battle:
# Choose the size of the formation depending on chapter difficulty
for cur_chapter in chapter_battle_mapping:
if area_id in chapter_battle_mapping.get(cur_chapter):
if cur_chapter == 0:
battle_homechapter = 1
else:
battle_homechapter = cur_chapter
break
chapter_difficulty = chapter_changes.get(battle_homechapter)
rnd_number_of_enemies = _get_random_formationsize(
chapter_difficulty,
do_progressive_scaling,
random
)
available_enemies = [
enemy for enemy in actor_areas.get(area_id) if enemy not in dont_randomize_enemies
]
if battle not in special_random_formations:
# Select an enemy at random for each occupied file
current_enemylist = []
placed_healer = False
for i in range(1, rnd_number_of_enemies + 1):
force_matching_firstfile = True
if i == 1 and force_matching_firstfile:
# Match the first enemy in the formation to the enemy
# appearing in the field, so first strikes don't get
# weird
current_enemylist.append(front_row_enemy)
else:
while True:
new_enemy = random.choice(available_enemies)
# In case of bat, check if battle stage has ceiling.
# If not, pick other enemy
if "Swoop" in new_enemy:
for stage in battlestage_ceiling_formations.keys():
if battle in battlestage_ceiling_formations.get(stage):
current_enemylist.append(new_enemy)
break
else:
continue
break
else:
if ( "WMagikoopa" in new_enemy
or "MediGuy" in new_enemy
):
if placed_healer:
# We do not want more than one healing
# enemy in a formation
continue
placed_healer = True
current_enemylist.append(new_enemy)
break
# Build new formation for current battle with chosen enemies
new_formation = _get_new_formation(
actor_pointers,
area_id,
formation_id,
current_enemylist,
random
)
battle_formations.append(new_formation)
else:
# Randomize a certain set of special battles in individual ways
new_special_formation = _get_new_special_formation(
actor_pointers,
area_id,
formation_id,
chapter_difficulty,
do_progressive_scaling,
available_enemies,
random
)
battle_formations.append(new_special_formation)
# Write test formation
#battle_formations.append([
# 0x0000010A,
# 0x802196EC,
# 0x00000000,
# 0x00000000,
# 0x00000209,
# 0x8021B0AC,
# 0x00000000,
# 0x00000000,
# 0x0001010A,
# 0x8021B0AC,
# 0x00000000,
# 0x00000000,
# 0x00010209,
# 0x8021B0AC,
# 0x00000000,
# 0x00000000,
#])
return battle_formations