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
430 lines
15 KiB
Python
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
|