Files
dockipelago/worlds/mm2/color.py
Silvris 0e6e359747 Mega Man 2: Implement New Game (#3256)
* initial (broken) commit

* small work on init

* Update Items.py

* beginning work, some rom patches

* commit progress from bh branch

* deathlink, fix soft-reset kill, e-tank loss

* begin work on targeting new bhclient

* write font

* definitely didn't forget to add the other two hashes no

* update to modern options, begin colors

* fix 6th letter bug

* palette shuffle + logic rewrite

* fix a bunch of pointers

* fix color changes, deathlink, and add wily 5 req

* adjust weapon weakness generation

* Update Rules.py

* attempt wily 5 softlock fix

* add explicit test for rbm weaknesses

* fix difficulty and hard reset

* fix connect deathlink and off by one item color

* fix atomic fire again

* de-jank deathlink

* rewrite wily5 rule

* fix rare solo-gen fill issue, hopefully

* Update Client.py

* fix wily 5 requirements

* undo fill hook

* fix picopico-kun rules

* for real this time

* update minimum damage requirement

* begin move to procedure patch

* finish move to APPP, allow rando boobeam, color updates

* fix color bug, UT support?

* what do you mean I forgot the procedure

* fix UT?

* plando weakness and fixes

* sfx when item received, more time stopper edge cases

* Update test_weakness.py

* fix rules and color bug

* fix color bug, support reduced flashing

* major world overhaul

* Update Locations.py

* fix first found bugs

* mypy cleanup

* headerless roms

* Update Rom.py

* further cleanup

* work on energylink

* el fixes

* update to energylink 2.0 packet

* energylink balancing

* potentially break other clients, more balancing

* Update Items.py

* remove startup change from basepatch

we write that in patch, since we also need to clean the area before applying

* el balancing and feedback

* hopefully less test failures?

* implement world version check

* add weapon/health option

* Update Rom.py

* x/x2

* specials

* Update Color.py

* Update Options.py

* finally apply location groups

* bump minor version number instead

* fix duplicate stage sends

* validate wily 5, tests

* see if renaming fixes

* add shuffled weakness

* remove passwords

* refresh rbm select, fix wily 5 validation

* forgot we can't check 0

* oops I broke the basepatch (remove failing test later)

* fix solo gen fill error?

* fix webhost patch recognition

* fix imports, basepatch

* move to flexibility metric for boss validation

* special case boobeam trap

* block strobe on stage select init

* more energylink balancing

* bump world version

* wily HP inaccurate in validation

* fix validation edge case

* save last completed wily to data storage

* mypy and pep8 cleanup

* fix file browse validation

* fix test failure, add enemy weakness

* remove test seed

* update enemy damage

* inno setup

* Update en_Mega Man 2.md

* setup guide

* Update en_Mega Man 2.md

* finish plando weakness section

* starting rbm edge case

* remove * imports

* properly wrap later weakness additions in regen playthrough

* fix import

* forgot readme

* remove time stopper special casing

since we moved to proper wily 5 validation, this special casing is no longer important

* properly type added locations

* Update CODEOWNERS

* add animation reduction

* deprioritize Time Stopper in rush checks

* special case wily phase 1

* fix key error

* forgot the test

* music and general cleanup

* the great rename

* fix import

* thanks pycharm

* reorder palette shuffle

* account for alien on shuffled weakness

* apply suggestions

* fix seedbleed

* fix invalid buster passthrough

* fix weakness landing beneath required amount

* fix failsafe

* finish music

* fix Time Stopper on Flash/Alien

* asar pls

* Apply suggestions from code review

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* world helpers

* init cleanup

* apostrophes

* clearer wording

* mypy and cleanup

* options doc cleanup

* Update rom.py

* rules cleanup

* Update __init__.py

* Update __init__.py

* move to defaultdict

* cleanup world helpers

* Update __init__.py

* remove unnecessary line from fill hook

* forgot the other one

* apply code review

* remove collect

* Update rules.py

* forgot another

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-08-20 04:59:29 +02:00

277 lines
9.1 KiB
Python

from typing import Dict, Tuple, List, TYPE_CHECKING, Union
from . import names
from zlib import crc32
import struct
import logging
if TYPE_CHECKING:
from . import MM2World
from .rom import MM2ProcedurePatch
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
}
MM2_COLORS: Dict[str, Tuple[int, int]] = {
names.atomic_fire: (0x28, 0x15),
names.air_shooter: (0x20, 0x11),
names.leaf_shield: (0x20, 0x19),
names.bubble_lead: (0x20, 0x00),
names.time_stopper: (0x34, 0x25),
names.quick_boomerang: (0x34, 0x14),
names.metal_blade: (0x37, 0x18),
names.crash_bomber: (0x20, 0x26),
names.item_1: (0x20, 0x16),
names.item_2: (0x20, 0x16),
names.item_3: (0x20, 0x16),
names.heat_man_stage: (0x28, 0x15),
names.air_man_stage: (0x28, 0x11),
names.wood_man_stage: (0x36, 0x17),
names.bubble_man_stage: (0x30, 0x19),
names.quick_man_stage: (0x28, 0x15),
names.flash_man_stage: (0x30, 0x12),
names.metal_man_stage: (0x28, 0x15),
names.crash_man_stage: (0x30, 0x16)
}
MM2_KNOWN_COLORS: Dict[str, Tuple[int, int]] = {
**MM2_COLORS,
# Street Fighter, technically
"Hadouken": (0x3C, 0x11),
"Shoryuken": (0x38, 0x16),
# X Series
"Z-Saber": (0x20, 0x16),
# 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),
}
palette_pointers: Dict[str, List[int]] = {
"Mega Buster": [0x3D314],
"Atomic Fire": [0x3D318],
"Air Shooter": [0x3D31C],
"Leaf Shield": [0x3D320],
"Bubble Lead": [0x3D324],
"Quick Boomerang": [0x3D328],
"Time Stopper": [0x3D32C],
"Metal Blade": [0x3D330],
"Crash Bomber": [0x3D334],
"Item 1": [0x3D338],
"Item 2": [0x3D33C],
"Item 3": [0x3D340],
"Heat Man": [0x34B6, 0x344F7],
"Air Man": [0x74B6, 0x344FF],
"Wood Man": [0xB4EC, 0x34507],
"Bubble Man": [0xF4B6, 0x3450F],
"Quick Man": [0x134C8, 0x34517],
"Flash Man": [0x174B6, 0x3451F],
"Metal Man": [0x1B4A4, 0x34527],
"Crash Man": [0x1F4EC, 0x3452F],
}
def add_color_to_mm2(name: str, color: Tuple[int, int]) -> None:
"""
Add a color combo for Mega Man 2 to recognize as the color to display for a given item.
For information on available colors: https://www.nesdev.org/wiki/PPU_palettes#2C02
"""
MM2_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 get_colors_for_item(name: str) -> Tuple[int, int]:
if name in MM2_KNOWN_COLORS:
return MM2_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 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: "MM2World", rom: "MM2ProcedurePatch") -> None:
palette_shuffle: Union[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.multiworld.get_player_name(world.player)} "
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 == 1:
shuffled_colors = list(MM2_COLORS.values())
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()
elif palette_shuffle > 1:
if palette_shuffle == 2:
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:
# 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
for character in palettes_to_write:
for pointer in palette_pointers[character]:
rom.write_bytes(pointer, bytes(palettes_to_write[character]))
if character == "Atomic Fire":
# special case, we need to update Atomic Fire's flashing routine
rom.write_byte(0x3DE4A, palettes_to_write[character][1])
rom.write_byte(0x3DE4C, palettes_to_write[character][1])