Files
dockipelago/worlds/mm3/color.py
2026-03-08 21:42:06 +01:00

332 lines
11 KiB
Python

import sys
from typing import TYPE_CHECKING
from . import names
from zlib import crc32
import struct
import logging
if TYPE_CHECKING:
from . import MM3World
from .rom import MM3ProcedurePatch
HTML_TO_NES: dict[str, int] = {
'SNOW': 0x20,
'LINEN': 0x36,
'SEASHELL': 0x36,
'AZURE': 0x3C,
'LAVENDER': 0x33,
'WHITE': 0x30,
'BLACK': 0x0F,
'GREY': 0x00,
'GRAY': 0x00,
'ROYALBLUE': 0x12,
'BLUE': 0x11,
'SKYBLUE': 0x21,
'LIGHTBLUE': 0x31,
'TURQUOISE': 0x2B,
'CYAN': 0x2C,
'AQUAMARINE': 0x3B,
'DARKGREEN': 0x0A,
'GREEN': 0x1A,
'YELLOW': 0x28,
'GOLD': 0x28,
'WHEAT': 0x37,
'TAN': 0x37,
'CHOCOLATE': 0x07,
'BROWN': 0x07,
'SALMON': 0x26,
'ORANGE': 0x27,
'CORAL': 0x36,
'TOMATO': 0x16,
'RED': 0x16,
'PINK': 0x25,
'MAROON': 0x06,
'MAGENTA': 0x24,
'FUSCHIA': 0x24,
'VIOLET': 0x24,
'PLUM': 0x33,
'PURPLE': 0x14,
'THISTLE': 0x34,
'DARKBLUE': 0x01,
'SILVER': 0x10,
'NAVY': 0x02,
'TEAL': 0x1C,
'OLIVE': 0x18,
'LIME': 0x2A,
'AQUA': 0x2C,
# can add more as needed
}
MM3_COLORS: dict[str, tuple[int, int]] = {
names.gemini_laser: (0x30, 0x21),
names.needle_cannon: (0x30, 0x17),
names.hard_knuckle: (0x10, 0x01),
names.magnet_missile: (0x10, 0x16),
names.top_spin: (0x36, 0x00),
names.search_snake: (0x30, 0x19),
names.rush_coil: (0x30, 0x15),
names.spark_shock: (0x30, 0x26),
names.rush_marine: (0x30, 0x15),
names.shadow_blade: (0x34, 0x14),
names.rush_jet: (0x30, 0x15),
names.needle_man_stage: (0x3C, 0x11),
names.magnet_man_stage: (0x30, 0x15),
names.gemini_man_stage: (0x30, 0x21),
names.hard_man_stage: (0x10, 0xC),
names.top_man_stage: (0x30, 0x26),
names.snake_man_stage: (0x30, 0x29),
names.spark_man_stage: (0x30, 0x26),
names.shadow_man_stage: (0x30, 0x11),
names.doc_needle_stage: (0x27, 0x15),
names.doc_gemini_stage: (0x27, 0x15),
names.doc_spark_stage: (0x27, 0x15),
names.doc_shadow_stage: (0x27, 0x15),
}
MM3_KNOWN_COLORS: dict[str, tuple[int, int]] = {
**MM3_COLORS,
# Metroid series
"Varia Suit": (0x27, 0x16),
"Gravity Suit": (0x14, 0x16),
"Phazon Suit": (0x06, 0x1D),
# Street Fighter, technically
"Hadouken": (0x3C, 0x11),
"Shoryuken": (0x38, 0x16),
# X Series
"Z-Saber": (0x20, 0x16),
"Helmet Upgrade": (0x20, 0x01),
"Body Upgrade": (0x20, 0x01),
"Arms Upgrade": (0x20, 0x01),
"Plasma Shot Upgrade": (0x20, 0x01),
"Stock Charge Upgrade": (0x20, 0x01),
"Legs Upgrade": (0x20, 0x01),
# X1
"Homing Torpedo": (0x3D, 0x37),
"Chameleon Sting": (0x3B, 0x1A),
"Rolling Shield": (0x3A, 0x25),
"Fire Wave": (0x37, 0x26),
"Storm Tornado": (0x34, 0x14),
"Electric Spark": (0x3D, 0x28),
"Boomerang Cutter": (0x3B, 0x2D),
"Shotgun Ice": (0x28, 0x2C),
# X2
"Crystal Hunter": (0x33, 0x21),
"Bubble Splash": (0x35, 0x28),
"Spin Wheel": (0x34, 0x1B),
"Silk Shot": (0x3B, 0x27),
"Sonic Slicer": (0x27, 0x01),
"Strike Chain": (0x30, 0x23),
"Magnet Mine": (0x28, 0x2D),
"Speed Burner": (0x31, 0x16),
# X3
"Acid Burst": (0x28, 0x2A),
"Tornado Fang": (0x28, 0x2C),
"Triad Thunder": (0x2B, 0x23),
"Spinning Blade": (0x20, 0x16),
"Ray Splasher": (0x28, 0x17),
"Gravity Well": (0x38, 0x14),
"Parasitic Bomb": (0x31, 0x28),
"Frost Shield": (0x23, 0x2C),
# X4
"Lightning Web": (0x3D, 0x28),
"Aiming Laser": (0x2C, 0x14),
"Double Cyclone": (0x28, 0x1A),
"Rising Fire": (0x20, 0x16),
"Ground Hunter": (0x2C, 0x15),
"Soul Body": (0x37, 0x27),
"Twin Slasher": (0x28, 0x00),
"Frost Tower": (0x3D, 0x2C),
}
if "worlds.mm2" in sys.modules:
# is this the proper way to do this? who knows!
try:
mm2 = sys.modules["worlds.mm2"]
MM3_KNOWN_COLORS.update(mm2.color.MM2_COLORS)
for item in MM3_COLORS:
mm2.color.add_color_to_mm2(item, MM3_COLORS[item])
except AttributeError:
# pass through if an old MM2 is found
pass
palette_pointers: dict[str, list[int]] = {
"Mega Buster": [0x7C8A8, 0x4650],
"Gemini Laser": [0x4654],
"Needle Cannon": [0x4658],
"Hard Knuckle": [0x465C],
"Magnet Missile": [0x4660],
"Top Spin": [0x4664],
"Search Snake": [0x4668],
"Rush Coil": [0x466C],
"Spark Shock": [0x4670],
"Rush Marine": [0x4674],
"Shadow Blade": [0x4678],
"Rush Jet": [0x467C],
"Needle Man": [0x216C],
"Magnet Man": [0x215C],
"Gemini Man": [0x217C],
"Hard Man": [0x2164],
"Top Man": [0x2194],
"Snake Man": [0x2174],
"Spark Man": [0x2184],
"Shadow Man": [0x218C],
"Doc Robot": [0x20B8]
}
def add_color_to_mm3(name: str, color: tuple[int, int]) -> None:
"""
Add a color combo for Mega Man 3 to recognize as the color to display for a given item.
For information on available colors: https://www.nesdev.org/wiki/PPU_palettes#2C02
"""
MM3_KNOWN_COLORS[name] = validate_colors(*color)
def extrapolate_color(color: int) -> tuple[int, int]:
if color > 0x1F:
color_1 = color
color_2 = color_1 - 0x10
else:
color_2 = color
color_1 = color_2 + 0x10
return color_1, color_2
def validate_colors(color_1: int, color_2: int, allow_match: bool = False) -> tuple[int, int]:
# Black should be reserved for outlines, a gray should suffice
if color_1 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
color_1 = 0x10
if color_2 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
color_2 = 0x10
# one final check, make sure we don't have two matching
if not allow_match and color_1 == color_2:
color_1 = 0x30 # color 1 to white works with about any paired color
return color_1, color_2
def expand_colors(color_1: int, color_2: int) -> tuple[tuple[int, int, int], tuple[int, int, int]]:
if color_2 >= 0x30:
color_a = color_b = color_2
else:
color_a = color_2 + 0x10
color_b = color_2
if color_1 < 0x10:
color_c = color_1 + 0x10
color_d = color_1
color_e = color_1 + 0x20
elif color_1 >= 0x30:
color_c = color_1 - 0x10
color_d = color_1 - 0x20
color_e = color_1
else:
color_c = color_1
color_d = color_1 - 0x10
color_e = color_1 + 0x10
return (0x30, color_a, color_b), (color_d, color_e, color_c)
def get_colors_for_item(name: str) -> tuple[tuple[int, int, int], tuple[int, int, int]]:
if name in MM3_KNOWN_COLORS:
return expand_colors(*MM3_KNOWN_COLORS[name])
check_colors = {color: color in name.upper().replace(" ", '') for color in HTML_TO_NES}
colors = [color for color in check_colors if check_colors[color]]
if colors:
# we have at least one color pattern matched
if len(colors) > 1:
# we have at least 2
color_1 = HTML_TO_NES[colors[0]]
color_2 = HTML_TO_NES[colors[1]]
else:
color_1, color_2 = extrapolate_color(HTML_TO_NES[colors[0]])
else:
# generate hash
crc_hash = crc32(name.encode('utf-8'))
hash_color = struct.pack("I", crc_hash)
color_1 = hash_color[0] % 0x3F
color_2 = hash_color[1] % 0x3F
if color_1 < color_2:
temp = color_1
color_1 = color_2
color_2 = temp
color_1, color_2 = validate_colors(color_1, color_2)
return expand_colors(color_1, color_2)
def parse_color(colors: list[str]) -> tuple[int, int]:
color_a = colors[0]
if color_a.startswith("$"):
color_1 = int(color_a[1:], 16)
else:
# assume it's in our list of colors
color_1 = HTML_TO_NES[color_a.upper()]
if len(colors) == 1:
color_1, color_2 = extrapolate_color(color_1)
else:
color_b = colors[1]
if color_b.startswith("$"):
color_2 = int(color_b[1:], 16)
else:
color_2 = HTML_TO_NES[color_b.upper()]
return color_1, color_2
def write_palette_shuffle(world: "MM3World", rom: "MM3ProcedurePatch") -> None:
palette_shuffle: int | str = world.options.palette_shuffle.value
palettes_to_write: dict[str, tuple[int, int]] = {}
if isinstance(palette_shuffle, str):
color_sets = palette_shuffle.split(";")
if len(color_sets) == 1:
palette_shuffle = world.options.palette_shuffle.option_none
# singularity is more correct, but this is faster
else:
palette_shuffle = world.options.palette_shuffle.options[color_sets.pop()]
for color_set in color_sets:
if "-" in color_set:
character, color = color_set.split("-")
if character.title() not in palette_pointers:
logging.warning(f"Player {world.player_name} "
f"attempted to set color for unrecognized option {character}")
colors = color.split("|")
real_colors = validate_colors(*parse_color(colors), allow_match=True)
palettes_to_write[character.title()] = real_colors
else:
# If color is provided with no character, assume singularity
colors = color_set.split("|")
real_colors = validate_colors(*parse_color(colors), allow_match=True)
for character in palette_pointers:
palettes_to_write[character] = real_colors
# Now we handle the real values
if palette_shuffle != 0:
if palette_shuffle > 1:
if palette_shuffle == 3:
# singularity
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
for character in palette_pointers:
if character not in palettes_to_write:
palettes_to_write[character] = real_colors
else:
for character in palette_pointers:
if character not in palettes_to_write:
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
palettes_to_write[character] = real_colors
else:
shuffled_colors = list(MM3_COLORS.values())[:-3] # only include one Doc Robot
shuffled_colors.append((0x2C, 0x11)) # Mega Buster
world.random.shuffle(shuffled_colors)
for character in palette_pointers:
if character not in palettes_to_write:
palettes_to_write[character] = shuffled_colors.pop()
for character in palettes_to_write:
for pointer in palette_pointers[character]:
rom.write_bytes(pointer + 2, bytes(palettes_to_write[character]))