From be7c890bd800472e7dd0521087481d9c11def157 Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Sat, 21 Dec 2024 22:18:45 -0600 Subject: [PATCH 01/13] Start cosmetic refactor #2263 --- randomizer/Patching/ApplyLocal.py | 4 +- randomizer/Patching/ApplyRandomizer.py | 6 +- randomizer/Patching/CosmeticColors.py | 2749 +---------------- randomizer/Patching/Cosmetics/Colorblind.py | 1047 +++++++ .../Patching/Cosmetics/CustomTextures.py | 206 ++ randomizer/Patching/Cosmetics/Holiday.py | 236 ++ randomizer/Patching/Cosmetics/Krusha.py | 181 ++ randomizer/Patching/Cosmetics/ModelSwaps.py | 333 ++ randomizer/Patching/Cosmetics/Puzzles.py | 122 + randomizer/Patching/Cosmetics/TextRando.py | 315 ++ randomizer/Patching/Cosmetics/__init__.py | 0 randomizer/Patching/LibImage.py | 221 +- 12 files changed, 2726 insertions(+), 2694 deletions(-) create mode 100644 randomizer/Patching/Cosmetics/Colorblind.py create mode 100644 randomizer/Patching/Cosmetics/CustomTextures.py create mode 100644 randomizer/Patching/Cosmetics/Holiday.py create mode 100644 randomizer/Patching/Cosmetics/Krusha.py create mode 100644 randomizer/Patching/Cosmetics/ModelSwaps.py create mode 100644 randomizer/Patching/Cosmetics/Puzzles.py create mode 100644 randomizer/Patching/Cosmetics/TextRando.py create mode 100644 randomizer/Patching/Cosmetics/__init__.py diff --git a/randomizer/Patching/ApplyLocal.py b/randomizer/Patching/ApplyLocal.py index fbd2a84d2..87306ba7e 100644 --- a/randomizer/Patching/ApplyLocal.py +++ b/randomizer/Patching/ApplyLocal.py @@ -15,12 +15,12 @@ from randomizer.Enums.Models import Model, ModelNames, HeadResizeImmune from randomizer.Enums.Settings import RandomModels, BigHeadMode from randomizer.Lists.Songs import ExcludedSongsSelector +from randomizer.Patching.Cosmetics.TextRando import writeCrownNames +from randomizer.Patching.Cosmetics.Holiday import applyHolidayMode from randomizer.Patching.CosmeticColors import ( apply_cosmetic_colors, - applyHolidayMode, overwrite_object_colors, writeMiscCosmeticChanges, - writeCrownNames, darkenDPad, darkenPauseBubble, ) diff --git a/randomizer/Patching/ApplyRandomizer.py b/randomizer/Patching/ApplyRandomizer.py index 6d4d57e23..e49838102 100644 --- a/randomizer/Patching/ApplyRandomizer.py +++ b/randomizer/Patching/ApplyRandomizer.py @@ -43,13 +43,11 @@ from randomizer.Patching.BananaPortRando import randomize_bananaport, move_bananaports from randomizer.Patching.BarrelRando import randomize_barrels from randomizer.Patching.CoinPlacer import randomize_coins +from randomizer.Patching.Cosmetics.TextRando import writeBootMessages +from randomizer.Patching.Cosmetics.Puzzles import updateMillLeverTexture, updateCryptLeverTexture, updateDiddyDoors from randomizer.Patching.CosmeticColors import ( applyHelmDoorCosmetics, applyKongModelSwaps, - updateCryptLeverTexture, - updateMillLeverTexture, - writeBootMessages, - updateDiddyDoors, showWinCondition, ) from randomizer.Patching.CratePlacer import randomize_melon_crate diff --git a/randomizer/Patching/CosmeticColors.py b/randomizer/Patching/CosmeticColors.py index c6bfcdb23..b9b7bd9b4 100644 --- a/randomizer/Patching/CosmeticColors.py +++ b/randomizer/Patching/CosmeticColors.py @@ -5,10 +5,8 @@ import gzip import random import zlib -import math from random import randint from typing import TYPE_CHECKING, List, Tuple -from enum import IntEnum, auto from io import BytesIO from PIL import Image, ImageDraw, ImageEnhance @@ -20,26 +18,61 @@ from randomizer.Enums.Maps import Maps from randomizer.Enums.Types import BarrierItems from randomizer.Patching.generate_kong_color_images import convertColors +from randomizer.Patching.Cosmetics.CustomTextures import writeTransition, writeCustomPaintings, writeCustomPortal +from randomizer.Patching.Cosmetics.Krusha import placeKrushaHead, fixBaboonBlasts, kong_index_mapping, fixModelSmallKongCollision +from randomizer.Patching.Cosmetics.Colorblind import ( + recolorKlaptraps, + writeWhiteKasplatHairColorToROM, + recolorBells, + recolorWrinklyDoors, + recolorSlamSwitches, + recolorBlueprintModelTwo, + recolorPotions, + recolorMushrooms, + writeKasplatHairColorToROM, + maskBlueprintImage, + maskLaserImage, + recolorKRoolShipSwitch, + recolorRotatingRoomTiles, +) from randomizer.Patching.Lib import ( - float_to_hex, - getObjectAddress, - int_to_list, - intf_to_float, PaletteFillType, SpawnerChange, applyCharacterSpawnerChanges, compatible_background_textures, - grabText, - writeText, TableNames, getRawFile, - writeRawFile, - Holidays, - getHoliday, ) -from randomizer.Patching.LibImage import getImageFile, TextureFormat, getRandomHueShift, hueShift, ExtraTextures, imageToCI, getBonusSkinOffset +from randomizer.Patching.LibImage import ( + getImageFile, + TextureFormat, + getRandomHueShift, + hueShift, + ExtraTextures, + getBonusSkinOffset, + writeColorImageToROM, + numberToImage, + getRGBFromHash, + maskImage, + maskImageWithColor, + getKongItemColor, + hueShiftImageContainer, +) from randomizer.Patching.Patcher import ROM, LocalROM from randomizer.Settings import Settings +from randomizer.Patching.Cosmetics.ModelSwaps import ( + turtle_models, + panic_models, + bother_models, + piano_models, + piano_extreme_model, + spotlight_fish_models, + candy_cutscene_models, + funky_cutscene_models, + funky_cutscene_models_extreme, + boot_cutscene_models, + melon_random_sprites, +) if TYPE_CHECKING: from PIL.Image import Image @@ -77,337 +110,7 @@ def __init__( self.format = format -turtle_models = [ - Model.Diddy, # Diddy - Model.DK, # DK - Model.Lanky, # Lanky - Model.Tiny, # Tiny - Model.Chunky, # Regular Chunky - Model.ChunkyDisco, # Disco Chunky - Model.Cranky, # Cranky - Model.Funky, # Funky - Model.Candy, # Candy - Model.Seal, # Seal - Model.Enguarde, # Enguarde - Model.BeaverBlue_LowPoly, # Beaver - Model.Squawks_28, # Squawks - Model.KlaptrapGreen, # Klaptrap Green - Model.KlaptrapPurple, # Klaptrap Purple - Model.KlaptrapRed, # Klaptrap Red - Model.KlaptrapTeeth, # Klaptrap Teeth - Model.SirDomino, # Sir Domino - Model.MrDice_41, # Mr Dice - Model.Beetle, # Beetle - Model.NintendoLogo, # N64 Logo - Model.MechanicalFish, # Mech Fish - Model.ToyCar, # Toy Car - Model.BananaFairy, # Fairy - Model.Shuri, # Starfish - Model.Gimpfish, # Gimpfish - Model.Spider, # Spider - Model.Rabbit, # Rabbit - Model.KRoolCutscene, # K Rool - Model.SkeletonHead, # Skeleton Head - Model.Vulture_76, # Vulture - Model.Vulture_77, # Racing Vulture - Model.Tomato, # Tomato - Model.Fly, # Fly - Model.SpotlightFish, # Spotlight Fish - Model.Puftup, # Pufftup - Model.CuckooBird, # Cuckoo Bird - Model.IceTomato, # Ice Tomato - Model.Boombox, # Boombox - Model.KRoolFight, # K Rool (Boxing) - Model.Microphone, # Microbuffer - Model.DeskKRool, # K Rool's Desk - Model.Bell, # Bell - Model.BonusBarrel, # Bonus Barrel - Model.HunkyChunkyBarrel, # HC Barrel - Model.MiniMonkeyBarrel, # MM Barrel - Model.TNTBarrel, # TNT Barrel - Model.Rocketbarrel, # RB Barrel - Model.StrongKongBarrel, # SK Barrel - Model.OrangstandSprintBarrel, # OSS Barrel - Model.BBBSlot_143, # BBB Slot - Model.PlayerCar, # Tiny Car - Model.Boulder, # Boulder - Model.Boat_158, # Boat - Model.Potion, # Potion - Model.ArmyDilloMissle, # AD Missile - Model.TagBarrel, # Tag Barrel - Model.QuestionMark, # Question Mark - Model.Krusha, # Krusha - Model.BananaPeel, # Banana Peel - Model.Butterfly, # Butterfly - Model.FunkyGun, # Funky's Gun -] - -panic_models = [ - Model.Diddy, # Diddy - Model.DK, # DK - Model.Lanky, # Lanky - Model.Tiny, # Tiny - Model.Chunky, # Regular Chunky - Model.ChunkyDisco, # Disco Chunky - Model.Cranky, # Cranky - Model.Funky, # Funky - Model.Candy, # Candy - Model.Seal, # Seal - Model.Enguarde, # Enguarde - Model.BeaverBlue_LowPoly, # Beaver - Model.Squawks_28, # Squawks - Model.KlaptrapGreen, # Klaptrap Green - Model.KlaptrapPurple, # Klaptrap Purple - Model.KlaptrapRed, # Klaptrap Red - Model.MadJack, # Mad Jack - Model.Troff, # Troff - Model.SirDomino, # Sir Domino - Model.MrDice_41, # Mr Dice - Model.RoboKremling, # Robo Kremling - Model.Scoff, # Scoff - Model.Beetle, # Beetle - Model.NintendoLogo, # N64 Logo - Model.MechanicalFish, # Mech Fish - Model.ToyCar, # Toy Car - Model.Klump, # Klump - Model.Dogadon, # Dogadon - Model.BananaFairy, # Fairy - Model.Guard, # Guard - Model.Shuri, # Starfish - Model.Gimpfish, # Gimpfish - Model.KLumsy, # K Lumsy - Model.Spider, # Spider - Model.Rabbit, # Rabbit - # Model.Beanstalk, # Beanstalk - Model.KRoolCutscene, # K Rool - Model.SkeletonHead, # Skeleton Head - Model.Vulture_76, # Vulture - Model.Vulture_77, # Racing Vulture - Model.Ghost, # Ghost - Model.Fly, # Fly - Model.FlySwatter_83, # Fly Swatter - Model.Owl, # Owl - Model.Book, # Book - Model.SpotlightFish, # Spotlight Fish - Model.Puftup, # Pufftup - Model.Mermaid, # Mermaid - Model.Mushroom, # Mushroom Man - Model.Worm, # Worm - Model.EscapeShip, # Escape Ship - Model.KRoolFight, # K Rool (Boxing) - Model.Microphone, # Microbuffer - Model.BonusBarrel, # Bonus Barrel - Model.HunkyChunkyBarrel, # HC Barrel - Model.MiniMonkeyBarrel, # MM Barrel - Model.TNTBarrel, # TNT Barrel - Model.Rocketbarrel, # RB Barrel - Model.StrongKongBarrel, # SK Barrel - Model.OrangstandSprintBarrel, # OSS Barrel - Model.PlayerCar, # Tiny Car - Model.Boulder, # Boulder - Model.VaseCircle, # Vase - Model.VaseColon, # Vase - Model.VaseTriangle, # Vase - Model.VasePlus, # Vase - Model.ArmyDilloMissle, # AD Missile - Model.TagBarrel, # Tag Barrel - Model.QuestionMark, # Question Mark - Model.Krusha, # Krusha - Model.Light, # Light - Model.BananaPeel, # Banana Peel - Model.FunkyGun, # Funky's Gun -] - -bother_models = [ - Model.BeaverBlue_LowPoly, # Beaver - Model.Klobber, # Klobber - Model.Kaboom, # Kaboom - Model.KlaptrapGreen, # Green Klap - Model.KlaptrapPurple, # Purple Klap - Model.KlaptrapRed, # Red Klap - Model.KlaptrapTeeth, # Klap Teeth - Model.Krash, # Krash - Model.Troff, # Troff - Model.NintendoLogo, # N64 Logo - Model.MechanicalFish, # Mech Fish - Model.Krossbones, # Krossbones - Model.Rabbit, # Rabbit - Model.SkeletonHead, # Minecart Skeleton Head - Model.Tomato, # Tomato - Model.IceTomato, # Ice Tomato - Model.GoldenBanana_104, # Golden Banana - Model.Microphone, # Microbuffer - Model.Bell, # Bell - Model.Missile, # Missile (Car Race) - Model.Buoy, # Red Buoy - Model.BuoyGreen, # Green Buoy - Model.RarewareLogo, # Rareware Logo -] - -piano_models = [ - Model.Krash, - Model.RoboKremling, - Model.KoshKremling, - Model.KoshKremlingRed, - Model.Kasplat, - Model.Guard, - Model.Krossbones, - Model.Mermaid, - Model.Mushroom, - Model.GoldenBanana_104, - Model.FlySwatter_83, - Model.Ruler, -] -piano_extreme_model = [ - Model.SkeletonHead, - Model.Owl, - Model.Kosha, - # Model.Beanstalk, -] -spotlight_fish_models = [ - # Model.Turtle, # Lighting Bug - Model.Seal, - Model.BeaverBlue, - Model.BeaverGold, - Model.Zinger, - Model.Squawks_28, - Model.Klobber, - Model.Kaboom, - Model.KlaptrapGreen, - Model.KlaptrapPurple, - Model.KlaptrapRed, - Model.Krash, - # Model.SirDomino, # Lighting issue - # Model.MrDice_41, # Lighting issue - # Model.Ruler, # Lighting issue - # Model.RoboKremling, # Lighting issue - Model.NintendoLogo, - Model.MechanicalFish, - Model.ToyCar, - Model.Kasplat, - Model.BananaFairy, - Model.Guard, - Model.Gimpfish, - # Model.Shuri, # Lighting issue - Model.Spider, - Model.Rabbit, - Model.KRoolCutscene, - Model.KRoolFight, - # Model.SkeletonHead, # Lighting bug - # Model.Vulture_76, # Lighting bug - # Model.Vulture_77, # Lighting bug - # Model.Bat, # Lighting bug - # Model.Tomato, # Lighting bug - # Model.IceTomato, # Lighting bug - # Model.FlySwatter_83, # Lighting bug - Model.SpotlightFish, - Model.Microphone, - # Model.Rocketbarrel, # Model too big, obstructs view - # Model.StrongKongBarrel, # Model too big, obstructs view - # Model.OrangstandSprintBarrel, # Model too big, obstructs view - # Model.MiniMonkeyBarrel, # Model too big, obstructs view - # Model.HunkyChunkyBarrel, # Model too big, obstructs view -] -candy_cutscene_models = [ - Model.Cranky, - # Model.Funky, # Disappears with collision - Model.Candy, - Model.Snide, - Model.Seal, - Model.BeaverBlue, - Model.BeaverGold, - Model.Klobber, - Model.Kaboom, - Model.Krash, - Model.Troff, - Model.Scoff, - Model.RoboKremling, - Model.Beetle, - Model.MrDice_41, - Model.MrDice_56, - Model.BananaFairy, - Model.Rabbit, - Model.KRoolCutscene, - Model.KRoolFight, - Model.Vulture_76, - Model.Vulture_77, - Model.Tomato, - Model.IceTomato, - Model.FlySwatter_83, - Model.Microphone, - Model.StrongKongBarrel, - Model.Rocketbarrel, - Model.OrangstandSprintBarrel, - Model.MiniMonkeyBarrel, - Model.HunkyChunkyBarrel, - Model.RambiCrate, - Model.EnguardeCrate, - Model.Boulder, - Model.SteelKeg, - Model.GoldenBanana_104, -] - -funky_cutscene_models = [ - Model.Cranky, - Model.Candy, - Model.Funky, - Model.Troff, - Model.Scoff, - Model.Ruler, - Model.RoboKremling, - Model.KRoolCutscene, - Model.KRoolFight, - Model.Microphone, -] - -# Not holding gun -funky_cutscene_models_extreme = [ - Model.BeaverBlue, - Model.BeaverGold, - Model.Klobber, - Model.Kaboom, - Model.SirDomino, - Model.MechanicalFish, - Model.BananaFairy, - Model.SkeletonHand, - Model.IceTomato, - Model.Tomato, -] - -boot_cutscene_models = [ - Model.Turtle, - Model.Enguarde, - Model.BeaverBlue, - Model.BeaverGold, - Model.Zinger, - Model.Squawks_28, - Model.KlaptrapGreen, - Model.KlaptrapPurple, - Model.KlaptrapRed, - Model.BananaFairy, - Model.Spider, - Model.Bat, - Model.KRoolGlove, -] - -melon_random_sprites = [ - Sprite.BouncingMelon, - Sprite.BouncingOrange, - Sprite.Coconut, - Sprite.Peanut, - Sprite.Grape, - Sprite.Feather, - Sprite.Pineapple, - Sprite.CrystalCoconut0, - Sprite.DKCoin, - Sprite.DiddyCoin, - Sprite.LankyCoin, - Sprite.TinyCoin, - Sprite.ChunkyCoin, - Sprite.Fairy, - Sprite.RaceCoin, -] model_mapping = { KongModels.default: 0, @@ -892,155 +595,8 @@ def apply_cosmetic_colors(settings: Settings): ) writeColorImageToROM(gb_shine_img, 25, tex, width, height, False, TextureFormat.RGBA5551) - -color_bases = [] balloon_single_frames = [(4, 38), (5, 38), (5, 38), (5, 38), (5, 38), (5, 38), (4, 38), (4, 38)] - -def getRGBFromHash(hash: str): - """Convert hash RGB code to rgb array.""" - red = int(hash[1:3], 16) - green = int(hash[3:5], 16) - blue = int(hash[5:7], 16) - return [red, green, blue] - - -def maskImageWithColor(im_f: Image, mask: tuple): - """Apply rgb mask to image using a rgb color tuple.""" - w, h = im_f.size - converter = ImageEnhance.Color(im_f) - im_f = converter.enhance(0) - im_dupe = im_f.copy() - brightener = ImageEnhance.Brightness(im_dupe) - im_dupe = brightener.enhance(2) - im_f.paste(im_dupe, (0, 0), im_dupe) - pix = im_f.load() - w, h = im_f.size - for x in range(w): - for y in range(h): - base = list(pix[x, y]) - if base[3] > 0: - for channel in range(3): - base[channel] = int(mask[channel] * (base[channel] / 255)) - pix[x, y] = (base[0], base[1], base[2], base[3]) - return im_f - - -def maskImage(im_f, base_index, min_y, keep_dark=False): - """Apply RGB mask to image.""" - w, h = im_f.size - converter = ImageEnhance.Color(im_f) - im_f = converter.enhance(0) - im_dupe = im_f.crop((0, min_y, w, h)) - if keep_dark is False: - brightener = ImageEnhance.Brightness(im_dupe) - im_dupe = brightener.enhance(2) - im_f.paste(im_dupe, (0, min_y), im_dupe) - pix = im_f.load() - mask = getRGBFromHash(color_bases[base_index]) - w, h = im_f.size - for x in range(w): - for y in range(min_y, h): - base = list(pix[x, y]) - if base[3] > 0: - for channel in range(3): - base[channel] = int(mask[channel] * (base[channel] / 255)) - pix[x, y] = (base[0], base[1], base[2], base[3]) - return im_f - - -def maskMushroomImage(im_f, reference_image, color, side_2=False): - """Apply RGB mask to mushroom image.""" - w, h = im_f.size - pixels_to_mask = [] - pix_ref = reference_image.load() - for x in range(w): - for y in range(h): - base_ref = list(pix_ref[x, y]) - # Filter out the white dots that won't get filtered out correctly with the below conditions - if not (max(abs(base_ref[0] - base_ref[2]), abs(base_ref[1] - base_ref[2])) < 41 and abs(base_ref[0] - base_ref[1]) < 11): - # Filter out that one lone pixel that is technically blue AND gets through the above filter, but should REALLY not be blue - if not (side_2 is True and x == 51 and y == 21): - # Select the exact pixels to mask, which is all the "blue" pixels, filtering out the white spots - if base_ref[2] > base_ref[0] and base_ref[2] > base_ref[1] and int(base_ref[0] + base_ref[1]) < 200: - pixels_to_mask.append([x, y]) - # Select the darker blue pixels as well - elif base_ref[2] > int(base_ref[0] + base_ref[1]): - pixels_to_mask.append([x, y]) - pix = im_f.load() - mask = getRGBFromHash(color) - for channel in range(3): - mask[channel] = max(1, mask[channel]) # Absolute black is bad - for x in range(w): - for y in range(h): - base = list(pix[x, y]) - if base[3] > 0 and [x, y] in pixels_to_mask: - average_light = int((base[0] + base[1] + base[2]) / 3) - for channel in range(3): - base[channel] = int(mask[channel] * (average_light / 255)) - pix[x, y] = (base[0], base[1], base[2], base[3]) - return im_f - - -def recolorRotatingRoomTiles(): - """Determine how to recolor the tiles rom the memory game in Donkey's Rotating Room in Caves.""" - question_mark_tiles = [900, 901, 892, 893, 896, 897, 890, 891, 898, 899, 894, 895] - face_tiles = [ - 874, - 878, - 875, - 879, - 876, - 886, - 877, - 885, - 880, - 887, - 881, - 888, - 870, - 872, - 871, - 873, - 866, - 882, - 867, - 883, - 868, - 889, - 869, - 884, - ] - question_mark_tile_masks = [508, 509] - face_tile_masks = [636, 635, 633, 634, 631, 632, 630, 629, 627, 628, 5478, 5478] - question_mark_resize = [17, 37] - face_resize = [[32, 64], [32, 64], [32, 64], [32, 64], [32, 64], [71, 66]] - question_mark_offsets = [[16, 14], [0, 14]] - face_offsets = [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [-5, -1], [-38, -1]] - - for tile in range(len(question_mark_tiles)): - tile_image = getImageFile(7, question_mark_tiles[tile], False, 32, 64, TextureFormat.RGBA5551) - mask = getImageFile(7, question_mark_tile_masks[(tile % 2)], False, 32, 64, TextureFormat.RGBA5551) - resize = question_mark_resize - mask = mask.resize((resize[0], resize[1])) - masked_tile = maskImageRotatingRoomTile(tile_image, mask, question_mark_offsets[(tile % 2)], int(tile / 2), (tile % 2)) - writeColorImageToROM(masked_tile, 7, question_mark_tiles[tile], 32, 64, False, TextureFormat.RGBA5551) - for tile in range(len(face_tiles)): - face_index = int(tile / 4) - if face_index < 5: - width = 32 - height = 64 - else: - width = 44 - height = 44 - mask = getImageFile(25, face_tile_masks[int(tile / 2)], True, width, height, TextureFormat.RGBA5551) - resize = face_resize[face_index] - mask = mask.resize((resize[0], resize[1])) - tile_image = getImageFile(7, face_tiles[tile], False, 32, 64, TextureFormat.RGBA5551) - masked_tile = maskImageRotatingRoomTile(tile_image, mask, face_offsets[int(tile / 2)], face_index, (int(tile / 2) % 2)) - writeColorImageToROM(masked_tile, 7, face_tiles[tile], 32, 64, False, TextureFormat.RGBA5551) - - def getSpinPixels() -> dict: """Get pixels that shouldn't be affected by the mask.""" spin_lengths = { @@ -1122,71 +678,6 @@ def maskImageGBSpin(im_f, color: tuple, image_index: int): px_0[point[0], point[1]] = px[point[0], point[1]] return masked_im - -def maskImageRotatingRoomTile(im_f, im_mask, paste_coords, image_color_index, tile_side): - """Apply RGB mask to image of a Rotating Room Memory Tile.""" - w, h = im_f.size - im_original = im_f - pix_original = im_original.load() - pixels_original = [] - for x in range(w): - pixels_original.append([]) - for y in range(h): - pixels_original[x].append(list(pix_original[x, y]).copy()) - converter = ImageEnhance.Color(im_f) - im_f = converter.enhance(0) - brightener = ImageEnhance.Brightness(im_f) - im_f = brightener.enhance(2) - pix = im_f.load() - pix_mask = im_mask.load() - w2, h2 = im_mask.size - mask_coords = [] - for x in range(w2): - for y in range(h2): - coord = list(pix_mask[x, y]) - if coord[3] > 0: - mask_coords.append([(x + paste_coords[0]), (y + paste_coords[1])]) - if image_color_index < 5: - mask = getRGBFromHash(color_bases[image_color_index]) - for channel in range(3): - mask[channel] = max(39, mask[channel]) # Too dark looks bad - else: - mask = getRGBFromHash(color_bases[2]) - mask2 = getRGBFromHash("#000000") - if image_color_index == 0: - mask2 = getRGBFromHash("#FFFFFF") - for x in range(w): - for y in range(h): - base = list(pix[x, y]) - base_original = list(pixels_original[x][y]) - if [x, y] not in mask_coords: - if image_color_index in [1, 2, 4]: # Diddy, Lanky and Chunky don't get any special features - for channel in range(3): - base[channel] = int(mask[channel] * (base[channel] / 255)) - elif image_color_index in [0, 3]: # Donkey and Tiny get a diamond-shape frame - side = w - if tile_side == 1: - side = 0 - if abs(abs(side - x) - y) < 2 or abs(abs(side - x) - abs(h - y)) < 2: - for channel in range(3): - base[channel] = int(mask2[channel] * (base[channel] / 255)) - else: - for channel in range(3): - base[channel] = int(mask[channel] * (base[channel] / 255)) - else: # Golden Banana gets a block-pattern - if (int(x / 8) + int(y / 8)) % 2 == 0: - for channel in range(3): - base[channel] = int(mask[channel] * (base[channel] / 255)) - else: - for channel in range(3): - base[channel] = int(mask2[channel] * (base[channel] / 255)) - else: - for channel in range(3): - base[channel] = base_original[channel] - pix[x, y] = (base[0], base[1], base[2], base[3]) - return im_f - - def hueShiftColor(color: tuple, amount: int, head_ratio: int = None) -> tuple: """Apply a hue shift to a color.""" # RGB -> HSV Conversion @@ -1252,11 +743,11 @@ def maskImageWithOutline(im_f, base_index, min_y, colorblind_mode, type=""): im_dupe = brightener.enhance(2) im_f.paste(im_dupe, (0, min_y), im_dupe) pix = im_f.load() - mask = getRGBFromHash(color_bases[base_index]) + mask = getRGBFromHash(getKongItemColor(colorblind_mode, base_index)) if base_index == 2 or (base_index == 0 and colorblind_mode == ColorblindMode.trit): # lanky or (DK in tritanopia mode) - border_color = color_bases[4] + border_color = getKongItemColor(colorblind_mode, Kongs.chunky) else: - border_color = color_bases[1] + border_color = getKongItemColor(colorblind_mode, Kongs.diddy) mask2 = getRGBFromHash(border_color) contrast = False if base_index == 0: @@ -1292,831 +783,11 @@ def maskImageWithOutline(im_f, base_index, min_y, colorblind_mode, type=""): pix[x, y] = (mask2[0], mask2[1], mask2[2], base[3]) return im_f - -def writeColorImageToROM( - im_f: PIL.Image.Image, - table_index: int, - file_index: int, - width: int, - height: int, - transparent_border: bool, - format: TextureFormat, -) -> None: - """Write texture to ROM.""" - file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] - file_end = js.pointer_addresses[table_index]["entries"][file_index + 1]["pointing_to"] - file_size = file_end - file_start - try: - LocalROM().seek(file_start) - except Exception: - ROM().seek(file_start) - pix = im_f.load() - width, height = im_f.size - bytes_array = [] - border = 1 - right_border = 3 - for y in range(height): - for x in range(width): - if transparent_border: - if ((x < border) or (y < border) or (x >= (width - border)) or (y >= (height - border))) or (x == (width - right_border)): - pix_data = [0, 0, 0, 0] - else: - pix_data = list(pix[x, y]) - else: - pix_data = list(pix[x, y]) - if format == TextureFormat.RGBA32: - bytes_array.extend(pix_data) - elif format == TextureFormat.RGBA5551: - red = int((pix_data[0] >> 3) << 11) - green = int((pix_data[1] >> 3) << 6) - blue = int((pix_data[2] >> 3) << 1) - alpha = int(pix_data[3] != 0) - value = red | green | blue | alpha - bytes_array.extend([(value >> 8) & 0xFF, value & 0xFF]) - elif format == TextureFormat.IA4: - intensity = pix_data[0] >> 5 - alpha = 0 if pix_data[3] == 0 else 1 - data = ((intensity << 1) | alpha) & 0xF - bytes_array.append(data) - bytes_per_px = 2 - if format == TextureFormat.IA4: - temp_ba = bytes_array.copy() - bytes_array = [] - value_storage = 0 - bytes_per_px = 0.5 - for idx, val in enumerate(temp_ba): - polarity = idx % 2 - if polarity == 0: - value_storage = val << 4 - else: - value_storage |= val - bytes_array.append(value_storage) - data = bytearray(bytes_array) - if format == TextureFormat.RGBA32: - bytes_per_px = 4 - if len(data) > (bytes_per_px * width * height): - print(f"Image too big error: {table_index} > {file_index}") - if table_index in (14, 25): - data = gzip.compress(data, compresslevel=9) - if len(data) > file_size: - print(f"File too big error: {table_index} > {file_index}") - try: - LocalROM().writeBytes(data) - except Exception: - ROM().writeBytes(data) - - -def writeKasplatHairColorToROM(color, table_index, file_index, format: str): - """Write color to ROM for kasplats.""" - file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] - mask = getRGBFromHash(color) - if format == TextureFormat.RGBA32: - color_lst = mask.copy() - color_lst.append(255) # Alpha - null_color = [0] * 4 - else: - val_r = int((mask[0] >> 3) << 11) - val_g = int((mask[1] >> 3) << 6) - val_b = int((mask[2] >> 3) << 1) - rgba_val = val_r | val_g | val_b | 1 - color_lst = [(rgba_val >> 8) & 0xFF, rgba_val & 0xFF] - null_color = [0, 0] - bytes_array = [] - for y in range(42): - for x in range(32): - bytes_array.extend(color_lst) - for i in range(18): - bytes_array.extend(color_lst) - for i in range(4): - bytes_array.extend(null_color) - for i in range(3): - bytes_array.extend(color_lst) - data = bytearray(bytes_array) - if table_index == 25: - data = gzip.compress(data, compresslevel=9) - ROM().seek(file_start) - ROM().writeBytes(data) - - -def writeWhiteKasplatHairColorToROM(color1, color2, table_index, file_index, format: str): - """Write color to ROM for white kasplats, giving them a black-white block pattern.""" - file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] - mask = getRGBFromHash(color1) - mask2 = getRGBFromHash(color2) - if format == TextureFormat.RGBA32: - color_lst_0 = mask.copy() - color_lst_0.append(255) - color_lst_1 = mask2.copy() - color_lst_1.append(255) - null_color = [0] * 4 - else: - val_r = int((mask[0] >> 3) << 11) - val_g = int((mask[1] >> 3) << 6) - val_b = int((mask[2] >> 3) << 1) - rgba_val = val_r | val_g | val_b | 1 - val_r2 = int((mask2[0] >> 3) << 11) - val_g2 = int((mask2[1] >> 3) << 6) - val_b2 = int((mask2[2] >> 3) << 1) - rgba_val2 = val_r2 | val_g2 | val_b2 | 1 - color_lst_0 = [(rgba_val >> 8) & 0xFF, rgba_val & 0xFF] - color_lst_1 = [(rgba_val2 >> 8) & 0xFF, rgba_val2 & 0xFF] - null_color = [0] * 2 - bytes_array = [] - for y in range(42): - for x in range(32): - if (int(y / 7) + int(x / 8)) % 2 == 0: - bytes_array.extend(color_lst_0) - else: - bytes_array.extend(color_lst_1) - for i in range(18): - bytes_array.extend(color_lst_0) - for i in range(4): - bytes_array.extend(null_color) - for i in range(3): - bytes_array.extend(color_lst_0) - data = bytearray(bytes_array) - if table_index == 25: - data = gzip.compress(data, compresslevel=9) - ROM().seek(file_start) - ROM().writeBytes(data) - - -def writeKlaptrapSkinColorToROM(color_index, table_index, file_index, format: str): - """Write color to ROM for klaptraps.""" - im_f = getImageFile(table_index, file_index, True, 32, 43, format) - im_f = maskImage(im_f, color_index, 0, (color_index != 3)) - pix = im_f.load() - file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] - if format == TextureFormat.RGBA32: - null_color = [0] * 4 - else: - null_color = [0, 0] - bytes_array = [] - for y in range(42): - for x in range(32): - color_lst = calculateKlaptrapPixel(list(pix[x, y]), format) - bytes_array.extend(color_lst) - for i in range(18): - color_lst = calculateKlaptrapPixel(list(pix[i, 42]), format) - bytes_array.extend(color_lst) - for i in range(4): - bytes_array.extend(null_color) - for i in range(3): - color_lst = calculateKlaptrapPixel(list(pix[(22 + i), 42]), format) - bytes_array.extend(color_lst) - data = bytearray(bytes_array) - if table_index == 25: - data = gzip.compress(data, compresslevel=9) - ROM().seek(file_start) - ROM().writeBytes(data) - - -def writeSpecialKlaptrapTextureToROM(color_index, table_index, file_index, format: str, pixels_to_ignore: list): - """Write color to ROM for klaptraps special texture(s).""" - im_f = getImageFile(table_index, file_index, True, 32, 43, format) - pix_original = im_f.load() - pixels_original = [] - for x in range(32): - pixels_original.append([]) - for y in range(43): - pixels_original[x].append(list(pix_original[x, y]).copy()) - im_f_masked = maskImage(im_f, color_index, 0, (color_index != 3)) - pix = im_f_masked.load() - file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] - if format == TextureFormat.RGBA32: - null_color = [0] * 4 - else: - null_color = [0, 0] - bytes_array = [] - for y in range(42): - for x in range(32): - if [x, y] not in pixels_to_ignore: - color_lst = calculateKlaptrapPixel(list(pix[x, y]), format) - else: - color_lst = calculateKlaptrapPixel(list(pixels_original[x][y]), format) - bytes_array.extend(color_lst) - for i in range(18): - if [i, 42] not in pixels_to_ignore: - color_lst = calculateKlaptrapPixel(list(pix[i, 42]), format) - else: - color_lst = calculateKlaptrapPixel(list(pixels_original[i][42]), format) - bytes_array.extend(color_lst) - for i in range(4): - bytes_array.extend(null_color) - for i in range(3): - if [(22 + i), 42] not in pixels_to_ignore: - color_lst = calculateKlaptrapPixel(list(pix[(22 + i), 42]), format) - else: - color_lst = calculateKlaptrapPixel(list(pixels_original[(22 + i)][42]), format) - bytes_array.extend(color_lst) - data = bytearray(bytes_array) - if table_index == 25: - data = gzip.compress(data, compresslevel=9) - ROM().seek(file_start) - ROM().writeBytes(data) - - -def calculateKlaptrapPixel(mask: list, format: str): - """Calculate the new color for the given pixel.""" - if format == TextureFormat.RGBA32: - color_lst = mask.copy() - color_lst.append(255) # Alpha - else: - val_r = int((mask[0] >> 3) << 11) - val_g = int((mask[1] >> 3) << 6) - val_b = int((mask[2] >> 3) << 1) - rgba_val = val_r | val_g | val_b | 1 - color_lst = [(rgba_val >> 8) & 0xFF, rgba_val & 0xFF] - return color_lst - - -def maskBlueprintImage(im_f, base_index): - """Apply RGB mask to blueprint image.""" - w, h = im_f.size - im_f_original = im_f - converter = ImageEnhance.Color(im_f) - im_f = converter.enhance(0) - im_dupe = im_f.crop((0, 0, w, h)) - brightener = ImageEnhance.Brightness(im_dupe) - im_dupe = brightener.enhance(2) - im_f.paste(im_dupe, (0, 0), im_dupe) - pix = im_f.load() - pix2 = im_f_original.load() - mask = getRGBFromHash(color_bases[base_index]) - if max(mask[0], max(mask[1], mask[2])) < 39: - for channel in range(3): - mask[channel] = max(39, mask[channel]) # Too black is bad for these items - w, h = im_f.size - for x in range(w): - for y in range(h): - base = list(pix[x, y]) - base2 = list(pix2[x, y]) - if base[3] > 0: - # Filter out the wooden frame - # brown is orange, is red and (red+green), is very little blue - # but, if the color is light, we can't rely on the blue value alone. - if base2[2] > 20 and (base2[2] > base2[1] or base2[1] - base2[2] < 20): - for channel in range(3): - base[channel] = int(mask[channel] * (base[channel] / 255)) - pix[x, y] = (base[0], base[1], base[2], base[3]) - else: - pix[x, y] = (base2[0], base2[1], base2[2], base2[3]) - return im_f - - -def maskLaserImage(im_f, base_index): - """Apply RGB mask to laser texture.""" - w, h = im_f.size - im_f_original = im_f - converter = ImageEnhance.Color(im_f) - im_f = converter.enhance(0) - im_dupe = im_f.crop((0, 0, w, h)) - brightener = ImageEnhance.Brightness(im_dupe) - im_dupe = brightener.enhance(2) - im_f.paste(im_dupe, (0, 0), im_dupe) - pix = im_f.load() - pix2 = im_f_original.load() - mask = getRGBFromHash(color_bases[base_index]) - w, h = im_f.size - for x in range(w): - for y in range(h): - base = list(pix[x, y]) - base2 = list(pix2[x, y]) - if base[3] > 0: - # Filter out the white center of the laser - if min(base2[0], min(base2[1], base2[2])) <= 210: - for channel in range(3): - base[channel] = int(mask[channel] * (base[channel] / 255)) - pix[x, y] = (base[0], base[1], base[2], base[3]) - else: - pix[x, y] = (base2[0], base2[1], base2[2], base2[3]) - return im_f - - -def maskPotionImage(im_f, primary_color, secondary_color=None): - """Apply RGB mask to DK arcade potion reward preview texture.""" - w, h = im_f.size - pix = im_f.load() - mask = getRGBFromHash(primary_color) - if secondary_color is not None: - mask2 = secondary_color - for channel in range(3): - mask[channel] = max(1, mask[channel]) - w, h = im_f.size - for x in range(w): - for y in range(h): - base = list(pix[x, y]) - # Filter out transparent pixels and the cork - if base[3] > 0 and y > 2 and [x, y] not in [[9, 4], [10, 4]]: - # Filter out the bottle's contents - if base[0] == base[1] and base[1] == base[2]: - if secondary_color is not None: - # Color the bottle itself - for channel in range(3): - base[channel] = int(mask2[channel] * (base[channel] / 255)) - else: - # Color the bottle's contents - average_light = int((base[0] + base[1] + base[2]) / 3) - for channel in range(3): - base[channel] = int(mask[channel] * (average_light / 255)) - pix[x, y] = (base[0], base[1], base[2], base[3]) - return im_f - - -def recolorWrinklyDoors(): - """Recolor the Wrinkly hint door doorframes for colorblind mode.""" - file = [0xF0, 0xF2, 0xEF, 0x67, 0xF1] - for kong in range(5): - wrinkly_door_start = js.pointer_addresses[4]["entries"][file[kong]]["pointing_to"] - wrinkly_door_finish = js.pointer_addresses[4]["entries"][file[kong] + 1]["pointing_to"] - wrinkly_door_size = wrinkly_door_finish - wrinkly_door_start - ROM().seek(wrinkly_door_start) - indicator = int.from_bytes(ROM().readBytes(2), "big") - ROM().seek(wrinkly_door_start) - data = ROM().readBytes(wrinkly_door_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) - num_data = [] # data, but represented as nums rather than b strings - for d in data: - num_data.append(d) - # Figure out which colors to use and where to put them (list extensions to mitigate the linter's "artistic freedom" putting 1 value per line) - color1_offsets = [ - 1548, - 1580, - 1612, - 1644, - 1676, - 1708, - 1756, - 1788, - 1804, - 1820, - 1836, - 1852, - 1868, - 1884, - 1900, - 1916, - ] - color1_offsets = color1_offsets + [ - 1932, - 1948, - 1964, - 1980, - 1996, - 2012, - 2028, - 2044, - 2076, - 2108, - 2124, - 2156, - 2188, - 2220, - 2252, - 2284, - ] - color1_offsets = color1_offsets + [ - 2316, - 2348, - 2380, - 2396, - 2412, - 2428, - 2444, - 2476, - 2508, - 2540, - 2572, - 2604, - 2636, - 2652, - 2668, - 2684, - ] - color1_offsets = color1_offsets + [ - 2700, - 2716, - 2732, - 2748, - 2764, - 2780, - 2796, - 2812, - 2828, - 2860, - 2892, - 2924, - 2956, - 2988, - 3020, - 3052, - ] - color2_offsets = [ - 1564, - 1596, - 1628, - 1660, - 1692, - 1724, - 1740, - 1772, - 2332, - 2364, - 2460, - 2492, - 2524, - 2556, - 2588, - 2620, - ] - new_color1 = getRGBFromHash(color_bases[kong]) - new_color2 = getRGBFromHash(color_bases[kong]) - if kong == 0: - for channel in range(3): - new_color2[channel] = max(80, new_color1[channel]) # Too black is bad, because anything times 0 is 0 - - # Recolor the doorframe - for offset in color1_offsets: - for i in range(3): - num_data[offset + i] = new_color1[i] - for offset in color2_offsets: - for i in range(3): - num_data[offset + i] = new_color2[i] - - data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - ROM().seek(wrinkly_door_start) - ROM().writeBytes(data) - - -def recolorSlamSwitches(galleon_switch_value, ROM_COPY: ROM): - """Recolor the Simian Slam switches for colorblind mode.""" - file = [0x94, 0x93, 0x95, 0x96, 0xB8, 0x16C, 0x16B, 0x16D, 0x16E, 0x16A, 0x167, 0x166, 0x168, 0x169, 0x165] - written_galleon_ship = False - for switch in range(15): - slam_switch_start = js.pointer_addresses[4]["entries"][file[switch]]["pointing_to"] - slam_switch_finish = js.pointer_addresses[4]["entries"][file[switch] + 1]["pointing_to"] - slam_switch_size = slam_switch_finish - slam_switch_start - ROM().seek(slam_switch_start) - indicator = int.from_bytes(ROM().readBytes(2), "big") - ROM().seek(slam_switch_start) - data = ROM().readBytes(slam_switch_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) - num_data = [] # data, but represented as nums rather than b strings - for d in data: - num_data.append(d) - # Figure out which colors to use and where to put them - color_offsets = [1828, 1844, 1860, 1876, 1892, 1908] - new_color1 = getRGBFromHash(color_bases[4]) # chunky's color - new_color2 = getRGBFromHash(color_bases[2]) # lanky's color - new_color3 = getRGBFromHash(color_bases[1]) # diddy's color - - # Green switches - if switch < 5: - for offset in color_offsets: - for i in range(3): - num_data[offset + i] = new_color1[i] - # Blue switches - elif switch < 10: - for offset in color_offsets: - for i in range(3): - num_data[offset + i] = new_color2[i] - # Red switches - else: - for offset in color_offsets: - for i in range(3): - num_data[offset + i] = new_color3[i] - - data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - ROM().seek(slam_switch_start) - ROM().writeBytes(data) - if not written_galleon_ship: - galleon_switch_color = new_color1.copy() - if galleon_switch_value is not None: - if galleon_switch_value != 1: - galleon_switch_color = new_color3.copy() - if galleon_switch_value == 2: - galleon_switch_color = new_color2.copy() - recolorKRoolShipSwitch(galleon_switch_color, ROM_COPY) - written_galleon_ship = True - - -def recolorBlueprintModelTwo(): - """Recolor the Blueprint Model2 items for colorblind mode.""" - file = [0xDE, 0xE0, 0xE1, 0xDD, 0xDF] - for kong in range(5): - blueprint_model2_start = js.pointer_addresses[4]["entries"][file[kong]]["pointing_to"] - blueprint_model2_finish = js.pointer_addresses[4]["entries"][file[kong] + 1]["pointing_to"] - blueprint_model2_size = blueprint_model2_finish - blueprint_model2_start - ROM().seek(blueprint_model2_start) - indicator = int.from_bytes(ROM().readBytes(2), "big") - ROM().seek(blueprint_model2_start) - data = ROM().readBytes(blueprint_model2_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) - num_data = [] # data, but represented as nums rather than b strings - for d in data: - num_data.append(d) - # Figure out which colors to use and where to put them - color1_offsets = [0x52C, 0x54C, 0x57C, 0x58C, 0x5AC, 0x5CC, 0x5FC, 0x61C] - color2_offsets = [0x53C, 0x55C, 0x5EC, 0x60C] - color3_offsets = [0x56C, 0x59C, 0x5BC, 0x5DC] - new_color = getRGBFromHash(color_bases[kong]) - if kong == 0: - for channel in range(3): - new_color[channel] = max(39, new_color[channel]) # Too black is bad, because anything times 0 is 0 - - # Recolor the model2 item - for offset in color1_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color2_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color3_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - - data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - ROM().seek(blueprint_model2_start) - ROM().writeBytes(data) - - -def recolorBells(): - """Recolor the Chunky Minecart bells for colorblind mode (prot/deut).""" - file = 693 - minecart_bell_start = js.pointer_addresses[4]["entries"][file]["pointing_to"] - minecart_bell_finish = js.pointer_addresses[4]["entries"][file + 1]["pointing_to"] - minecart_bell_size = minecart_bell_finish - minecart_bell_start - ROM().seek(minecart_bell_start) - indicator = int.from_bytes(ROM().readBytes(2), "big") - ROM().seek(minecart_bell_start) - data = ROM().readBytes(minecart_bell_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) - num_data = [] # data, but represented as nums rather than b strings - for d in data: - num_data.append(d) - # Figure out which colors to use and where to put them - color1_offsets = [0x214, 0x244, 0x264, 0x274, 0x284] - color2_offsets = [0x224, 0x234, 0x254] - new_color1 = getRGBFromHash("#0066FF") - new_color2 = getRGBFromHash("#0000FF") - - # Recolor the bell - for offset in color1_offsets: - for i in range(3): - num_data[offset + i] = new_color1[i] - for offset in color2_offsets: - for i in range(3): - num_data[offset + i] = new_color2[i] - - data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - ROM().seek(minecart_bell_start) - ROM().writeBytes(data) - - -def recolorKlaptraps(): - """Recolor the klaptrap models for colorblind mode.""" - green_files = [0xF31, 0xF32, 0xF33, 0xF35, 0xF37, 0xF39] # 0xF2F collar? 0xF30 feet? - red_files = [0xF44, 0xF45, 0xF46, 0xF47, 0xF48, 0xF49] # , 0xF42 collar? 0xF43 feet? - purple_files = [0xF3C, 0xF3D, 0xF3E, 0xF3F, 0xF40, 0xF41] # 0xF3B feet?, 0xF3A collar? - - # Regular textures - for file in range(6): - writeKlaptrapSkinColorToROM(4, 25, green_files[file], TextureFormat.RGBA5551) - writeKlaptrapSkinColorToROM(1, 25, red_files[file], TextureFormat.RGBA5551) - writeKlaptrapSkinColorToROM(3, 25, purple_files[file], TextureFormat.RGBA5551) - - belly_pixels_to_ignore = [] - for x in range(32): - for y in range(43): - if y < 29 or (y > 31 and y < 39) or y == 40 or y == 42: - belly_pixels_to_ignore.append([x, y]) - elif (y == 39 and x < 16) or (y == 41 and x < 24): - belly_pixels_to_ignore.append([x, y]) - - # Special texture that requires only partial recoloring, in this case file 0xF38 which is the belly, and only the few green pixels - writeSpecialKlaptrapTextureToROM(4, 25, 0xF38, TextureFormat.RGBA5551, belly_pixels_to_ignore) - - -def recolorPotions(colorblind_mode): - """Overwrite potion colors.""" - secondary_color = [color_bases[1], None, color_bases[4], color_bases[1], None, None] - if colorblind_mode == ColorblindMode.trit: - secondary_color[0] = color_bases[4] - secondary_color[2] = None - for color in range(len(secondary_color)): - if secondary_color[color] is not None: - secondary_color[color] = getRGBFromHash(secondary_color[color]) - - # Actor: - file = [[0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2], [0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA]] - for type in range(2): - for potion_color in range(6): - potion_actor_start = js.pointer_addresses[5]["entries"][file[type][potion_color]]["pointing_to"] - potion_actor_finish = js.pointer_addresses[5]["entries"][file[type][potion_color] + 1]["pointing_to"] - potion_actor_size = potion_actor_finish - potion_actor_start - ROM().seek(potion_actor_start) - indicator = int.from_bytes(ROM().readBytes(2), "big") - ROM().seek(potion_actor_start) - data = ROM().readBytes(potion_actor_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) - num_data = [] # data, but represented as nums rather than b strings - for d in data: - num_data.append(d) - # Figure out which colors to use and where to put them - color1_offsets = [0x34] - color2_offsets = [0x44, 0x54, 0xA4] - color3_offsets = [0x64, 0x74, 0x84, 0xE4] - color4_offsets = [0x94] - color5_offsets = [0xB4, 0xC4, 0xD4] - # color6_offsets = [0xF4, 0x104, 0x114, 0x124, 0x134, 0x144, 0x154, 0x164] - if potion_color < 5: - new_color = getRGBFromHash(color_bases[potion_color]) - else: - new_color = getRGBFromHash("#FFFFFF") - - # Recolor the actor item - for offset in color1_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - if secondary_color[potion_color] is not None and potion_color == 3: # tiny - num_data[offset + i] = int(num_data[offset + i] * (secondary_color[potion_color][i] / 255)) - elif secondary_color[potion_color] is not None: # donkey gets an even darker shade - num_data[offset + i] = int(num_data[offset + i] * (int(secondary_color[potion_color][i] / 8) / 255)) - elif secondary_color[potion_color] is not None: # other kongs with a secondary color get a darker shade - num_data[offset + i] = int(num_data[offset + i] * (int(secondary_color[potion_color][i] / 4) / 255)) - else: - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color2_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color3_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color4_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color5_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - - data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - if len(data) > potion_actor_size: - print(f"Attempted size bigger {hex(len(data))} than slot {hex(potion_actor_size)}") - continue - ROM().seek(potion_actor_start) - ROM().writeBytes(data) - - # Model2: - file = [91, 498, 89, 499, 501, 502] - for potion_color in range(6): - potion_model2_start = js.pointer_addresses[4]["entries"][file[potion_color]]["pointing_to"] - potion_model2_finish = js.pointer_addresses[4]["entries"][file[potion_color] + 1]["pointing_to"] - potion_model2_size = potion_model2_finish - potion_model2_start - ROM().seek(potion_model2_start) - indicator = int.from_bytes(ROM().readBytes(2), "big") - ROM().seek(potion_model2_start) - data = ROM().readBytes(potion_model2_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) - num_data = [] # data, but represented as nums rather than b strings - for d in data: - num_data.append(d) - # Figure out which colors to use and where to put them - color1_offsets = [0x144] - color2_offsets = [0x154, 0x164, 0x1B4] - color3_offsets = [0x174, 0x184, 0x194, 0x1F4] - color4_offsets = [0x1A4] - color5_offsets = [0x1C4, 0x1D4, 0x1E4] - # color6_offsets = [0x204, 0x214, 0x224, 0x234, 0x244, 0x254, 0x264, 0x274] - if potion_color < 5: - new_color = getRGBFromHash(color_bases[potion_color]) - else: - new_color = getRGBFromHash("#FFFFFF") - - # Recolor the model2 item - for offset in color1_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - if secondary_color[potion_color] is not None and potion_color == 3: # tiny - num_data[offset + i] = int(num_data[offset + i] * (secondary_color[potion_color][i] / 255)) - elif secondary_color[potion_color] is not None: # donkey gets an even darker shade - num_data[offset + i] = int(num_data[offset + i] * (int(secondary_color[potion_color][i] / 8) / 255)) - elif secondary_color[potion_color] is not None: # other kongs with a secondary color get a darker shade - num_data[offset + i] = int(num_data[offset + i] * (int(secondary_color[potion_color][i] / 4) / 255)) - else: - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color2_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color3_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color4_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color5_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - - data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - ROM().seek(potion_model2_start) - ROM().writeBytes(data) - - return - # DK Arcade sprites - for file in range(8, 14): - index = file - 8 - if index < 5: - color = color_bases[index] - else: - color = "#FFFFFF" - potion_image = getImageFile(6, file, False, 20, 20, TextureFormat.RGBA5551) - potion_image = maskPotionImage(potion_image, color, secondary_color[index]) - writeColorImageToROM(potion_image, 6, file, 20, 20, False, TextureFormat.RGBA5551) - - -def recolorMushrooms(): - """Recolor the various colored mushrooms in the game for colorblind mode.""" - reference_mushroom_image = getImageFile(7, 297, False, 32, 32, TextureFormat.RGBA5551) - reference_mushroom_image_side1 = getImageFile(25, 0xD64, True, 64, 32, TextureFormat.RGBA5551) - reference_mushroom_image_side2 = getImageFile(25, 0xD65, True, 64, 32, TextureFormat.RGBA5551) - files_table_7 = [296, 295, 297, 299, 298] - files_table_25_side_1 = [0xD60, getBonusSkinOffset(ExtraTextures.MushTop0), 0xD64, 0xD62, 0xD66] - files_table_25_side_2 = [0xD61, getBonusSkinOffset(ExtraTextures.MushTop1), 0xD65, 0xD63, 0xD67] - for file in range(5): - # Mushroom on the ceiling inside Fungi Forest Lobby - mushroom_image = getImageFile(7, files_table_7[file], False, 32, 32, TextureFormat.RGBA5551) - mushroom_image = maskMushroomImage(mushroom_image, reference_mushroom_image, color_bases[file]) - writeColorImageToROM(mushroom_image, 7, files_table_7[file], 32, 32, False, TextureFormat.RGBA5551) - # Mushrooms in Lanky's colored mushroom puzzle (and possibly also the bouncy mushrooms) - mushroom_image_side_1 = getImageFile(25, files_table_25_side_1[file], True, 64, 32, TextureFormat.RGBA5551) - mushroom_image_side_1 = maskMushroomImage(mushroom_image_side_1, reference_mushroom_image_side1, color_bases[file]) - writeColorImageToROM(mushroom_image_side_1, 25, files_table_25_side_1[file], 64, 32, False, TextureFormat.RGBA5551) - mushroom_image_side_2 = getImageFile(25, files_table_25_side_2[file], True, 64, 32, TextureFormat.RGBA5551) - mushroom_image_side_2 = maskMushroomImage(mushroom_image_side_2, reference_mushroom_image_side2, color_bases[file], True) - writeColorImageToROM(mushroom_image_side_2, 25, files_table_25_side_2[file], 64, 32, False, TextureFormat.RGBA5551) - - BALLOON_START = [5835, 5827, 5843, 5851, 5819] def overwrite_object_colors(settings, ROM_COPY: ROM): """Overwrite object colors.""" - global color_bases mode = settings.colorblind_mode sav = settings.rom_data galleon_switch_value = None @@ -2126,12 +797,6 @@ def overwrite_object_colors(settings, ROM_COPY: ROM): ROM_COPY.seek(sav + 0x104 + 3) galleon_switch_value = int.from_bytes(ROM_COPY.readBytes(1), "big") if mode != ColorblindMode.off: - if mode == ColorblindMode.prot: - color_bases = ["#000000", "#0072FF", "#766D5A", "#FFFFFF", "#FDE400"] - elif mode == ColorblindMode.deut: - color_bases = ["#000000", "#318DFF", "#7F6D59", "#FFFFFF", "#E3A900"] - elif mode == ColorblindMode.trit: - color_bases = ["#000000", "#C72020", "#13C4D8", "#FFFFFF", "#FFA4A4"] if mode in (ColorblindMode.prot, ColorblindMode.deut): recolorBells() # Preload DK single cb image to paste onto balloons @@ -2143,37 +808,37 @@ def overwrite_object_colors(settings, ROM_COPY: ROM): for file in range(8): blueprint_lanky.append(getImageFile(25, 5519 + (file), True, 48, 42, TextureFormat.RGBA5551)) writeWhiteKasplatHairColorToROM("#FFFFFF", "#000000", 25, 4125, TextureFormat.RGBA5551) - recolorWrinklyDoors() - recolorSlamSwitches(galleon_switch_value, ROM_COPY) - recolorRotatingRoomTiles() - recolorBlueprintModelTwo() - recolorKlaptraps() + recolorWrinklyDoors(mode) + recolorSlamSwitches(galleon_switch_value, ROM_COPY, mode) + recolorRotatingRoomTiles(mode) + recolorBlueprintModelTwo(mode) + recolorKlaptraps(mode) recolorPotions(mode) - recolorMushrooms() + recolorMushrooms(mode) for kong_index in range(5): # file = 4120 # # Kasplat Hair # hair_im = getFile(25, file, True, 32, 44, TextureFormat.RGBA5551) # hair_im = maskImage(hair_im, kong_index, 0) # writeColorImageToROM(hair_im, 25, [4124, 4122, 4123, 4120, 4121][kong_index], 32, 44, False) - writeKasplatHairColorToROM(color_bases[kong_index], 25, [4124, 4122, 4123, 4120, 4121][kong_index], TextureFormat.RGBA5551) + writeKasplatHairColorToROM(getKongItemColor(mode, kong_index), 25, [4124, 4122, 4123, 4120, 4121][kong_index], TextureFormat.RGBA5551) for file in range(5519, 5527): # Blueprint sprite blueprint_start = [5624, 5608, 5519, 5632, 5616] blueprint_im = blueprint_lanky[(file - 5519)] - blueprint_im = maskBlueprintImage(blueprint_im, kong_index) + blueprint_im = maskBlueprintImage(blueprint_im, kong_index, mode) writeColorImageToROM(blueprint_im, 25, blueprint_start[kong_index] + (file - 5519), 48, 42, False, TextureFormat.RGBA5551) for file in range(4925, 4931): # Shockwave shockwave_start = [4897, 4903, 4712, 4950, 4925] shockwave_im = getImageFile(25, shockwave_start[kong_index] + (file - 4925), True, 32, 32, TextureFormat.RGBA32) - shockwave_im = maskImage(shockwave_im, kong_index, 0) + shockwave_im = maskImage(shockwave_im, kong_index, 0, False, mode) writeColorImageToROM(shockwave_im, 25, shockwave_start[kong_index] + (file - 4925), 32, 32, False, TextureFormat.RGBA32) for file in range(784, 796): # Helm Laser (will probably also affect the Pufftoss laser and the Game Over laser) laser_start = [784, 748, 363, 760, 772] laser_im = getImageFile(7, laser_start[kong_index] + (file - 784), False, 32, 32, TextureFormat.RGBA32) - laser_im = maskLaserImage(laser_im, kong_index) + laser_im = maskLaserImage(laser_im, kong_index, mode) writeColorImageToROM(laser_im, 7, laser_start[kong_index] + (file - 784), 32, 32, False, TextureFormat.RGBA32) if kong_index == 0 or kong_index == 3 or (kong_index == 2 and mode != ColorblindMode.trit): # Lanky (prot, deut only) or DK or Tiny for file in range(152, 160): @@ -2205,24 +870,24 @@ def overwrite_object_colors(settings, ROM_COPY: ROM): # Single single_start = [168, 152, 232, 208, 240] single_im = getImageFile(7, single_start[kong_index] + (file - 152), False, 44, 44, TextureFormat.RGBA5551) - single_im = maskImage(single_im, kong_index, 0) + single_im = maskImage(single_im, kong_index, 0, False, mode) writeColorImageToROM(single_im, 7, single_start[kong_index] + (file - 152), 44, 44, False, TextureFormat.RGBA5551) for file in range(216, 224): # Coin coin_start = [224, 256, 248, 216, 264] coin_im = getImageFile(7, coin_start[kong_index] + (file - 216), False, 48, 42, TextureFormat.RGBA5551) - coin_im = maskImage(coin_im, kong_index, 0) + coin_im = maskImage(coin_im, kong_index, 0, False, mode) writeColorImageToROM(coin_im, 7, coin_start[kong_index] + (file - 216), 48, 42, False, TextureFormat.RGBA5551) for file in range(274, 286): # Bunch bunch_start = [274, 854, 818, 842, 830] bunch_im = getImageFile(7, bunch_start[kong_index] + (file - 274), False, 44, 44, TextureFormat.RGBA5551) - bunch_im = maskImage(bunch_im, kong_index, 0, True) + bunch_im = maskImage(bunch_im, kong_index, 0, True, mode) writeColorImageToROM(bunch_im, 7, bunch_start[kong_index] + (file - 274), 44, 44, False, TextureFormat.RGBA5551) for file in range(5819, 5827): # Balloon balloon_im = getImageFile(25, BALLOON_START[kong_index] + (file - 5819), True, 32, 64, TextureFormat.RGBA5551) - balloon_im = maskImage(balloon_im, kong_index, 33) + balloon_im = maskImage(balloon_im, kong_index, 33, False, mode) balloon_im.paste(dk_single, balloon_single_frames[file - 5819], dk_single) writeColorImageToROM(balloon_im, 25, BALLOON_START[kong_index] + (file - 5819), 32, 64, False, TextureFormat.RGBA5551) else: @@ -2244,14 +909,6 @@ def overwrite_object_colors(settings, ROM_COPY: ROM): ORANGE_SCALING = 0.7 -kong_index_mapping = { - # Regular model, instrument model - Kongs.donkey: (3, None), - Kongs.diddy: (0, 1), - Kongs.lanky: (5, 6), - Kongs.tiny: (8, 9), - Kongs.chunky: (11, 12), -} model_index_mapping = { # Regular model, instrument model KongModels.krusha: (0xDA, 0xDA), @@ -2346,110 +1003,6 @@ def applyKongModelSwaps(settings: Settings) -> None: base_im.paste(orange_im, (dim_offset, dim_offset), orange_im) writeColorImageToROM(base_im, 25, switch_faces[index], 32, 32, False, TextureFormat.RGBA5551) - -DK_SCALE = 0.75 -GENERIC_SCALE = 0.49 -krusha_scaling = [ - # [x, y, z, xz, y] - # DK - [ - lambda x: x * DK_SCALE, - lambda x: x * DK_SCALE, - lambda x: x * GENERIC_SCALE, - lambda x: x * DK_SCALE, - lambda x: x * DK_SCALE, - ], - # Diddy - [ - lambda x: (x * 1.043) - 41.146, - lambda x: (x * 9.893) - 8.0, - lambda x: x * GENERIC_SCALE, - lambda x: (x * 1.103) - 14.759, - lambda x: (x * 0.823) + 35.220, - ], - # Lanky - [ - lambda x: (x * 0.841) - 17.231, - lambda x: (x * 6.925) - 2.0, - lambda x: x * GENERIC_SCALE, - lambda x: (x * 0.680) - 18.412, - lambda x: (x * 0.789) + 42.138, - ], - # Tiny - [ - lambda x: (x * 0.632) + 7.590, - lambda x: (x * 6.925) + 0.0, - lambda x: x * GENERIC_SCALE, - lambda x: (x * 1.567) - 21.676, - lambda x: (x * 0.792) + 41.509, - ], - # Chunky - [lambda x: x, lambda x: x, lambda x: x, lambda x: x, lambda x: x], -] - - -def readListAsInt(arr: list, start: int, size: int) -> int: - """Read list and convert to int.""" - val = 0 - for i in range(size): - val = (val * 256) + arr[start + i] - return val - - -def fixModelSmallKongCollision(kong_index: int): - """Modify Krusha Model to be smaller to enable him to fit through smaller gaps.""" - for x in range(2): - file = kong_index_mapping[kong_index][x] - if file is None: - continue - krusha_model_start = js.pointer_addresses[5]["entries"][file]["pointing_to"] - krusha_model_finish = js.pointer_addresses[5]["entries"][file + 1]["pointing_to"] - krusha_model_size = krusha_model_finish - krusha_model_start - ROM_COPY = LocalROM() - ROM_COPY.seek(krusha_model_start) - indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") - ROM_COPY.seek(krusha_model_start) - data = ROM_COPY.readBytes(krusha_model_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) - num_data = [] # data, but represented as nums rather than b strings - for d in data: - num_data.append(d) - head = readListAsInt(num_data, 0, 4) - ptr = readListAsInt(num_data, 0xC, 4) - base = (ptr - head) + 0x28 + 8 - count_0 = readListAsInt(num_data, base, 4) - changes = krusha_scaling[kong_index][:3] - changes_0 = [ - krusha_scaling[kong_index][3], - krusha_scaling[kong_index][4], - krusha_scaling[kong_index][3], - ] - for i in range(count_0): - i_start = base + 4 + (i * 0x14) - for coord_index, change in enumerate(changes): - val_i = readListAsInt(num_data, i_start + (4 * coord_index) + 4, 4) - val_f = change(intf_to_float(val_i)) - val_i = int(float_to_hex(val_f), 16) - for di, d in enumerate(int_to_list(val_i, 4)): - num_data[i_start + (4 * coord_index) + 4 + di] = d - section_2_start = base + 4 + (count_0 * 0x14) - count_1 = readListAsInt(num_data, section_2_start, 4) - for i in range(count_1): - i_start = section_2_start + 4 + (i * 0x10) - for coord_index, change in enumerate(changes_0): - val_i = readListAsInt(num_data, i_start + (4 * coord_index), 4) - val_f = change(intf_to_float(val_i)) - val_i = int(float_to_hex(val_f), 16) - for di, d in enumerate(int_to_list(val_i, 4)): - num_data[i_start + (4 * coord_index) + di] = d - data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - LocalROM().seek(krusha_model_start) - LocalROM().writeBytes(data) - - def changeModelTextures(settings: Settings, kong_index: int): """Change the textures associated with a model.""" settings_values = [ @@ -2494,36 +1047,6 @@ def changeModelTextures(settings: Settings, kong_index: int): LocalROM().seek(krusha_model_start) LocalROM().writeBytes(data) - -def fixBaboonBlasts(): - """Fix various baboon blasts to work for Krusha.""" - # Fungi Baboon Blast - ROM_COPY = LocalROM() - for id in (2, 5): - item_start = getObjectAddress(0xBC, id, "actor") - if item_start is not None: - ROM_COPY.seek(item_start + 0x14) - ROM_COPY.writeMultipleBytes(0xFFFFFFEC, 4) - ROM_COPY.seek(item_start + 0x1B) - ROM_COPY.writeMultipleBytes(0, 1) - # Caves Baboon Blast - item_start = getObjectAddress(0xBA, 4, "actor") - if item_start is not None: - ROM_COPY.seek(item_start + 0x4) - ROM_COPY.writeMultipleBytes(int(float_to_hex(510), 16), 4) - item_start = getObjectAddress(0xBA, 12, "actor") - if item_start is not None: - ROM_COPY.seek(item_start + 0x4) - ROM_COPY.writeMultipleBytes(int(float_to_hex(333), 16), 4) - # Castle Baboon Blast - item_start = getObjectAddress(0xBB, 4, "actor") - if item_start is not None: - ROM_COPY.seek(item_start + 0x0) - ROM_COPY.writeMultipleBytes(int(float_to_hex(2472), 16), 4) - ROM_COPY.seek(item_start + 0x8) - ROM_COPY.writeMultipleBytes(int(float_to_hex(1980), 16), 4) - - def darkenDPad(): """Change the DPad cross texture for the DPad HUD.""" img = getImageFile(14, 187, True, 32, 32, TextureFormat.RGBA5551) @@ -2551,93 +1074,6 @@ def darkenDPad(): ROM().seek(js.pointer_addresses[14]["entries"][187]["pointing_to"]) ROM().writeBytes(px_data) - -def placeKrushaHead(settings: Settings, slot): - """Replace a kong's face with the Krusha face.""" - if settings.colorblind_mode != ColorblindMode.off: - return - - kong_face_textures = [[0x27C, 0x27B], [0x279, 0x27A], [0x277, 0x278], [0x276, 0x275], [0x273, 0x274]] - unc_face_textures = [[579, 586], [580, 587], [581, 588], [582, 589], [577, 578]] - krushaFace64 = getImageFile(TableNames.TexturesGeometry, getBonusSkinOffset(ExtraTextures.KrushaFace1 + slot), True, 64, 64, TextureFormat.RGBA5551) - krushaFace64Left = krushaFace64.crop([0, 0, 32, 64]) - krushaFace64Right = krushaFace64.crop([32, 0, 64, 64]) - # Used in File Select, Pause Menu, Tag Barrels, Switches, Transformation Barrels - writeColorImageToROM(krushaFace64Left, 25, kong_face_textures[slot][0], 32, 64, False, TextureFormat.RGBA5551) - writeColorImageToROM(krushaFace64Right, 25, kong_face_textures[slot][1], 32, 64, False, TextureFormat.RGBA5551) - # Used in Troff and Scoff - writeColorImageToROM(krushaFace64Left, 7, unc_face_textures[slot][0], 32, 64, False, TextureFormat.RGBA5551) - writeColorImageToROM(krushaFace64Right, 7, unc_face_textures[slot][1], 32, 64, False, TextureFormat.RGBA5551) - - krushaFace32 = krushaFace64.resize((32, 32)) - krushaFace32 = krushaFace32.transpose(Image.Transpose.FLIP_TOP_BOTTOM) - krushaFace32RBGA32 = getImageFile(TableNames.TexturesGeometry, getBonusSkinOffset(ExtraTextures.KrushaFace321 + slot), True, 32, 32, TextureFormat.RGBA32) - # Used in the DPad Selection Menu - writeColorImageToROM(krushaFace32, 14, 190 + slot, 32, 32, False, TextureFormat.RGBA5551) - # Used in Shops Previews - writeColorImageToROM(krushaFace32RBGA32, 14, 197 + slot, 32, 32, False, TextureFormat.RGBA32) - - """kong_face_textures = [[0x27C, 0x27B], [0x279, 0x27A], [0x277, 0x278], [0x276, 0x275], [0x273, 0x274]] - unc_face_textures = [[579, 586], [580, 587], [581, 588], [582, 589], [577, 578]] - ROM_COPY = LocalROM() - ROM_COPY.seek(0x1FF6000) - left = [] - right = [] - img32 = [] - img32_rgba32 = [] - y32 = [] - y32_rgba32 = [] - for y in range(64): - x32 = [] - x32_rgba32 = [] - for x in range(64): - data_hi = int.from_bytes(ROM_COPY.readBytes(1), "big") - data_lo = int.from_bytes(ROM_COPY.readBytes(1), "big") - val = (data_hi << 8) | data_lo - val_r = ((val >> 11) & 0x1F) << 3 - val_g = ((val >> 6) & 0x1F) << 3 - val_b = ((val >> 1) & 0x1F) << 3 - val_a = 0 - if val & 1: - val_a = 255 - data_rgba32 = [val_r, val_g, val_b, val_a] - if x < 32: - right.extend([data_hi, data_lo]) - else: - left.extend([data_hi, data_lo]) - if ((x % 2) + (y % 2)) == 0: - x32.extend([data_hi, data_lo]) - x32_rgba32.extend(data_rgba32) - if len(x32) > 0 and len(x32_rgba32): - y32.append(x32) - y32_rgba32.append(x32_rgba32) - y32.reverse() - for y in y32: - img32.extend(y) - y32_rgba32.reverse() - for y in y32_rgba32: - img32_rgba32.extend(y) - for x in range(2): - img_data = [right, left][x] - texture_index = kong_face_textures[slot][x] - unc_index = unc_face_textures[slot][x] - texture_addr = js.pointer_addresses[25]["entries"][texture_index]["pointing_to"] - unc_addr = js.pointer_addresses[7]["entries"][unc_index]["pointing_to"] - data = gzip.compress(bytearray(img_data), compresslevel=9) - ROM_COPY.seek(texture_addr) - ROM_COPY.writeBytes(data) - ROM_COPY.seek(unc_addr) - ROM_COPY.writeBytes(bytearray(img_data)) - rgba32_addr32 = js.pointer_addresses[14]["entries"][197 + slot]["pointing_to"] - rgba16_addr32 = js.pointer_addresses[14]["entries"][190 + slot]["pointing_to"] - data32 = gzip.compress(bytearray(img32), compresslevel=9) - data32_rgba32 = gzip.compress(bytearray(img32_rgba32), compresslevel=9) - ROM_COPY.seek(rgba32_addr32) - ROM_COPY.writeBytes(bytearray(data32_rgba32)) - ROM_COPY.seek(rgba16_addr32) - ROM_COPY.writeBytes(bytearray(data32))""" - - def getValueFromByteArray(ba: bytearray, offset: int, size: int) -> int: """Get value from byte array given an offset and size.""" value = 0 @@ -2648,31 +1084,6 @@ def getValueFromByteArray(ba: bytearray, offset: int, size: int) -> int: return value -def hueShiftImageContainer(table: int, image: int, width: int, height: int, format: TextureFormat, shift: int): - """Load an image, shift the hue and rewrite it back to ROM.""" - loaded_im = getImageFile(table, image, table != 7, width, height, format) - loaded_im = hueShift(loaded_im, shift) - loaded_px = loaded_im.load() - bytes_array = [] - for y in range(height): - for x in range(width): - pix_data = list(loaded_px[x, y]) - if format == TextureFormat.RGBA32: - bytes_array.extend(pix_data) - elif format == TextureFormat.RGBA5551: - red = int((pix_data[0] >> 3) << 11) - green = int((pix_data[1] >> 3) << 6) - blue = int((pix_data[2] >> 3) << 1) - alpha = int(pix_data[3] != 0) - value = red | green | blue | alpha - bytes_array.extend([(value >> 8) & 0xFF, value & 0xFF]) - px_data = bytearray(bytes_array) - if table != 7: - px_data = gzip.compress(px_data, compresslevel=9) - ROM().seek(js.pointer_addresses[table]["entries"][image]["pointing_to"]) - ROM().writeBytes(px_data) - - def getEnemySwapColor(channel_min: int = 0, channel_max: int = 255, min_channel_variance: int = 0) -> int: """Get an RGB color compatible with enemy swaps.""" channels = [] @@ -3373,194 +1784,6 @@ def writeMiscCosmeticChanges(settings): ROM().seek(js.pointer_addresses[5]["entries"][enemy]["pointing_to"]) ROM().writeBytes(file_data) - -def getNumberImage(number: int) -> PIL.Image.Image: - """Get Number Image from number.""" - if number < 5: - num_0_bounds = [0, 20, 30, 45, 58, 76] - x = number - return getImageFile(14, 15, True, 76, 24, TextureFormat.RGBA5551).crop((num_0_bounds[x], 0, num_0_bounds[x + 1], 24)) - num_1_bounds = [0, 15, 28, 43, 58, 76] - x = number - 5 - return getImageFile(14, 16, True, 76, 24, TextureFormat.RGBA5551).crop((num_1_bounds[x], 0, num_1_bounds[x + 1], 24)) - - -def numberToImage(number: int, dim: Tuple[int, int]) -> PIL.Image.Image: - """Convert multi-digit number to image.""" - digits = 1 - if number < 10: - digits = 1 - elif number < 100: - digits = 2 - else: - digits = 3 - current = number - nums = [] - total_width = 0 - max_height = 0 - sep_dist = 1 - for _ in range(digits): - base = getNumberImage(current % 10) - bbox = base.getbbox() - base = base.crop(bbox) - nums.append(base) - base_w, base_h = base.size - max_height = max(max_height, base_h) - total_width += base_w - current = int(current / 10) - nums.reverse() - total_width += (digits - 1) * sep_dist - base = Image.new(mode="RGBA", size=(total_width, max_height)) - pos = 0 - for num in nums: - base.paste(num, (pos, 0), num) - num_w, num_h = num.size - pos += num_w + sep_dist - output = Image.new(mode="RGBA", size=dim) - xScale = dim[0] / total_width - yScale = dim[1] / max_height - scale = xScale - if yScale < xScale: - scale = yScale - new_w = int(total_width * scale) - new_h = int(max_height * scale) - x_offset = int((dim[0] - new_w) / 2) - y_offset = int((dim[1] - new_h) / 2) - new_dim = (new_w, new_h) - base = base.resize(new_dim) - output.paste(base, (x_offset, y_offset), base) - return output - - -def recolorKRoolShipSwitch(color: tuple, ROM_COPY: ROM): - """Recolors the simian slam switch that is part of K. Rool's ship in galleon.""" - addresses = ( - 0x4C34, - 0x4C44, - 0x4C54, - 0x4C64, - 0x4C74, - 0x4C84, - ) - data = bytearray(getRawFile(TableNames.ModelTwoGeometry, 305, True)) - for addr in addresses: - for x in range(3): - data[addr + x] = color[x] - new_tex = [ - 0xE7, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0xE2, - 0x00, - 0x00, - 0x1C, - 0x0C, - 0x19, - 0x20, - 0x38, - 0xE3, - 0x00, - 0x0A, - 0x01, - 0x00, - 0x10, - 0x00, - 0x00, - 0xE3, - 0x00, - 0x0F, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0xE7, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0xFC, - 0x12, - 0x7E, - 0x03, - 0xFF, - 0xFF, - 0xF9, - 0xF8, - 0xFD, - 0x90, - 0x00, - 0x00, - 0x00, - 0x00, - 0x0B, - 0xAF, - 0xF5, - 0x90, - 0x00, - 0x00, - 0x07, - 0x08, - 0x02, - 0x00, - 0xE6, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0xF3, - 0x00, - 0x00, - 0x00, - 0x07, - 0x7F, - 0xF1, - 0x00, - 0xE7, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0xF5, - 0x88, - 0x10, - 0x00, - 0x00, - 0x08, - 0x02, - 0x00, - 0xF2, - 0x00, - 0x00, - 0x00, - 0x00, - 0x0F, - 0xC0, - 0xFC, - ] - for x in range(8): - data[0x1AD8 + x] = 0 - for xi, x in enumerate(new_tex): - data[0x1AE8 + xi] = x - for x in range(40): - data[0x1B58 + x] = 0 - writeRawFile(TableNames.ModelTwoGeometry, 305, True, data, ROM_COPY) - - def applyHelmDoorCosmetics(settings: Settings) -> None: """Apply Helm Door Cosmetic Changes.""" crown_door_required_item = settings.crown_door_item @@ -3635,344 +1858,6 @@ def applyHelmDoorCosmetics(settings: Settings) -> None: TextureFormat.RGBA5551, ) - -def changeBarrelColor(barrel_color: tuple = None, metal_color: tuple = None, brighten_barrel: bool = False): - """Change the colors of the various barrels.""" - wood_img = getImageFile(25, getBonusSkinOffset(ExtraTextures.ShellWood), True, 32, 64, TextureFormat.RGBA5551) - metal_img = getImageFile(25, getBonusSkinOffset(ExtraTextures.ShellMetal), True, 32, 64, TextureFormat.RGBA5551) - qmark_img = getImageFile(25, getBonusSkinOffset(ExtraTextures.ShellQMark), True, 32, 64, TextureFormat.RGBA5551) - if barrel_color is not None: - if brighten_barrel: - enhancer = ImageEnhance.Brightness(wood_img) - wood_img = enhancer.enhance(2) - wood_img = maskImageWithColor(wood_img, barrel_color) - if metal_color is not None: - metal_img = maskImageWithColor(metal_img, metal_color) - wood_img.paste(metal_img, (0, 0), metal_img) - writeColorImageToROM(wood_img, 25, getBonusSkinOffset(ExtraTextures.BonusShell), 32, 64, False, TextureFormat.RGBA5551) # Bonus Barrel - tag_img = Image.new(mode="RGBA", size=(32, 64)) - tag_img.paste(wood_img, (0, 0), wood_img) - tag_img.paste(qmark_img, (0, 0), qmark_img) - writeColorImageToROM(tag_img, 25, 4938, 32, 64, False, TextureFormat.RGBA5551) # Tag Barrel - # Compose Transform Barrels - kongs = [ - {"face_left": 0x27C, "face_right": 0x27B, "barrel_tex_start": 4817, "targ_width": 24}, # DK - {"face_left": 0x279, "face_right": 0x27A, "barrel_tex_start": 4815, "targ_width": 24}, # Diddy - {"face_left": 0x277, "face_right": 0x278, "barrel_tex_start": 4819, "targ_width": 24}, # Lanky - {"face_left": 0x276, "face_right": 0x275, "barrel_tex_start": 4769, "targ_width": 24}, # Tiny - {"face_left": 0x273, "face_right": 0x274, "barrel_tex_start": 4747, "targ_width": 24}, # Chunky - ] - for kong in kongs: - bar_left = Image.new(mode="RGBA", size=(32, 64)) - bar_right = Image.new(mode="RGBA", size=(32, 64)) - face_left = getImageFile(25, kong["face_left"], True, 32, 64, TextureFormat.RGBA5551) - face_right = getImageFile(25, kong["face_right"], True, 32, 64, TextureFormat.RGBA5551) - width = kong["targ_width"] - height = width * 2 - face_left = face_left.resize((width, height)) - face_right = face_right.resize((width, height)) - right_w_offset = 32 - width - top_h_offset = (64 - height) >> 1 - bar_left.paste(wood_img, (0, 0), wood_img) - bar_right.paste(wood_img, (0, 0), wood_img) - bar_left.paste(face_left, (right_w_offset, top_h_offset), face_left) - bar_right.paste(face_right, (0, top_h_offset), face_right) - writeColorImageToROM(bar_left, 25, kong["barrel_tex_start"], 32, 64, False, TextureFormat.RGBA5551) - writeColorImageToROM(bar_right, 25, kong["barrel_tex_start"] + 1, 32, 64, False, TextureFormat.RGBA5551) - # Cannons - barrel_left = Image.new(mode="RGBA", size=(32, 64)) - barrel_right = Image.new(mode="RGBA", size=(32, 64)) - barrel_left.paste(wood_img, (0, 0), wood_img) - barrel_right.paste(wood_img, (0, 0), wood_img) - barrel_left = barrel_left.crop((0, 0, 16, 64)) - barrel_right = barrel_right.crop((16, 0, 32, 64)) - writeColorImageToROM(barrel_left, 25, 0x12B3, 16, 64, False, TextureFormat.RGBA5551) - writeColorImageToROM(barrel_right, 25, 0x12B4, 16, 64, False, TextureFormat.RGBA5551) - if barrel_color is not None: - tex_data = { - getBonusSkinOffset(ExtraTextures.RocketTop): (1, 1372), - 0x12B5: (48, 32), - 0x12B8: (44, 44), - } - for img in tex_data: - dim_x = tex_data[img][0] - dim_y = tex_data[img][1] - img_output = getImageFile(25, img, True, dim_x, dim_y, TextureFormat.RGBA5551) - img_output = maskImageWithColor(img_output, barrel_color) - writeColorImageToROM(img_output, 25, img, dim_x, dim_y, False, TextureFormat.RGBA5551) - - -def applyCelebrationRims(hue_shift: int, enabled_bananas: list[bool] = [False, False, False, False, False]): - """Retexture the warp pad rims to have a more celebratory tone.""" - banana_textures = [] - vanilla_banana_textures = [0xA8, 0x98, 0xE8, 0xD0, 0xF0] - for kong_index, ban in enumerate(enabled_bananas): - if ban: - banana_textures.append(vanilla_banana_textures[kong_index]) - place_bananas = False - if len(banana_textures) > 0: - place_bananas = True - if len(banana_textures) < 4: - banana_textures = (banana_textures * 4)[:4] - if place_bananas: - bananas = [getImageFile(7, x, False, 44, 44, TextureFormat.RGBA5551).resize((14, 14)) for x in banana_textures] - banana_placement = [ - # File, x, y - [0xBB3, 15, 1], # 3 - [0xBB2, 2, 1], # 2 - [0xBB3, 0, 1], # 4 - [0xBB2, 17, 1], # 1 - ] - for img in (0xBB2, 0xBB3): - side_im = getImageFile(25, img, True, 32, 16, TextureFormat.RGBA5551) - hueShift(side_im, hue_shift) - if place_bananas: - for bi, banana in enumerate(bananas): - if banana_placement[bi][0] == img: - b_x = banana_placement[bi][1] - b_y = banana_placement[bi][2] - side_im.paste(banana, (b_x, b_y), banana) - side_by = [] - side_px = side_im.load() - for y in range(16): - for x in range(32): - red_short = (side_px[x, y][0] >> 3) & 31 - green_short = (side_px[x, y][1] >> 3) & 31 - blue_short = (side_px[x, y][2] >> 3) & 31 - alpha_short = 1 if side_px[x, y][3] > 128 else 0 - value = (red_short << 11) | (green_short << 6) | (blue_short << 1) | alpha_short - side_by.extend([(value >> 8) & 0xFF, value & 0xFF]) - px_data = bytearray(side_by) - px_data = gzip.compress(px_data, compresslevel=9) - ROM().seek(js.pointer_addresses[25]["entries"][img]["pointing_to"]) - ROM().writeBytes(px_data) - - -def applyHolidayMode(settings): - """Change grass texture to snow.""" - HOLIDAY = getHoliday(settings) - if HOLIDAY == Holidays.no_holiday: - changeBarrelColor() # Fixes some Krusha stuff - return - if HOLIDAY == Holidays.Christmas: - # Set season to Christmas - ROM().seek(settings.rom_data + 0xDB) - ROM().writeMultipleBytes(2, 1) - # Grab Snow texture, transplant it - ROM().seek(0x1FF8000) - snow_im = Image.new(mode="RGBA", size=((32, 32))) - snow_px = snow_im.load() - snow_by = [] - for y in range(32): - for x in range(32): - rgba_px = int.from_bytes(ROM().readBytes(2), "big") - red = ((rgba_px >> 11) & 31) << 3 - green = ((rgba_px >> 6) & 31) << 3 - blue = ((rgba_px >> 1) & 31) << 3 - alpha = (rgba_px & 1) * 255 - snow_px[x, y] = (red, green, blue, alpha) - for dim in (32, 16, 8, 4): - snow_im = snow_im.resize((dim, dim)) - px = snow_im.load() - for y in range(dim): - for x in range(dim): - rgba_data = list(px[x, y]) - data = 0 - for c in range(3): - data |= (rgba_data[c] >> 3) << (1 + (5 * c)) - if rgba_data[3] != 0: - data |= 1 - snow_by.extend([(data >> 8), (data & 0xFF)]) - byte_data = gzip.compress(bytearray(snow_by), compresslevel=9) - for img in (0x4DD, 0x4E4, 0x6B, 0xF0, 0x8B2, 0x5C2, 0x66E, 0x66F, 0x685, 0x6A1, 0xF8, 0x136): - start = js.pointer_addresses[25]["entries"][img]["pointing_to"] - ROM().seek(start) - ROM().writeBytes(byte_data) - # Alter CI4 Palettes - start = js.pointer_addresses[25]["entries"][2007]["pointing_to"] - mags = [140, 181, 156, 181, 222, 206, 173, 230, 255, 255, 255, 189, 206, 255, 181, 255] - new_ci4_palette = [] - for mag in mags: - comp_mag = mag >> 3 - data = (comp_mag << 11) | (comp_mag << 6) | (comp_mag << 1) | 1 - new_ci4_palette.extend([(data >> 8), (data & 0xFF)]) - byte_data = gzip.compress(bytearray(new_ci4_palette), compresslevel=9) - ROM().seek(start) - ROM().writeBytes(byte_data) - # Alter rims - applyCelebrationRims(50, [True, True, True, True, False]) - # Change DK's Tie and Tiny's Hair - if settings.dk_tie_colors != CharacterColors.custom and settings.kong_model_dk == KongModels.default: - tie_hang = [0xFF] * 0xAB8 - tie_hang_data = gzip.compress(bytearray(tie_hang), compresslevel=9) - ROM().seek(js.pointer_addresses[25]["entries"][0xE8D]["pointing_to"]) - ROM().writeBytes(tie_hang_data) - tie_loop = [0xFF] * (32 * 32 * 2) - tie_loop_data = gzip.compress(bytearray(tie_loop), compresslevel=9) - ROM().seek(js.pointer_addresses[25]["entries"][0x177D]["pointing_to"]) - ROM().writeBytes(tie_loop_data) - if settings.tiny_hair_colors != CharacterColors.custom and settings.kong_model_tiny == KongModels.default: - tiny_hair = [] - for x in range(32 * 32): - tiny_hair.extend([0xF8, 0x01]) - tiny_hair_data = gzip.compress(bytearray(tiny_hair), compresslevel=9) - ROM().seek(js.pointer_addresses[25]["entries"][0xE68]["pointing_to"]) - ROM().writeBytes(tiny_hair_data) - # Tag Barrel, Bonus Barrel & Transform Barrels - changeBarrelColor(None, (0x00, 0xC0, 0x00)) - elif HOLIDAY == Holidays.Halloween: - ROM().seek(settings.rom_data + 0xDB) - ROM().writeMultipleBytes(1, 1) - # Pad Rim - applyCelebrationRims(-12) - # Tag Barrel, Bonus Barrel & Transform Barrels - changeBarrelColor((0x00, 0xC0, 0x00)) - # Turn Ice Tomato Orange - sizes = { - 0x1237: 700, - 0x1238: 1404, - 0x1239: 1372, - 0x123A: 1372, - 0x123B: 692, - 0x123C: 1372, - 0x123D: 1372, - 0x123E: 1372, - 0x123F: 1372, - 0x1240: 1372, - 0x1241: 1404, - } - for img in range(0x1237, 0x1241 + 1): - hueShiftImageContainer(25, img, 1, sizes[img], TextureFormat.RGBA5551, 240) - elif HOLIDAY == Holidays.Anniv25: - changeBarrelColor((0xFF, 0xFF, 0x00), None, True) - sticker_im = getImageFile(25, getBonusSkinOffset(ExtraTextures.Anniv25Sticker), True, 1, 1372, TextureFormat.RGBA5551) - vanilla_sticker_im = getImageFile(25, 0xB7D, True, 1, 1372, TextureFormat.RGBA5551) - sticker_im_snipped = sticker_im.crop((0, 0, 1, 1360)) - writeColorImageToROM(sticker_im_snipped, 25, 0xB7D, 1, 1360, False, TextureFormat.RGBA5551) - vanilla_sticker_portion = vanilla_sticker_im.crop((0, 1360, 1, 1372)) - new_im = Image.new(mode="RGBA", size=(1, 1372)) - new_im.paste(sticker_im_snipped, (0, 0), sticker_im_snipped) - new_im.paste(vanilla_sticker_portion, (0, 1360), vanilla_sticker_portion) - writeColorImageToROM(new_im, 25, 0x1266, 1, 1372, False, TextureFormat.RGBA5551) - applyCelebrationRims(0, [False, True, True, True, True]) - - -def updateMillLeverTexture(settings: Settings) -> None: - """Update the 21132 texture.""" - if settings.mill_levers[0] > 0: - # Get Number bounds - base_num_texture = getImageFile(table_index=25, file_index=0x7CA, compressed=True, width=64, height=32, format=TextureFormat.RGBA5551) - number_textures = [None, None, None] - number_x_bounds = ( - (18, 25), - (5, 16), - (36, 47), - ) - modified_tex = getImageFile(table_index=25, file_index=0x7CA, compressed=True, width=64, height=32, format=TextureFormat.RGBA5551) - for tex in range(3): - number_textures[tex] = base_num_texture.crop((number_x_bounds[tex][0], 7, number_x_bounds[tex][1], 25)) - total_width = 0 - for x in range(5): - if settings.mill_levers[x] > 0: - idx = settings.mill_levers[x] - 1 - total_width += number_x_bounds[idx][1] - number_x_bounds[idx][0] - # Overwrite old panel - overwrite_panel = Image.new(mode="RGBA", size=(58, 26), color=(131, 65, 24)) - modified_tex.paste(overwrite_panel, (3, 3), overwrite_panel) - # Generate new number texture - new_num_texture = Image.new(mode="RGBA", size=(total_width, 18)) - x_pos = 0 - for num in range(5): - if settings.mill_levers[num] > 0: - num_val = settings.mill_levers[num] - 1 - new_num_texture.paste(number_textures[num_val], (x_pos, 0), number_textures[num_val]) - x_pos += number_x_bounds[num_val][1] - number_x_bounds[num_val][0] - scale_x = 58 / total_width - scale_y = 26 / 18 - scale = min(scale_x, scale_y) - x_size = int(total_width * scale) - y_size = int(18 * scale) - new_num_texture = new_num_texture.resize((x_size, y_size)) - x_offset = int((58 - x_size) / 2) - modified_tex.paste(new_num_texture, (3 + x_offset, 3), new_num_texture) - writeColorImageToROM(modified_tex, 25, 0x7CA, 64, 32, False, TextureFormat.RGBA5551) - - -def updateDiddyDoors(settings: Settings): - """Update the textures for the doors.""" - enable_code = False - for code in settings.diddy_rnd_doors: - if sum(code) > 0: # Has a non-zero element - enable_code = True - SEG_WIDTH = 48 - SEG_HEIGHT = 42 - NUMBERS_START = (27, 33) - if enable_code: - # Order: 4231, 3124, 1342 - starts = (0xCE8, 0xCE4, 0xCE0) - for index, code in enumerate(settings.diddy_rnd_doors): - start = starts[index] - total = Image.new(mode="RGBA", size=(SEG_WIDTH * 2, SEG_HEIGHT * 2)) - for img_index in range(4): - img = getImageFile(25, start + img_index, True, SEG_WIDTH, SEG_HEIGHT, TextureFormat.RGBA5551) - x_offset = SEG_WIDTH * (img_index & 1) - y_offset = SEG_HEIGHT * ((img_index & 2) >> 1) - total.paste(img, (x_offset, y_offset), img) - total = total.transpose(Image.FLIP_TOP_BOTTOM) - # Overlay color - cover = Image.new(mode="RGBA", size=(42, 20), color=(115, 98, 65)) - total.paste(cover, NUMBERS_START, cover) - # Paste numbers - number_images = [] - number_offsets = [] - total_length = 0 - for num in code: - num_img = getNumberImage(num + 1) - w, h = num_img.size - number_offsets.append(total_length) - total_length += w - number_images.append(num_img) - total_numbers = Image.new(mode="RGBA", size=(total_length, 24)) - for img_index, img in enumerate(number_images): - total_numbers.paste(img, (number_offsets[img_index], 0), img) - total.paste(total_numbers, (SEG_WIDTH - int(total_length / 2), SEG_HEIGHT - 12), total_numbers) - total = total.transpose(Image.FLIP_TOP_BOTTOM) - for img_index in range(4): - x_offset = SEG_WIDTH * (img_index & 1) - y_offset = SEG_HEIGHT * ((img_index & 2) >> 1) - sub_img = total.crop((x_offset, y_offset, x_offset + SEG_WIDTH, y_offset + SEG_HEIGHT)) - writeColorImageToROM(sub_img, 25, start + img_index, SEG_WIDTH, SEG_HEIGHT, False, TextureFormat.RGBA5551) - - -def updateCryptLeverTexture(settings: Settings) -> None: - """Update the two textures for Donkey Minecart entry.""" - if settings.crypt_levers[0] > 0: - # Get a blank texture - texture_0 = getImageFile(table_index=25, file_index=0x999, compressed=True, width=32, height=64, format=TextureFormat.RGBA5551) - blank = texture_0.crop((8, 5, 23, 22)) - texture_0.paste(blank, (8, 42), blank) - texture_1 = texture_0.copy() - for xi, x in enumerate(settings.crypt_levers): - corrected = x - 1 - y_slot = corrected % 3 - num = getNumberImage(xi + 1) - num = num.transpose(Image.FLIP_TOP_BOTTOM) - w, h = num.size - scale = 2 / 3 - y_offset = int((h * scale) / 2) - x_offset = int((w * scale) / 2) - num = num.resize((int(w * scale), int(h * scale))) - y_pos = (51, 33, 14) - tl_y = y_pos[y_slot] - y_offset - tl_x = 16 - x_offset - if corrected < 3: - texture_0.paste(num, (tl_x, tl_y), num) - else: - texture_1.paste(num, (tl_x, tl_y), num) - writeColorImageToROM(texture_0, 25, 0x99A, 32, 64, False, TextureFormat.RGBA5551) - writeColorImageToROM(texture_1, 25, 0x999, 32, 64, False, TextureFormat.RGBA5551) - - def darkenPauseBubble(settings: Settings): """Change the brightness of the text bubble used for the pause menu for dark mode.""" if not settings.dark_mode_textboxes: @@ -4072,516 +1957,6 @@ def showWinCondition(settings: Settings): base_im.paste(num_im, (6, 6), num_im) writeColorImageToROM(base_im, 14, 195, 32, 32, False, TextureFormat.RGBA5551) - -boot_phrases = ( - "Removing Lanky Kong", - "Telling 2dos to play DK64", - "Locking K. Lumsy in a cage", - "Stealing the Banana Hoard", - "Finishing the game in a cave", - "Becoming the peak of randomizers", - "Giving kops better eyesight", - "Patching in the glitches", - "Enhancing Cfox Luck", - "Finding Rareware GB in Galleon", - "Resurrecting Chunky Kong", - "Shouting out Grant Kirkhope", - "Crediting L. Godfrey", - "Removing Stop n Swop", - "Assembling the scraps", - "Blowing in the cartridge", - "Backflipping in Chunky Phase", - "Hiding 20 fairies", - "Randomizing collision normals", - "Removing hit detection", - "Compressing K Rools Voice Lines", - "Checking divide by 0 doesnt work", - "Adding every move to Isles", - "Segueing in dk64randomizer.com", - "Removing lag. Or am I?", - "Hiding a dirt patch under grass", - "Giving Wrinkly the spoiler log", - "Questioning sub 2:30 in LUA Rando", - "Chasing Lanky in Fungi Forest", - "Banning Potions from Candys Shop", - "Finding someone who can help you", - "Messing up your seed", - "Crashing Krem Isle", - "Increasing Robot Punch Resistance", - "Caffeinating banana fairies", - "Bothering Beavers", - "Inflating Banana Balloons", - "Counting to 16", - "Removing Walls", - "Taking it to the fridge", - "Brewing potions", - "Reticulating Splines", # SimCity 2000 - "Ironing Donks", - "Replacing mentions of Hero with Hoard", - "Suggesting you also try BK Randomizer", - "Scattering 3500 Bananas", - "Stealing ideas from other randomizers", - "Fixing Krushas Collision", - "Falling on 75m", - "Summoning Salt", - "Combing Chunkys Afro", - "Asking what you gonna do", - "Thinking with portals", - "Reminding you to hydrate", - "Injecting lag", - "Turning Sentient", - "Performing for you", - "Charging 2 coins per save", - "Loading in Beavers", - "Lifting Boulders with Relative Ease", - "Doing Monkey Science Probably", - "Telling Killi to eventually play DK64", - "Crediting Grant Kirkhope", - "Dropping Crayons", - "Saying Hello when others wont", - "Mangling Music", - "Killing Speedrunning", - "Enhancing Cfox Luck Voice Linesmizers", - "Enforcing the law of the Jungle", - "Saving 20 frames", - "Reporting bugs. Unlike some", - "Color-coding Krusha for convenience", -) - -crown_heads = ( - # Object - "Arena", - "Beaver", - "Bish Bash", - "Forest", - "Kamikaze", - "Kritter", - "Pinnacle", - "Plinth", - "Shockwave", - "Bean", - "Dogadon", - "Banana", - "Squawks", - "Lanky", - "Diddy", - "Tiny", - "Chunky", - "DK", - "Krusha", - "Kosha", - "Klaptrap", - "Zinger", - "Gnawty", - "Kasplat", - "Pufftup", - "Shuri", - "Krossbones", - "Caves", - "Castle", - "Helm", - "Japes", - "Jungle", - "Angry", - "Aztec", - "Frantic", - "Factory", - "Gloomy", - "Galleon", - "Crystal", - "Creepy", - "Hideout", - "Cranky", - "Funky", - "Candy", - "Kong", - "Monkey", - "Amazing", - "Incredible", - "Ultimate", - "Wrinkly", - "Heroic", - "Final", - "Fantastic", - "Krazy", - "Komplete", - "Unhinted", - "Unstable", - "Extreme", - "Royal", - "Monster", - "Primate", - "Baboon", - "Walnut", - "Peanut", - "Coconut", - "Feather", - "Grape", - "Pineapple", - "Barrel", - "Monkeyport", - "Kalamity", - "Kaboom", - "Magic", - "Fairy", - "Karnivorous", - "Krispy", - "Kooky", - "Cookin", - "Klutz", - "Kingdom", - "Super Duper", - "Rainbow", - "Bongo", - "Guitar", - "Trombone", - "Saxophone", - "Triangle", - "Dixie", - "Gorilla", - "Chimpy", - "Museum", - "Ballroom", - "Winch", - "Shipyard", - "Hillside", - "Oasis", - "Arcade", - "Mushroom", - "Igloo", - "Stupid", - "Spicy", - "Dizzy", - "Slot Car", - "Minecart", - "Rambi", - "Enguarde", - "Reptile", - "Bramble", - "Toxic", - "Rabbit", - "Beetle", - "Vulture", - "Boulder", -) - -crown_tails = ( - # Synonym for brawl/similar - "Ambush", - "Brawl", - "Fracas", - "Karnage", - "Kremlings", - "Palaver", - "Panic", - "Showdown", - "Slam", - "Melee", - "Tussle", - "Altercation", - "Wrangle", - "Clash", - "Free for All", - "Skirmish", - "Scrap", - "Fight", - "Rumpus", - "Fray", - "Wrestle", - "Brouhaha", - "Commotion", - "Uproar", - "Rough and Tumble", - "Broil", - "Argy Bargy", - "Bother", - "Mayhem", - "Bonanza", - "Battle", - "Kerfuffle", - "Rumble", - "Fisticuffs", - "Ruckus", - "Scrimmage", - "Strife", - "Dog and Duck", - "Joust", - "Scuffle", - "Hootenanny", - "Blitz", - "Tourney", - "Explosion", - "Contest", - "Chaos", - "Combat", - "Knockdown", - "Demolition", - "Capture", - "Storm", - "Earthquake", - "Charge", - "Tremor", - "Trample", - "Gauntlet", - "Challenge", - "Blowout", - "Riot", - "Buffoonery", - "Hijinxs", - "Frenzy", - "Rampage", - "Antics", - "Trouble", - "Revenge", - "Klamber", - "Wreckage", - "Quarrel", - "Feud", - "Thwack", - "Wallop", - "Donnybrook", - "Tangle", - "Crossfire", - "Royale", -) - - -def getCrownNames() -> list: - """Get crown names from head and tail pools.""" - # Get 10 names for heads just in case "Forest" and "Fracas" show up - heads = random.sample(crown_heads, 10) - tails = random.sample(crown_tails, 9) - # Remove "Forest" if both "Forest" and "Fracas" show up - if "Forest" in heads and "Fracas" in tails: - heads.remove("Forest") - # Only get 9 names, Forest Fracas can't be overwritten without having negative impacts - names = [] - for x in range(9): - head = heads[x] - tail = tails[x] - if head[0] == "K" and tail[0] == "C": - split_tail = list(tail) - split_tail[0] = "K" - tail = "".join(split_tail) - names.append(f"{head} {tail}!".upper()) - names.append("Forest Fracas!".upper()) - return names - - -def writeCrownNames(): - """Write Crown Names to ROM.""" - names = getCrownNames() - old_text = grabText(35, True) - for name_index, name in enumerate(names): - old_text[0x1E + name_index] = ({"text": [name]},) - writeText(35, old_text, True) - - -def writeBootMessages() -> None: - """Write boot messages into ROM.""" - ROM_COPY = LocalROM() - placed_messages = random.sample(boot_phrases, 4) - for message_index, message in enumerate(placed_messages): - ROM_COPY.seek(0x1FFD000 + (0x40 * message_index)) - ROM_COPY.writeBytes(message.upper().encode("ascii")) - - -def writeTransition(settings: Settings) -> None: - """Write transition cosmetic to ROM.""" - if js.cosmetics is None: - return - if js.cosmetics.transitions is None: - return - if js.cosmetic_names.transitions is None: - return - file_data = list(zip(js.cosmetics.transitions, js.cosmetic_names.transitions)) - settings.custom_transition = None - if len(file_data) == 0: - return - selected_transition = random.choice(file_data) - settings.custom_transition = selected_transition[1].split("/")[-1] # File Name - im_f = Image.open(BytesIO(bytes(selected_transition[0]))) - writeColorImageToROM(im_f, 14, 95, 64, 64, False, TextureFormat.IA4) - - -def getImageChunk(im_f, width: int, height: int): - """Get an image chunk based on a width and height.""" - width_height_ratio = width / height - im_w, im_h = im_f.size - im_wh_ratio = im_w / im_h - if im_wh_ratio != width_height_ratio: - # Ratio doesn't match, we have to do some rejigging - scale = 1 - if width_height_ratio > im_wh_ratio: - # Scale based on width - scale = width / im_w - else: - # Height needs growing - scale = height / im_h - im_f = im_f.resize((int(im_w * scale), int(im_h * scale))) - im_w, im_h = im_f.size - middle_w = im_w / 2 - middle_h = im_h / 2 - middle_targ_w = width / 2 - middle_targ_h = height / 2 - return im_f.crop( - ( - int(middle_w - middle_targ_w), - int(middle_h - middle_targ_h), - int(middle_w + middle_targ_w), - int(middle_h + middle_targ_h), - ) - ) - # Ratio matches, just scale up - return im_f.resize((width, height)) - - -def writeCustomPortal(settings: Settings) -> None: - """Write custom portal file to ROM.""" - if js.cosmetics is None: - return - if js.cosmetics.tns_portals is None: - return - if js.cosmetic_names.tns_portals is None: - return - file_data = list(zip(js.cosmetics.tns_portals, js.cosmetic_names.tns_portals)) - settings.custom_troff_portal = None - if len(file_data) == 0: - return - selected_portal = random.choice(file_data) - settings.custom_troff_portal = selected_portal[1].split("/")[-1] # File Name - im_f = Image.open(BytesIO(bytes(selected_portal[0]))) - im_f = getImageChunk(im_f, 63, 63) - im_f = im_f.transpose(Image.FLIP_TOP_BOTTOM).convert("RGBA") - portal_data = { - "NW": { - "x_min": 0, - "y_min": 0, - "writes": [0x39E, 0x39F], - }, - "SW": { - "x_min": 0, - "y_min": 31, - "writes": [0x3A0, 0x39D], - }, - "SE": { - "x_min": 31, - "y_min": 31, - "writes": [0x3A2, 0x39B], - }, - "NE": { - "x_min": 31, - "y_min": 0, - "writes": [0x39C, 0x3A1], - }, - } - for sub in portal_data.keys(): - x_min = portal_data[sub]["x_min"] - y_min = portal_data[sub]["y_min"] - local_img = im_f.crop((x_min, y_min, x_min + 32, y_min + 32)) - for idx in portal_data[sub]["writes"]: - writeColorImageToROM(local_img, 7, idx, 32, 32, False, TextureFormat.RGBA5551) - - -class PaintingData: - """Class to store information regarding a painting.""" - - def __init__(self, width: int, height: int, x_split: int, y_split: int, is_bordered: bool, texture_order: list, is_ci: bool = False): - """Initialize with given parameters.""" - self.width = width - self.height = height - self.x_split = x_split - self.y_split = y_split - self.is_bordered = is_bordered - self.texture_order = texture_order.copy() - self.name = None - - -def writeCustomPaintings(settings: Settings) -> None: - """Write custom painting files to ROM.""" - if js.cosmetics is None: - return - if js.cosmetics.tns_portals is None: - return - if js.cosmetic_names.tns_portals is None: - return - PAINTING_INFO = [ - PaintingData(64, 64, 2, 1, False, [0x1EA, 0x1E9]), # DK Isles - PaintingData(128, 128, 2, 4, True, [0x90A, 0x909, 0x903, 0x908, 0x904, 0x907, 0x905, 0x906]), # K Rool - PaintingData(128, 128, 2, 4, True, [0x9B4, 0x9AD, 0x9B3, 0x9AE, 0x9B2, 0x9AF, 0x9B1, 0x9B0]), # Knight - PaintingData(128, 128, 2, 4, True, [0x9A5, 0x9AC, 0x9A6, 0x9AB, 0x9A7, 0x9AA, 0x9A8, 0x9A9]), # Sword - PaintingData(64, 32, 1, 1, False, [0xA53]), # Dolphin - PaintingData(32, 64, 1, 1, False, [0xA46]), # Candy - # PaintingData(64, 64, 1, 1, False, [0x614, 0x615], True), # K Rool Run - # PaintingData(64, 64, 1, 1, False, [0x625, 0x626], True), # K Rool Blunderbuss - # PaintingData(64, 64, 1, 1, False, [0x627, 0x628], True), # K Rool Head - ] - file_data = list(zip(js.cosmetics.paintings, js.cosmetic_names.paintings)) - settings.painting_isles = None - settings.painting_museum_krool = None - settings.painting_museum_knight = None - settings.painting_museum_swords = None - settings.painting_treehouse_dolphin = None - settings.painting_treehouse_candy = None - if len(file_data) == 0: - return - list_pool = file_data.copy() - PAINTING_COUNT = len(PAINTING_INFO) - if len(list_pool) < PAINTING_COUNT: - mult = math.ceil(PAINTING_COUNT / len(list_pool)) - 1 - for _ in range(mult): - list_pool.extend(file_data.copy()) - random.shuffle(list_pool) - for painting in PAINTING_INFO: - painting.name = None - selected_painting = list_pool.pop(0) - painting.name = selected_painting[1].split("/")[-1] # File Name - im_f = Image.open(BytesIO(bytes(selected_painting[0]))) - im_f = getImageChunk(im_f, painting.width, painting.height) - im_f = im_f.transpose(Image.FLIP_TOP_BOTTOM).convert("RGBA") - chunks = [] - chunk_w = int(painting.width / painting.x_split) - chunk_h = int(painting.height / painting.y_split) - for y in range(painting.y_split): - for x in range(painting.x_split): - left = x * chunk_w - top = y * chunk_h - chunk_im = im_f.crop((int(left), int(top), int(left + chunk_w), int(top + chunk_h))) - chunks.append(chunk_im) - border_imgs = [] - for x in range(8): - border_tex = PAINTING_INFO[1].texture_order[x] - border_img = getImageFile(25, border_tex, True, 64, 32, TextureFormat.RGBA5551) - border_imgs.append(border_img) - for chunk_index, chunk in enumerate(chunks): - if painting.is_bordered: - border_img = border_imgs[chunk_index] - if chunk_index in (0, 1): - # Top - border_seg_img = border_img.crop((0, 0, 64, 14)) - chunk.paste(border_seg_img, (0, 0), border_seg_img) - if chunk_index in (0, 2, 4, 6): - # Left - border_seg_img = border_img.crop((0, 0, 14, 32)) - chunk.paste(border_seg_img, (0, 0), border_seg_img) - if chunk_index in (1, 3, 5, 7): - # Right - border_seg_img = border_img.crop((50, 0, 64, 32)) - chunk.paste(border_seg_img, (50, 0), border_seg_img) - if chunk_index in (6, 7): - # Bottom - border_seg_img = border_img.crop((0, 20, 64, 32)) - chunk.paste(border_seg_img, (0, 20), border_seg_img) - img_index = painting.texture_order[chunk_index] - writeColorImageToROM(chunk, 25, img_index, chunk_w, chunk_h, False, TextureFormat.RGBA5551) - settings.painting_isles = PAINTING_INFO[0].name - settings.painting_museum_krool = PAINTING_INFO[1].name - settings.painting_museum_knight = PAINTING_INFO[2].name - settings.painting_museum_swords = PAINTING_INFO[3].name - settings.painting_treehouse_dolphin = PAINTING_INFO[4].name - settings.painting_treehouse_candy = PAINTING_INFO[5].name - - def randomizePlants(ROM_COPY: ROM, settings: Settings): """Randomize the plants in the setup file.""" if not settings.misc_cosmetics: diff --git a/randomizer/Patching/Cosmetics/Colorblind.py b/randomizer/Patching/Cosmetics/Colorblind.py new file mode 100644 index 000000000..93249aa1a --- /dev/null +++ b/randomizer/Patching/Cosmetics/Colorblind.py @@ -0,0 +1,1047 @@ +import js +import gzip +import zlib +from randomizer.Settings import ColorblindMode +from randomizer.Patching.LibImage import ( + getRGBFromHash, + TextureFormat, + maskImage, + getImageFile, + getKongItemColor, + writeColorImageToROM, + ExtraTextures, + getBonusSkinOffset, +) +from randomizer.Patching.Lib import getRawFile, TableNames, writeRawFile +from randomizer.Patching.Patcher import ROM +from randomizer.Enums.Kongs import Kongs +from PIL import ImageEnhance + +def writeKasplatHairColorToROM(color, table_index, file_index, format: str): + """Write color to ROM for kasplats.""" + file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] + mask = getRGBFromHash(color) + if format == TextureFormat.RGBA32: + color_lst = mask.copy() + color_lst.append(255) # Alpha + null_color = [0] * 4 + else: + val_r = int((mask[0] >> 3) << 11) + val_g = int((mask[1] >> 3) << 6) + val_b = int((mask[2] >> 3) << 1) + rgba_val = val_r | val_g | val_b | 1 + color_lst = [(rgba_val >> 8) & 0xFF, rgba_val & 0xFF] + null_color = [0, 0] + bytes_array = [] + for y in range(42): + for x in range(32): + bytes_array.extend(color_lst) + for i in range(18): + bytes_array.extend(color_lst) + for i in range(4): + bytes_array.extend(null_color) + for i in range(3): + bytes_array.extend(color_lst) + data = bytearray(bytes_array) + if table_index == 25: + data = gzip.compress(data, compresslevel=9) + ROM().seek(file_start) + ROM().writeBytes(data) + + +def writeWhiteKasplatHairColorToROM(color1, color2, table_index, file_index, format: str): + """Write color to ROM for white kasplats, giving them a black-white block pattern.""" + file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] + mask = getRGBFromHash(color1) + mask2 = getRGBFromHash(color2) + if format == TextureFormat.RGBA32: + color_lst_0 = mask.copy() + color_lst_0.append(255) + color_lst_1 = mask2.copy() + color_lst_1.append(255) + null_color = [0] * 4 + else: + val_r = int((mask[0] >> 3) << 11) + val_g = int((mask[1] >> 3) << 6) + val_b = int((mask[2] >> 3) << 1) + rgba_val = val_r | val_g | val_b | 1 + val_r2 = int((mask2[0] >> 3) << 11) + val_g2 = int((mask2[1] >> 3) << 6) + val_b2 = int((mask2[2] >> 3) << 1) + rgba_val2 = val_r2 | val_g2 | val_b2 | 1 + color_lst_0 = [(rgba_val >> 8) & 0xFF, rgba_val & 0xFF] + color_lst_1 = [(rgba_val2 >> 8) & 0xFF, rgba_val2 & 0xFF] + null_color = [0] * 2 + bytes_array = [] + for y in range(42): + for x in range(32): + if (int(y / 7) + int(x / 8)) % 2 == 0: + bytes_array.extend(color_lst_0) + else: + bytes_array.extend(color_lst_1) + for i in range(18): + bytes_array.extend(color_lst_0) + for i in range(4): + bytes_array.extend(null_color) + for i in range(3): + bytes_array.extend(color_lst_0) + data = bytearray(bytes_array) + if table_index == 25: + data = gzip.compress(data, compresslevel=9) + ROM().seek(file_start) + ROM().writeBytes(data) + + +def writeKlaptrapSkinColorToROM(color_index, table_index, file_index, format: str, mode: ColorblindMode): + """Write color to ROM for klaptraps.""" + im_f = getImageFile(table_index, file_index, True, 32, 43, format) + im_f = maskImage(im_f, color_index, 0, (color_index != 3), mode) + pix = im_f.load() + file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] + if format == TextureFormat.RGBA32: + null_color = [0] * 4 + else: + null_color = [0, 0] + bytes_array = [] + for y in range(42): + for x in range(32): + color_lst = calculateKlaptrapPixel(list(pix[x, y]), format) + bytes_array.extend(color_lst) + for i in range(18): + color_lst = calculateKlaptrapPixel(list(pix[i, 42]), format) + bytes_array.extend(color_lst) + for i in range(4): + bytes_array.extend(null_color) + for i in range(3): + color_lst = calculateKlaptrapPixel(list(pix[(22 + i), 42]), format) + bytes_array.extend(color_lst) + data = bytearray(bytes_array) + if table_index == 25: + data = gzip.compress(data, compresslevel=9) + ROM().seek(file_start) + ROM().writeBytes(data) + + +def writeSpecialKlaptrapTextureToROM(color_index, table_index, file_index, format: str, pixels_to_ignore: list, mode: ColorblindMode): + """Write color to ROM for klaptraps special texture(s).""" + im_f = getImageFile(table_index, file_index, True, 32, 43, format) + pix_original = im_f.load() + pixels_original = [] + for x in range(32): + pixels_original.append([]) + for y in range(43): + pixels_original[x].append(list(pix_original[x, y]).copy()) + im_f_masked = maskImage(im_f, color_index, 0, (color_index != 3), mode) + pix = im_f_masked.load() + file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] + if format == TextureFormat.RGBA32: + null_color = [0] * 4 + else: + null_color = [0, 0] + bytes_array = [] + for y in range(42): + for x in range(32): + if [x, y] not in pixels_to_ignore: + color_lst = calculateKlaptrapPixel(list(pix[x, y]), format) + else: + color_lst = calculateKlaptrapPixel(list(pixels_original[x][y]), format) + bytes_array.extend(color_lst) + for i in range(18): + if [i, 42] not in pixels_to_ignore: + color_lst = calculateKlaptrapPixel(list(pix[i, 42]), format) + else: + color_lst = calculateKlaptrapPixel(list(pixels_original[i][42]), format) + bytes_array.extend(color_lst) + for i in range(4): + bytes_array.extend(null_color) + for i in range(3): + if [(22 + i), 42] not in pixels_to_ignore: + color_lst = calculateKlaptrapPixel(list(pix[(22 + i), 42]), format) + else: + color_lst = calculateKlaptrapPixel(list(pixels_original[(22 + i)][42]), format) + bytes_array.extend(color_lst) + data = bytearray(bytes_array) + if table_index == 25: + data = gzip.compress(data, compresslevel=9) + ROM().seek(file_start) + ROM().writeBytes(data) + + +def calculateKlaptrapPixel(mask: list, format: str): + """Calculate the new color for the given pixel.""" + if format == TextureFormat.RGBA32: + color_lst = mask.copy() + color_lst.append(255) # Alpha + else: + val_r = int((mask[0] >> 3) << 11) + val_g = int((mask[1] >> 3) << 6) + val_b = int((mask[2] >> 3) << 1) + rgba_val = val_r | val_g | val_b | 1 + color_lst = [(rgba_val >> 8) & 0xFF, rgba_val & 0xFF] + return color_lst + + +def maskBlueprintImage(im_f, base_index, mode: ColorblindMode): + """Apply RGB mask to blueprint image.""" + w, h = im_f.size + im_f_original = im_f + converter = ImageEnhance.Color(im_f) + im_f = converter.enhance(0) + im_dupe = im_f.crop((0, 0, w, h)) + brightener = ImageEnhance.Brightness(im_dupe) + im_dupe = brightener.enhance(2) + im_f.paste(im_dupe, (0, 0), im_dupe) + pix = im_f.load() + pix2 = im_f_original.load() + mask = getRGBFromHash(getKongItemColor(mode, base_index)) + if max(mask[0], max(mask[1], mask[2])) < 39: + for channel in range(3): + mask[channel] = max(39, mask[channel]) # Too black is bad for these items + w, h = im_f.size + for x in range(w): + for y in range(h): + base = list(pix[x, y]) + base2 = list(pix2[x, y]) + if base[3] > 0: + # Filter out the wooden frame + # brown is orange, is red and (red+green), is very little blue + # but, if the color is light, we can't rely on the blue value alone. + if base2[2] > 20 and (base2[2] > base2[1] or base2[1] - base2[2] < 20): + for channel in range(3): + base[channel] = int(mask[channel] * (base[channel] / 255)) + pix[x, y] = (base[0], base[1], base[2], base[3]) + else: + pix[x, y] = (base2[0], base2[1], base2[2], base2[3]) + return im_f + + +def maskLaserImage(im_f, base_index, mode: ColorblindMode): + """Apply RGB mask to laser texture.""" + w, h = im_f.size + im_f_original = im_f + converter = ImageEnhance.Color(im_f) + im_f = converter.enhance(0) + im_dupe = im_f.crop((0, 0, w, h)) + brightener = ImageEnhance.Brightness(im_dupe) + im_dupe = brightener.enhance(2) + im_f.paste(im_dupe, (0, 0), im_dupe) + pix = im_f.load() + pix2 = im_f_original.load() + mask = getRGBFromHash(getKongItemColor(mode, base_index)) + w, h = im_f.size + for x in range(w): + for y in range(h): + base = list(pix[x, y]) + base2 = list(pix2[x, y]) + if base[3] > 0: + # Filter out the white center of the laser + if min(base2[0], min(base2[1], base2[2])) <= 210: + for channel in range(3): + base[channel] = int(mask[channel] * (base[channel] / 255)) + pix[x, y] = (base[0], base[1], base[2], base[3]) + else: + pix[x, y] = (base2[0], base2[1], base2[2], base2[3]) + return im_f + + +def maskPotionImage(im_f, primary_color, secondary_color=None): + """Apply RGB mask to DK arcade potion reward preview texture.""" + w, h = im_f.size + pix = im_f.load() + mask = getRGBFromHash(primary_color) + if secondary_color is not None: + mask2 = secondary_color + for channel in range(3): + mask[channel] = max(1, mask[channel]) + w, h = im_f.size + for x in range(w): + for y in range(h): + base = list(pix[x, y]) + # Filter out transparent pixels and the cork + if base[3] > 0 and y > 2 and [x, y] not in [[9, 4], [10, 4]]: + # Filter out the bottle's contents + if base[0] == base[1] and base[1] == base[2]: + if secondary_color is not None: + # Color the bottle itself + for channel in range(3): + base[channel] = int(mask2[channel] * (base[channel] / 255)) + else: + # Color the bottle's contents + average_light = int((base[0] + base[1] + base[2]) / 3) + for channel in range(3): + base[channel] = int(mask[channel] * (average_light / 255)) + pix[x, y] = (base[0], base[1], base[2], base[3]) + return im_f + + +def recolorWrinklyDoors(mode: ColorblindMode): + """Recolor the Wrinkly hint door doorframes for colorblind mode.""" + file = [0xF0, 0xF2, 0xEF, 0x67, 0xF1] + for kong in range(5): + wrinkly_door_start = js.pointer_addresses[4]["entries"][file[kong]]["pointing_to"] + wrinkly_door_finish = js.pointer_addresses[4]["entries"][file[kong] + 1]["pointing_to"] + wrinkly_door_size = wrinkly_door_finish - wrinkly_door_start + ROM().seek(wrinkly_door_start) + indicator = int.from_bytes(ROM().readBytes(2), "big") + ROM().seek(wrinkly_door_start) + data = ROM().readBytes(wrinkly_door_size) + if indicator == 0x1F8B: + data = zlib.decompress(data, (15 + 32)) + num_data = [] # data, but represented as nums rather than b strings + for d in data: + num_data.append(d) + # Figure out which colors to use and where to put them (list extensions to mitigate the linter's "artistic freedom" putting 1 value per line) + color1_offsets = [ + 1548, + 1580, + 1612, + 1644, + 1676, + 1708, + 1756, + 1788, + 1804, + 1820, + 1836, + 1852, + 1868, + 1884, + 1900, + 1916, + ] + color1_offsets = color1_offsets + [ + 1932, + 1948, + 1964, + 1980, + 1996, + 2012, + 2028, + 2044, + 2076, + 2108, + 2124, + 2156, + 2188, + 2220, + 2252, + 2284, + ] + color1_offsets = color1_offsets + [ + 2316, + 2348, + 2380, + 2396, + 2412, + 2428, + 2444, + 2476, + 2508, + 2540, + 2572, + 2604, + 2636, + 2652, + 2668, + 2684, + ] + color1_offsets = color1_offsets + [ + 2700, + 2716, + 2732, + 2748, + 2764, + 2780, + 2796, + 2812, + 2828, + 2860, + 2892, + 2924, + 2956, + 2988, + 3020, + 3052, + ] + color2_offsets = [ + 1564, + 1596, + 1628, + 1660, + 1692, + 1724, + 1740, + 1772, + 2332, + 2364, + 2460, + 2492, + 2524, + 2556, + 2588, + 2620, + ] + color_str = getKongItemColor(mode, kong) + new_color1 = getRGBFromHash(color_str) + new_color2 = getRGBFromHash(color_str) + if kong == 0: + for channel in range(3): + new_color2[channel] = max(80, new_color1[channel]) # Too black is bad, because anything times 0 is 0 + + # Recolor the doorframe + for offset in color1_offsets: + for i in range(3): + num_data[offset + i] = new_color1[i] + for offset in color2_offsets: + for i in range(3): + num_data[offset + i] = new_color2[i] + + data = bytearray(num_data) # convert num_data back to binary string + if indicator == 0x1F8B: + data = gzip.compress(data, compresslevel=9) + ROM().seek(wrinkly_door_start) + ROM().writeBytes(data) + +def recolorKRoolShipSwitch(color: tuple, ROM_COPY: ROM): + """Recolors the simian slam switch that is part of K. Rool's ship in galleon.""" + addresses = ( + 0x4C34, + 0x4C44, + 0x4C54, + 0x4C64, + 0x4C74, + 0x4C84, + ) + data = bytearray(getRawFile(TableNames.ModelTwoGeometry, 305, True)) + for addr in addresses: + for x in range(3): + data[addr + x] = color[x] + new_tex = [ + 0xE7, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xE2, + 0x00, + 0x00, + 0x1C, + 0x0C, + 0x19, + 0x20, + 0x38, + 0xE3, + 0x00, + 0x0A, + 0x01, + 0x00, + 0x10, + 0x00, + 0x00, + 0xE3, + 0x00, + 0x0F, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xE7, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFC, + 0x12, + 0x7E, + 0x03, + 0xFF, + 0xFF, + 0xF9, + 0xF8, + 0xFD, + 0x90, + 0x00, + 0x00, + 0x00, + 0x00, + 0x0B, + 0xAF, + 0xF5, + 0x90, + 0x00, + 0x00, + 0x07, + 0x08, + 0x02, + 0x00, + 0xE6, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xF3, + 0x00, + 0x00, + 0x00, + 0x07, + 0x7F, + 0xF1, + 0x00, + 0xE7, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xF5, + 0x88, + 0x10, + 0x00, + 0x00, + 0x08, + 0x02, + 0x00, + 0xF2, + 0x00, + 0x00, + 0x00, + 0x00, + 0x0F, + 0xC0, + 0xFC, + ] + for x in range(8): + data[0x1AD8 + x] = 0 + for xi, x in enumerate(new_tex): + data[0x1AE8 + xi] = x + for x in range(40): + data[0x1B58 + x] = 0 + writeRawFile(TableNames.ModelTwoGeometry, 305, True, data, ROM_COPY) + +def recolorSlamSwitches(galleon_switch_value, ROM_COPY: ROM, mode: ColorblindMode): + """Recolor the Simian Slam switches for colorblind mode.""" + file = [0x94, 0x93, 0x95, 0x96, 0xB8, 0x16C, 0x16B, 0x16D, 0x16E, 0x16A, 0x167, 0x166, 0x168, 0x169, 0x165] + written_galleon_ship = False + for switch in range(15): + slam_switch_start = js.pointer_addresses[4]["entries"][file[switch]]["pointing_to"] + slam_switch_finish = js.pointer_addresses[4]["entries"][file[switch] + 1]["pointing_to"] + slam_switch_size = slam_switch_finish - slam_switch_start + ROM().seek(slam_switch_start) + indicator = int.from_bytes(ROM().readBytes(2), "big") + ROM().seek(slam_switch_start) + data = ROM().readBytes(slam_switch_size) + if indicator == 0x1F8B: + data = zlib.decompress(data, (15 + 32)) + num_data = [] # data, but represented as nums rather than b strings + for d in data: + num_data.append(d) + # Figure out which colors to use and where to put them + color_offsets = [1828, 1844, 1860, 1876, 1892, 1908] + green_switch_str = getKongItemColor(mode, Kongs.chunky) + blue_switch_str = getKongItemColor(mode, Kongs.lanky) + red_switch_str = getKongItemColor(mode, Kongs.diddy) + new_color1 = getRGBFromHash(green_switch_str) # chunky's color + new_color2 = getRGBFromHash(blue_switch_str) # lanky's color + new_color3 = getRGBFromHash(red_switch_str) # diddy's color + + # Green switches + if switch < 5: + for offset in color_offsets: + for i in range(3): + num_data[offset + i] = new_color1[i] + # Blue switches + elif switch < 10: + for offset in color_offsets: + for i in range(3): + num_data[offset + i] = new_color2[i] + # Red switches + else: + for offset in color_offsets: + for i in range(3): + num_data[offset + i] = new_color3[i] + + data = bytearray(num_data) # convert num_data back to binary string + if indicator == 0x1F8B: + data = gzip.compress(data, compresslevel=9) + ROM().seek(slam_switch_start) + ROM().writeBytes(data) + if not written_galleon_ship: + galleon_switch_color = new_color1.copy() + if galleon_switch_value is not None: + if galleon_switch_value != 1: + galleon_switch_color = new_color3.copy() + if galleon_switch_value == 2: + galleon_switch_color = new_color2.copy() + recolorKRoolShipSwitch(galleon_switch_color, ROM_COPY) + written_galleon_ship = True + + +def recolorBlueprintModelTwo(mode: ColorblindMode): + """Recolor the Blueprint Model2 items for colorblind mode.""" + file = [0xDE, 0xE0, 0xE1, 0xDD, 0xDF] + for kong in range(5): + blueprint_model2_start = js.pointer_addresses[4]["entries"][file[kong]]["pointing_to"] + blueprint_model2_finish = js.pointer_addresses[4]["entries"][file[kong] + 1]["pointing_to"] + blueprint_model2_size = blueprint_model2_finish - blueprint_model2_start + ROM().seek(blueprint_model2_start) + indicator = int.from_bytes(ROM().readBytes(2), "big") + ROM().seek(blueprint_model2_start) + data = ROM().readBytes(blueprint_model2_size) + if indicator == 0x1F8B: + data = zlib.decompress(data, (15 + 32)) + num_data = [] # data, but represented as nums rather than b strings + for d in data: + num_data.append(d) + # Figure out which colors to use and where to put them + color1_offsets = [0x52C, 0x54C, 0x57C, 0x58C, 0x5AC, 0x5CC, 0x5FC, 0x61C] + color2_offsets = [0x53C, 0x55C, 0x5EC, 0x60C] + color3_offsets = [0x56C, 0x59C, 0x5BC, 0x5DC] + new_color = getRGBFromHash(getKongItemColor(mode, kong)) + if kong == 0: + for channel in range(3): + new_color[channel] = max(39, new_color[channel]) # Too black is bad, because anything times 0 is 0 + + # Recolor the model2 item + for offset in color1_offsets: + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + for i in range(3): + num_data[offset + i] = int(total_light / 3) + for i in range(3): + num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + for offset in color2_offsets: + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + for i in range(3): + num_data[offset + i] = int(total_light / 3) + for i in range(3): + num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + for offset in color3_offsets: + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + for i in range(3): + num_data[offset + i] = int(total_light / 3) + for i in range(3): + num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + + data = bytearray(num_data) # convert num_data back to binary string + if indicator == 0x1F8B: + data = gzip.compress(data, compresslevel=9) + ROM().seek(blueprint_model2_start) + ROM().writeBytes(data) + +def maskImageRotatingRoomTile(im_f, im_mask, paste_coords, image_color_index, tile_side, mode: ColorblindMode): + """Apply RGB mask to image of a Rotating Room Memory Tile.""" + w, h = im_f.size + im_original = im_f + pix_original = im_original.load() + pixels_original = [] + for x in range(w): + pixels_original.append([]) + for y in range(h): + pixels_original[x].append(list(pix_original[x, y]).copy()) + converter = ImageEnhance.Color(im_f) + im_f = converter.enhance(0) + brightener = ImageEnhance.Brightness(im_f) + im_f = brightener.enhance(2) + pix = im_f.load() + pix_mask = im_mask.load() + w2, h2 = im_mask.size + mask_coords = [] + for x in range(w2): + for y in range(h2): + coord = list(pix_mask[x, y]) + if coord[3] > 0: + mask_coords.append([(x + paste_coords[0]), (y + paste_coords[1])]) + if image_color_index < 5: + mask = getRGBFromHash(getKongItemColor(mode, image_color_index)) + for channel in range(3): + mask[channel] = max(39, mask[channel]) # Too dark looks bad + else: + mask = getRGBFromHash(getKongItemColor(mode, Kongs.lanky)) + mask2 = getRGBFromHash("#000000") + if image_color_index == 0: + mask2 = getRGBFromHash("#FFFFFF") + for x in range(w): + for y in range(h): + base = list(pix[x, y]) + base_original = list(pixels_original[x][y]) + if [x, y] not in mask_coords: + if image_color_index in [1, 2, 4]: # Diddy, Lanky and Chunky don't get any special features + for channel in range(3): + base[channel] = int(mask[channel] * (base[channel] / 255)) + elif image_color_index in [0, 3]: # Donkey and Tiny get a diamond-shape frame + side = w + if tile_side == 1: + side = 0 + if abs(abs(side - x) - y) < 2 or abs(abs(side - x) - abs(h - y)) < 2: + for channel in range(3): + base[channel] = int(mask2[channel] * (base[channel] / 255)) + else: + for channel in range(3): + base[channel] = int(mask[channel] * (base[channel] / 255)) + else: # Golden Banana gets a block-pattern + if (int(x / 8) + int(y / 8)) % 2 == 0: + for channel in range(3): + base[channel] = int(mask[channel] * (base[channel] / 255)) + else: + for channel in range(3): + base[channel] = int(mask2[channel] * (base[channel] / 255)) + else: + for channel in range(3): + base[channel] = base_original[channel] + pix[x, y] = (base[0], base[1], base[2], base[3]) + return im_f + +def recolorRotatingRoomTiles(mode): + """Determine how to recolor the tiles rom the memory game in Donkey's Rotating Room in Caves.""" + question_mark_tiles = [900, 901, 892, 893, 896, 897, 890, 891, 898, 899, 894, 895] + face_tiles = [ + 874, + 878, + 875, + 879, + 876, + 886, + 877, + 885, + 880, + 887, + 881, + 888, + 870, + 872, + 871, + 873, + 866, + 882, + 867, + 883, + 868, + 889, + 869, + 884, + ] + question_mark_tile_masks = [508, 509] + face_tile_masks = [636, 635, 633, 634, 631, 632, 630, 629, 627, 628, 5478, 5478] + question_mark_resize = [17, 37] + face_resize = [[32, 64], [32, 64], [32, 64], [32, 64], [32, 64], [71, 66]] + question_mark_offsets = [[16, 14], [0, 14]] + face_offsets = [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [-5, -1], [-38, -1]] + + for tile in range(len(question_mark_tiles)): + tile_image = getImageFile(7, question_mark_tiles[tile], False, 32, 64, TextureFormat.RGBA5551) + mask = getImageFile(7, question_mark_tile_masks[(tile % 2)], False, 32, 64, TextureFormat.RGBA5551) + resize = question_mark_resize + mask = mask.resize((resize[0], resize[1])) + masked_tile = maskImageRotatingRoomTile(tile_image, mask, question_mark_offsets[(tile % 2)], int(tile / 2), (tile % 2), mode) + writeColorImageToROM(masked_tile, 7, question_mark_tiles[tile], 32, 64, False, TextureFormat.RGBA5551) + for tile in range(len(face_tiles)): + face_index = int(tile / 4) + if face_index < 5: + width = 32 + height = 64 + else: + width = 44 + height = 44 + mask = getImageFile(25, face_tile_masks[int(tile / 2)], True, width, height, TextureFormat.RGBA5551) + resize = face_resize[face_index] + mask = mask.resize((resize[0], resize[1])) + tile_image = getImageFile(7, face_tiles[tile], False, 32, 64, TextureFormat.RGBA5551) + masked_tile = maskImageRotatingRoomTile(tile_image, mask, face_offsets[int(tile / 2)], face_index, (int(tile / 2) % 2), mode) + writeColorImageToROM(masked_tile, 7, face_tiles[tile], 32, 64, False, TextureFormat.RGBA5551) + +def recolorBells(): + """Recolor the Chunky Minecart bells for colorblind mode (prot/deut).""" + file = 693 + minecart_bell_start = js.pointer_addresses[4]["entries"][file]["pointing_to"] + minecart_bell_finish = js.pointer_addresses[4]["entries"][file + 1]["pointing_to"] + minecart_bell_size = minecart_bell_finish - minecart_bell_start + ROM().seek(minecart_bell_start) + indicator = int.from_bytes(ROM().readBytes(2), "big") + ROM().seek(minecart_bell_start) + data = ROM().readBytes(minecart_bell_size) + if indicator == 0x1F8B: + data = zlib.decompress(data, (15 + 32)) + num_data = [] # data, but represented as nums rather than b strings + for d in data: + num_data.append(d) + # Figure out which colors to use and where to put them + color1_offsets = [0x214, 0x244, 0x264, 0x274, 0x284] + color2_offsets = [0x224, 0x234, 0x254] + new_color1 = getRGBFromHash("#0066FF") + new_color2 = getRGBFromHash("#0000FF") + + # Recolor the bell + for offset in color1_offsets: + for i in range(3): + num_data[offset + i] = new_color1[i] + for offset in color2_offsets: + for i in range(3): + num_data[offset + i] = new_color2[i] + + data = bytearray(num_data) # convert num_data back to binary string + if indicator == 0x1F8B: + data = gzip.compress(data, compresslevel=9) + ROM().seek(minecart_bell_start) + ROM().writeBytes(data) + + +def recolorKlaptraps(mode): + """Recolor the klaptrap models for colorblind mode.""" + green_files = [0xF31, 0xF32, 0xF33, 0xF35, 0xF37, 0xF39] # 0xF2F collar? 0xF30 feet? + red_files = [0xF44, 0xF45, 0xF46, 0xF47, 0xF48, 0xF49] # , 0xF42 collar? 0xF43 feet? + purple_files = [0xF3C, 0xF3D, 0xF3E, 0xF3F, 0xF40, 0xF41] # 0xF3B feet?, 0xF3A collar? + + # Regular textures + for file in range(6): + writeKlaptrapSkinColorToROM(4, 25, green_files[file], TextureFormat.RGBA5551, mode) + writeKlaptrapSkinColorToROM(1, 25, red_files[file], TextureFormat.RGBA5551, mode) + writeKlaptrapSkinColorToROM(3, 25, purple_files[file], TextureFormat.RGBA5551, mode) + + belly_pixels_to_ignore = [] + for x in range(32): + for y in range(43): + if y < 29 or (y > 31 and y < 39) or y == 40 or y == 42: + belly_pixels_to_ignore.append([x, y]) + elif (y == 39 and x < 16) or (y == 41 and x < 24): + belly_pixels_to_ignore.append([x, y]) + + # Special texture that requires only partial recoloring, in this case file 0xF38 which is the belly, and only the few green pixels + writeSpecialKlaptrapTextureToROM(4, 25, 0xF38, TextureFormat.RGBA5551, belly_pixels_to_ignore, mode) + + +def recolorPotions(colorblind_mode): + """Overwrite potion colors.""" + diddy_color = getKongItemColor(colorblind_mode, Kongs.diddy) + chunky_color = getKongItemColor(colorblind_mode, Kongs.chunky) + secondary_color = [diddy_color, None, chunky_color, diddy_color, None, None] + if colorblind_mode == ColorblindMode.trit: + secondary_color[0] = chunky_color + secondary_color[2] = None + for color in range(len(secondary_color)): + if secondary_color[color] is not None: + secondary_color[color] = getRGBFromHash(secondary_color[color]) + + # Actor: + file = [[0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2], [0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA]] + for type in range(2): + for potion_color in range(6): + potion_actor_start = js.pointer_addresses[5]["entries"][file[type][potion_color]]["pointing_to"] + potion_actor_finish = js.pointer_addresses[5]["entries"][file[type][potion_color] + 1]["pointing_to"] + potion_actor_size = potion_actor_finish - potion_actor_start + ROM().seek(potion_actor_start) + indicator = int.from_bytes(ROM().readBytes(2), "big") + ROM().seek(potion_actor_start) + data = ROM().readBytes(potion_actor_size) + if indicator == 0x1F8B: + data = zlib.decompress(data, (15 + 32)) + num_data = [] # data, but represented as nums rather than b strings + for d in data: + num_data.append(d) + # Figure out which colors to use and where to put them + color1_offsets = [0x34] + color2_offsets = [0x44, 0x54, 0xA4] + color3_offsets = [0x64, 0x74, 0x84, 0xE4] + color4_offsets = [0x94] + color5_offsets = [0xB4, 0xC4, 0xD4] + # color6_offsets = [0xF4, 0x104, 0x114, 0x124, 0x134, 0x144, 0x154, 0x164] + if potion_color < 5: + new_color = getRGBFromHash(getKongItemColor(colorblind_mode, potion_color)) + else: + new_color = getRGBFromHash("#FFFFFF") + + # Recolor the actor item + for offset in color1_offsets: + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + for i in range(3): + num_data[offset + i] = int(total_light / 3) + for i in range(3): + if secondary_color[potion_color] is not None and potion_color == 3: # tiny + num_data[offset + i] = int(num_data[offset + i] * (secondary_color[potion_color][i] / 255)) + elif secondary_color[potion_color] is not None: # donkey gets an even darker shade + num_data[offset + i] = int(num_data[offset + i] * (int(secondary_color[potion_color][i] / 8) / 255)) + elif secondary_color[potion_color] is not None: # other kongs with a secondary color get a darker shade + num_data[offset + i] = int(num_data[offset + i] * (int(secondary_color[potion_color][i] / 4) / 255)) + else: + num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + for offset in color2_offsets: + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + for i in range(3): + num_data[offset + i] = int(total_light / 3) + for i in range(3): + num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + for offset in color3_offsets: + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + for i in range(3): + num_data[offset + i] = int(total_light / 3) + for i in range(3): + num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + for offset in color4_offsets: + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + for i in range(3): + num_data[offset + i] = int(total_light / 3) + for i in range(3): + num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + for offset in color5_offsets: + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + for i in range(3): + num_data[offset + i] = int(total_light / 3) + for i in range(3): + num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + + data = bytearray(num_data) # convert num_data back to binary string + if indicator == 0x1F8B: + data = gzip.compress(data, compresslevel=9) + if len(data) > potion_actor_size: + print(f"Attempted size bigger {hex(len(data))} than slot {hex(potion_actor_size)}") + continue + ROM().seek(potion_actor_start) + ROM().writeBytes(data) + + # Model2: + file = [91, 498, 89, 499, 501, 502] + for potion_color in range(6): + potion_model2_start = js.pointer_addresses[4]["entries"][file[potion_color]]["pointing_to"] + potion_model2_finish = js.pointer_addresses[4]["entries"][file[potion_color] + 1]["pointing_to"] + potion_model2_size = potion_model2_finish - potion_model2_start + ROM().seek(potion_model2_start) + indicator = int.from_bytes(ROM().readBytes(2), "big") + ROM().seek(potion_model2_start) + data = ROM().readBytes(potion_model2_size) + if indicator == 0x1F8B: + data = zlib.decompress(data, (15 + 32)) + num_data = [] # data, but represented as nums rather than b strings + for d in data: + num_data.append(d) + # Figure out which colors to use and where to put them + color1_offsets = [0x144] + color2_offsets = [0x154, 0x164, 0x1B4] + color3_offsets = [0x174, 0x184, 0x194, 0x1F4] + color4_offsets = [0x1A4] + color5_offsets = [0x1C4, 0x1D4, 0x1E4] + # color6_offsets = [0x204, 0x214, 0x224, 0x234, 0x244, 0x254, 0x264, 0x274] + if potion_color < 5: + new_color = getRGBFromHash(getKongItemColor(colorblind_mode, potion_color)) + else: + new_color = getRGBFromHash("#FFFFFF") + + # Recolor the model2 item + for offset in color1_offsets: + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + for i in range(3): + num_data[offset + i] = int(total_light / 3) + for i in range(3): + if secondary_color[potion_color] is not None and potion_color == 3: # tiny + num_data[offset + i] = int(num_data[offset + i] * (secondary_color[potion_color][i] / 255)) + elif secondary_color[potion_color] is not None: # donkey gets an even darker shade + num_data[offset + i] = int(num_data[offset + i] * (int(secondary_color[potion_color][i] / 8) / 255)) + elif secondary_color[potion_color] is not None: # other kongs with a secondary color get a darker shade + num_data[offset + i] = int(num_data[offset + i] * (int(secondary_color[potion_color][i] / 4) / 255)) + else: + num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + for offset in color2_offsets: + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + for i in range(3): + num_data[offset + i] = int(total_light / 3) + for i in range(3): + num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + for offset in color3_offsets: + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + for i in range(3): + num_data[offset + i] = int(total_light / 3) + for i in range(3): + num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + for offset in color4_offsets: + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + for i in range(3): + num_data[offset + i] = int(total_light / 3) + for i in range(3): + num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + for offset in color5_offsets: + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + for i in range(3): + num_data[offset + i] = int(total_light / 3) + for i in range(3): + num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + + data = bytearray(num_data) # convert num_data back to binary string + if indicator == 0x1F8B: + data = gzip.compress(data, compresslevel=9) + ROM().seek(potion_model2_start) + ROM().writeBytes(data) + + return + # DK Arcade sprites + for file in range(8, 14): + index = file - 8 + if index < 5: + color = getKongItemColor(colorblind_mode, index) + else: + color = "#FFFFFF" + potion_image = getImageFile(6, file, False, 20, 20, TextureFormat.RGBA5551) + potion_image = maskPotionImage(potion_image, color, secondary_color[index]) + writeColorImageToROM(potion_image, 6, file, 20, 20, False, TextureFormat.RGBA5551) + +def maskMushroomImage(im_f, reference_image, color, side_2=False): + """Apply RGB mask to mushroom image.""" + w, h = im_f.size + pixels_to_mask = [] + pix_ref = reference_image.load() + for x in range(w): + for y in range(h): + base_ref = list(pix_ref[x, y]) + # Filter out the white dots that won't get filtered out correctly with the below conditions + if not (max(abs(base_ref[0] - base_ref[2]), abs(base_ref[1] - base_ref[2])) < 41 and abs(base_ref[0] - base_ref[1]) < 11): + # Filter out that one lone pixel that is technically blue AND gets through the above filter, but should REALLY not be blue + if not (side_2 is True and x == 51 and y == 21): + # Select the exact pixels to mask, which is all the "blue" pixels, filtering out the white spots + if base_ref[2] > base_ref[0] and base_ref[2] > base_ref[1] and int(base_ref[0] + base_ref[1]) < 200: + pixels_to_mask.append([x, y]) + # Select the darker blue pixels as well + elif base_ref[2] > int(base_ref[0] + base_ref[1]): + pixels_to_mask.append([x, y]) + pix = im_f.load() + mask = getRGBFromHash(color) + for channel in range(3): + mask[channel] = max(1, mask[channel]) # Absolute black is bad + for x in range(w): + for y in range(h): + base = list(pix[x, y]) + if base[3] > 0 and [x, y] in pixels_to_mask: + average_light = int((base[0] + base[1] + base[2]) / 3) + for channel in range(3): + base[channel] = int(mask[channel] * (average_light / 255)) + pix[x, y] = (base[0], base[1], base[2], base[3]) + return im_f + +def recolorMushrooms(mode: ColorblindMode): + """Recolor the various colored mushrooms in the game for colorblind mode.""" + reference_mushroom_image = getImageFile(7, 297, False, 32, 32, TextureFormat.RGBA5551) + reference_mushroom_image_side1 = getImageFile(25, 0xD64, True, 64, 32, TextureFormat.RGBA5551) + reference_mushroom_image_side2 = getImageFile(25, 0xD65, True, 64, 32, TextureFormat.RGBA5551) + files_table_7 = [296, 295, 297, 299, 298] + files_table_25_side_1 = [0xD60, getBonusSkinOffset(ExtraTextures.MushTop0), 0xD64, 0xD62, 0xD66] + files_table_25_side_2 = [0xD61, getBonusSkinOffset(ExtraTextures.MushTop1), 0xD65, 0xD63, 0xD67] + for file in range(5): + # Mushroom on the ceiling inside Fungi Forest Lobby + file_color = getKongItemColor(mode, file) + mushroom_image = getImageFile(7, files_table_7[file], False, 32, 32, TextureFormat.RGBA5551) + mushroom_image = maskMushroomImage(mushroom_image, reference_mushroom_image, file_color) + writeColorImageToROM(mushroom_image, 7, files_table_7[file], 32, 32, False, TextureFormat.RGBA5551) + # Mushrooms in Lanky's colored mushroom puzzle (and possibly also the bouncy mushrooms) + mushroom_image_side_1 = getImageFile(25, files_table_25_side_1[file], True, 64, 32, TextureFormat.RGBA5551) + mushroom_image_side_1 = maskMushroomImage(mushroom_image_side_1, reference_mushroom_image_side1, file_color) + writeColorImageToROM(mushroom_image_side_1, 25, files_table_25_side_1[file], 64, 32, False, TextureFormat.RGBA5551) + mushroom_image_side_2 = getImageFile(25, files_table_25_side_2[file], True, 64, 32, TextureFormat.RGBA5551) + mushroom_image_side_2 = maskMushroomImage(mushroom_image_side_2, reference_mushroom_image_side2, file_color, True) + writeColorImageToROM(mushroom_image_side_2, 25, files_table_25_side_2[file], 64, 32, False, TextureFormat.RGBA5551) \ No newline at end of file diff --git a/randomizer/Patching/Cosmetics/CustomTextures.py b/randomizer/Patching/Cosmetics/CustomTextures.py new file mode 100644 index 000000000..fa3b77c99 --- /dev/null +++ b/randomizer/Patching/Cosmetics/CustomTextures.py @@ -0,0 +1,206 @@ +import js +import random +import math +from io import BytesIO +from typing import TYPE_CHECKING + +from randomizer.Settings import Settings +from randomizer.Patching.LibImage import writeColorImageToROM, TextureFormat, getImageFile + +if TYPE_CHECKING: + from PIL.Image import Image + +def writeTransition(settings: Settings) -> None: + """Write transition cosmetic to ROM.""" + if js.cosmetics is None: + return + if js.cosmetics.transitions is None: + return + if js.cosmetic_names.transitions is None: + return + file_data = list(zip(js.cosmetics.transitions, js.cosmetic_names.transitions)) + settings.custom_transition = None + if len(file_data) == 0: + return + selected_transition = random.choice(file_data) + settings.custom_transition = selected_transition[1].split("/")[-1] # File Name + im_f = Image.open(BytesIO(bytes(selected_transition[0]))) + writeColorImageToROM(im_f, 14, 95, 64, 64, False, TextureFormat.IA4) + + +def getImageChunk(im_f, width: int, height: int): + """Get an image chunk based on a width and height.""" + width_height_ratio = width / height + im_w, im_h = im_f.size + im_wh_ratio = im_w / im_h + if im_wh_ratio != width_height_ratio: + # Ratio doesn't match, we have to do some rejigging + scale = 1 + if width_height_ratio > im_wh_ratio: + # Scale based on width + scale = width / im_w + else: + # Height needs growing + scale = height / im_h + im_f = im_f.resize((int(im_w * scale), int(im_h * scale))) + im_w, im_h = im_f.size + middle_w = im_w / 2 + middle_h = im_h / 2 + middle_targ_w = width / 2 + middle_targ_h = height / 2 + return im_f.crop( + ( + int(middle_w - middle_targ_w), + int(middle_h - middle_targ_h), + int(middle_w + middle_targ_w), + int(middle_h + middle_targ_h), + ) + ) + # Ratio matches, just scale up + return im_f.resize((width, height)) + + +def writeCustomPortal(settings: Settings) -> None: + """Write custom portal file to ROM.""" + if js.cosmetics is None: + return + if js.cosmetics.tns_portals is None: + return + if js.cosmetic_names.tns_portals is None: + return + file_data = list(zip(js.cosmetics.tns_portals, js.cosmetic_names.tns_portals)) + settings.custom_troff_portal = None + if len(file_data) == 0: + return + selected_portal = random.choice(file_data) + settings.custom_troff_portal = selected_portal[1].split("/")[-1] # File Name + im_f = Image.open(BytesIO(bytes(selected_portal[0]))) + im_f = getImageChunk(im_f, 63, 63) + im_f = im_f.transpose(Image.FLIP_TOP_BOTTOM).convert("RGBA") + portal_data = { + "NW": { + "x_min": 0, + "y_min": 0, + "writes": [0x39E, 0x39F], + }, + "SW": { + "x_min": 0, + "y_min": 31, + "writes": [0x3A0, 0x39D], + }, + "SE": { + "x_min": 31, + "y_min": 31, + "writes": [0x3A2, 0x39B], + }, + "NE": { + "x_min": 31, + "y_min": 0, + "writes": [0x39C, 0x3A1], + }, + } + for sub in portal_data.keys(): + x_min = portal_data[sub]["x_min"] + y_min = portal_data[sub]["y_min"] + local_img = im_f.crop((x_min, y_min, x_min + 32, y_min + 32)) + for idx in portal_data[sub]["writes"]: + writeColorImageToROM(local_img, 7, idx, 32, 32, False, TextureFormat.RGBA5551) + + +class PaintingData: + """Class to store information regarding a painting.""" + + def __init__(self, width: int, height: int, x_split: int, y_split: int, is_bordered: bool, texture_order: list, is_ci: bool = False): + """Initialize with given parameters.""" + self.width = width + self.height = height + self.x_split = x_split + self.y_split = y_split + self.is_bordered = is_bordered + self.texture_order = texture_order.copy() + self.name = None + + +def writeCustomPaintings(settings: Settings) -> None: + """Write custom painting files to ROM.""" + if js.cosmetics is None: + return + if js.cosmetics.tns_portals is None: + return + if js.cosmetic_names.tns_portals is None: + return + PAINTING_INFO = [ + PaintingData(64, 64, 2, 1, False, [0x1EA, 0x1E9]), # DK Isles + PaintingData(128, 128, 2, 4, True, [0x90A, 0x909, 0x903, 0x908, 0x904, 0x907, 0x905, 0x906]), # K Rool + PaintingData(128, 128, 2, 4, True, [0x9B4, 0x9AD, 0x9B3, 0x9AE, 0x9B2, 0x9AF, 0x9B1, 0x9B0]), # Knight + PaintingData(128, 128, 2, 4, True, [0x9A5, 0x9AC, 0x9A6, 0x9AB, 0x9A7, 0x9AA, 0x9A8, 0x9A9]), # Sword + PaintingData(64, 32, 1, 1, False, [0xA53]), # Dolphin + PaintingData(32, 64, 1, 1, False, [0xA46]), # Candy + # PaintingData(64, 64, 1, 1, False, [0x614, 0x615], True), # K Rool Run + # PaintingData(64, 64, 1, 1, False, [0x625, 0x626], True), # K Rool Blunderbuss + # PaintingData(64, 64, 1, 1, False, [0x627, 0x628], True), # K Rool Head + ] + file_data = list(zip(js.cosmetics.paintings, js.cosmetic_names.paintings)) + settings.painting_isles = None + settings.painting_museum_krool = None + settings.painting_museum_knight = None + settings.painting_museum_swords = None + settings.painting_treehouse_dolphin = None + settings.painting_treehouse_candy = None + if len(file_data) == 0: + return + list_pool = file_data.copy() + PAINTING_COUNT = len(PAINTING_INFO) + if len(list_pool) < PAINTING_COUNT: + mult = math.ceil(PAINTING_COUNT / len(list_pool)) - 1 + for _ in range(mult): + list_pool.extend(file_data.copy()) + random.shuffle(list_pool) + for painting in PAINTING_INFO: + painting.name = None + selected_painting = list_pool.pop(0) + painting.name = selected_painting[1].split("/")[-1] # File Name + im_f = Image.open(BytesIO(bytes(selected_painting[0]))) + im_f = getImageChunk(im_f, painting.width, painting.height) + im_f = im_f.transpose(Image.FLIP_TOP_BOTTOM).convert("RGBA") + chunks = [] + chunk_w = int(painting.width / painting.x_split) + chunk_h = int(painting.height / painting.y_split) + for y in range(painting.y_split): + for x in range(painting.x_split): + left = x * chunk_w + top = y * chunk_h + chunk_im = im_f.crop((int(left), int(top), int(left + chunk_w), int(top + chunk_h))) + chunks.append(chunk_im) + border_imgs = [] + for x in range(8): + border_tex = PAINTING_INFO[1].texture_order[x] + border_img = getImageFile(25, border_tex, True, 64, 32, TextureFormat.RGBA5551) + border_imgs.append(border_img) + for chunk_index, chunk in enumerate(chunks): + if painting.is_bordered: + border_img = border_imgs[chunk_index] + if chunk_index in (0, 1): + # Top + border_seg_img = border_img.crop((0, 0, 64, 14)) + chunk.paste(border_seg_img, (0, 0), border_seg_img) + if chunk_index in (0, 2, 4, 6): + # Left + border_seg_img = border_img.crop((0, 0, 14, 32)) + chunk.paste(border_seg_img, (0, 0), border_seg_img) + if chunk_index in (1, 3, 5, 7): + # Right + border_seg_img = border_img.crop((50, 0, 64, 32)) + chunk.paste(border_seg_img, (50, 0), border_seg_img) + if chunk_index in (6, 7): + # Bottom + border_seg_img = border_img.crop((0, 20, 64, 32)) + chunk.paste(border_seg_img, (0, 20), border_seg_img) + img_index = painting.texture_order[chunk_index] + writeColorImageToROM(chunk, 25, img_index, chunk_w, chunk_h, False, TextureFormat.RGBA5551) + settings.painting_isles = PAINTING_INFO[0].name + settings.painting_museum_krool = PAINTING_INFO[1].name + settings.painting_museum_knight = PAINTING_INFO[2].name + settings.painting_museum_swords = PAINTING_INFO[3].name + settings.painting_treehouse_dolphin = PAINTING_INFO[4].name + settings.painting_treehouse_candy = PAINTING_INFO[5].name \ No newline at end of file diff --git a/randomizer/Patching/Cosmetics/Holiday.py b/randomizer/Patching/Cosmetics/Holiday.py new file mode 100644 index 000000000..221164431 --- /dev/null +++ b/randomizer/Patching/Cosmetics/Holiday.py @@ -0,0 +1,236 @@ +import gzip +import js +from PIL import Image, ImageEnhance +from randomizer.Patching.Patcher import ROM +from randomizer.Patching.Lib import Holidays, getHoliday +from randomizer.Patching.LibImage import ( + getImageFile, + getBonusSkinOffset, + ExtraTextures, + TextureFormat, + maskImageWithColor, + writeColorImageToROM, + hueShift, + hueShiftImageContainer, +) +from randomizer.Settings import CharacterColors, KongModels + +def changeBarrelColor(barrel_color: tuple = None, metal_color: tuple = None, brighten_barrel: bool = False): + """Change the colors of the various barrels.""" + wood_img = getImageFile(25, getBonusSkinOffset(ExtraTextures.ShellWood), True, 32, 64, TextureFormat.RGBA5551) + metal_img = getImageFile(25, getBonusSkinOffset(ExtraTextures.ShellMetal), True, 32, 64, TextureFormat.RGBA5551) + qmark_img = getImageFile(25, getBonusSkinOffset(ExtraTextures.ShellQMark), True, 32, 64, TextureFormat.RGBA5551) + if barrel_color is not None: + if brighten_barrel: + enhancer = ImageEnhance.Brightness(wood_img) + wood_img = enhancer.enhance(2) + wood_img = maskImageWithColor(wood_img, barrel_color) + if metal_color is not None: + metal_img = maskImageWithColor(metal_img, metal_color) + wood_img.paste(metal_img, (0, 0), metal_img) + writeColorImageToROM(wood_img, 25, getBonusSkinOffset(ExtraTextures.BonusShell), 32, 64, False, TextureFormat.RGBA5551) # Bonus Barrel + tag_img = Image.new(mode="RGBA", size=(32, 64)) + tag_img.paste(wood_img, (0, 0), wood_img) + tag_img.paste(qmark_img, (0, 0), qmark_img) + writeColorImageToROM(tag_img, 25, 4938, 32, 64, False, TextureFormat.RGBA5551) # Tag Barrel + # Compose Transform Barrels + kongs = [ + {"face_left": 0x27C, "face_right": 0x27B, "barrel_tex_start": 4817, "targ_width": 24}, # DK + {"face_left": 0x279, "face_right": 0x27A, "barrel_tex_start": 4815, "targ_width": 24}, # Diddy + {"face_left": 0x277, "face_right": 0x278, "barrel_tex_start": 4819, "targ_width": 24}, # Lanky + {"face_left": 0x276, "face_right": 0x275, "barrel_tex_start": 4769, "targ_width": 24}, # Tiny + {"face_left": 0x273, "face_right": 0x274, "barrel_tex_start": 4747, "targ_width": 24}, # Chunky + ] + for kong in kongs: + bar_left = Image.new(mode="RGBA", size=(32, 64)) + bar_right = Image.new(mode="RGBA", size=(32, 64)) + face_left = getImageFile(25, kong["face_left"], True, 32, 64, TextureFormat.RGBA5551) + face_right = getImageFile(25, kong["face_right"], True, 32, 64, TextureFormat.RGBA5551) + width = kong["targ_width"] + height = width * 2 + face_left = face_left.resize((width, height)) + face_right = face_right.resize((width, height)) + right_w_offset = 32 - width + top_h_offset = (64 - height) >> 1 + bar_left.paste(wood_img, (0, 0), wood_img) + bar_right.paste(wood_img, (0, 0), wood_img) + bar_left.paste(face_left, (right_w_offset, top_h_offset), face_left) + bar_right.paste(face_right, (0, top_h_offset), face_right) + writeColorImageToROM(bar_left, 25, kong["barrel_tex_start"], 32, 64, False, TextureFormat.RGBA5551) + writeColorImageToROM(bar_right, 25, kong["barrel_tex_start"] + 1, 32, 64, False, TextureFormat.RGBA5551) + # Cannons + barrel_left = Image.new(mode="RGBA", size=(32, 64)) + barrel_right = Image.new(mode="RGBA", size=(32, 64)) + barrel_left.paste(wood_img, (0, 0), wood_img) + barrel_right.paste(wood_img, (0, 0), wood_img) + barrel_left = barrel_left.crop((0, 0, 16, 64)) + barrel_right = barrel_right.crop((16, 0, 32, 64)) + writeColorImageToROM(barrel_left, 25, 0x12B3, 16, 64, False, TextureFormat.RGBA5551) + writeColorImageToROM(barrel_right, 25, 0x12B4, 16, 64, False, TextureFormat.RGBA5551) + if barrel_color is not None: + tex_data = { + getBonusSkinOffset(ExtraTextures.RocketTop): (1, 1372), + 0x12B5: (48, 32), + 0x12B8: (44, 44), + } + for img in tex_data: + dim_x = tex_data[img][0] + dim_y = tex_data[img][1] + img_output = getImageFile(25, img, True, dim_x, dim_y, TextureFormat.RGBA5551) + img_output = maskImageWithColor(img_output, barrel_color) + writeColorImageToROM(img_output, 25, img, dim_x, dim_y, False, TextureFormat.RGBA5551) + + +def applyCelebrationRims(hue_shift: int, enabled_bananas: list[bool] = [False, False, False, False, False]): + """Retexture the warp pad rims to have a more celebratory tone.""" + banana_textures = [] + vanilla_banana_textures = [0xA8, 0x98, 0xE8, 0xD0, 0xF0] + for kong_index, ban in enumerate(enabled_bananas): + if ban: + banana_textures.append(vanilla_banana_textures[kong_index]) + place_bananas = False + if len(banana_textures) > 0: + place_bananas = True + if len(banana_textures) < 4: + banana_textures = (banana_textures * 4)[:4] + if place_bananas: + bananas = [getImageFile(7, x, False, 44, 44, TextureFormat.RGBA5551).resize((14, 14)) for x in banana_textures] + banana_placement = [ + # File, x, y + [0xBB3, 15, 1], # 3 + [0xBB2, 2, 1], # 2 + [0xBB3, 0, 1], # 4 + [0xBB2, 17, 1], # 1 + ] + for img in (0xBB2, 0xBB3): + side_im = getImageFile(25, img, True, 32, 16, TextureFormat.RGBA5551) + hueShift(side_im, hue_shift) + if place_bananas: + for bi, banana in enumerate(bananas): + if banana_placement[bi][0] == img: + b_x = banana_placement[bi][1] + b_y = banana_placement[bi][2] + side_im.paste(banana, (b_x, b_y), banana) + side_by = [] + side_px = side_im.load() + for y in range(16): + for x in range(32): + red_short = (side_px[x, y][0] >> 3) & 31 + green_short = (side_px[x, y][1] >> 3) & 31 + blue_short = (side_px[x, y][2] >> 3) & 31 + alpha_short = 1 if side_px[x, y][3] > 128 else 0 + value = (red_short << 11) | (green_short << 6) | (blue_short << 1) | alpha_short + side_by.extend([(value >> 8) & 0xFF, value & 0xFF]) + px_data = bytearray(side_by) + px_data = gzip.compress(px_data, compresslevel=9) + ROM().seek(js.pointer_addresses[25]["entries"][img]["pointing_to"]) + ROM().writeBytes(px_data) + + +def applyHolidayMode(settings): + """Change grass texture to snow.""" + HOLIDAY = getHoliday(settings) + if HOLIDAY == Holidays.no_holiday: + changeBarrelColor() # Fixes some Krusha stuff + return + if HOLIDAY == Holidays.Christmas: + # Set season to Christmas + ROM().seek(settings.rom_data + 0xDB) + ROM().writeMultipleBytes(2, 1) + # Grab Snow texture, transplant it + ROM().seek(0x1FF8000) + snow_im = Image.new(mode="RGBA", size=((32, 32))) + snow_px = snow_im.load() + snow_by = [] + for y in range(32): + for x in range(32): + rgba_px = int.from_bytes(ROM().readBytes(2), "big") + red = ((rgba_px >> 11) & 31) << 3 + green = ((rgba_px >> 6) & 31) << 3 + blue = ((rgba_px >> 1) & 31) << 3 + alpha = (rgba_px & 1) * 255 + snow_px[x, y] = (red, green, blue, alpha) + for dim in (32, 16, 8, 4): + snow_im = snow_im.resize((dim, dim)) + px = snow_im.load() + for y in range(dim): + for x in range(dim): + rgba_data = list(px[x, y]) + data = 0 + for c in range(3): + data |= (rgba_data[c] >> 3) << (1 + (5 * c)) + if rgba_data[3] != 0: + data |= 1 + snow_by.extend([(data >> 8), (data & 0xFF)]) + byte_data = gzip.compress(bytearray(snow_by), compresslevel=9) + for img in (0x4DD, 0x4E4, 0x6B, 0xF0, 0x8B2, 0x5C2, 0x66E, 0x66F, 0x685, 0x6A1, 0xF8, 0x136): + start = js.pointer_addresses[25]["entries"][img]["pointing_to"] + ROM().seek(start) + ROM().writeBytes(byte_data) + # Alter CI4 Palettes + start = js.pointer_addresses[25]["entries"][2007]["pointing_to"] + mags = [140, 181, 156, 181, 222, 206, 173, 230, 255, 255, 255, 189, 206, 255, 181, 255] + new_ci4_palette = [] + for mag in mags: + comp_mag = mag >> 3 + data = (comp_mag << 11) | (comp_mag << 6) | (comp_mag << 1) | 1 + new_ci4_palette.extend([(data >> 8), (data & 0xFF)]) + byte_data = gzip.compress(bytearray(new_ci4_palette), compresslevel=9) + ROM().seek(start) + ROM().writeBytes(byte_data) + # Alter rims + applyCelebrationRims(50, [True, True, True, True, False]) + # Change DK's Tie and Tiny's Hair + if settings.dk_tie_colors != CharacterColors.custom and settings.kong_model_dk == KongModels.default: + tie_hang = [0xFF] * 0xAB8 + tie_hang_data = gzip.compress(bytearray(tie_hang), compresslevel=9) + ROM().seek(js.pointer_addresses[25]["entries"][0xE8D]["pointing_to"]) + ROM().writeBytes(tie_hang_data) + tie_loop = [0xFF] * (32 * 32 * 2) + tie_loop_data = gzip.compress(bytearray(tie_loop), compresslevel=9) + ROM().seek(js.pointer_addresses[25]["entries"][0x177D]["pointing_to"]) + ROM().writeBytes(tie_loop_data) + if settings.tiny_hair_colors != CharacterColors.custom and settings.kong_model_tiny == KongModels.default: + tiny_hair = [] + for x in range(32 * 32): + tiny_hair.extend([0xF8, 0x01]) + tiny_hair_data = gzip.compress(bytearray(tiny_hair), compresslevel=9) + ROM().seek(js.pointer_addresses[25]["entries"][0xE68]["pointing_to"]) + ROM().writeBytes(tiny_hair_data) + # Tag Barrel, Bonus Barrel & Transform Barrels + changeBarrelColor(None, (0x00, 0xC0, 0x00)) + elif HOLIDAY == Holidays.Halloween: + ROM().seek(settings.rom_data + 0xDB) + ROM().writeMultipleBytes(1, 1) + # Pad Rim + applyCelebrationRims(-12) + # Tag Barrel, Bonus Barrel & Transform Barrels + changeBarrelColor((0x00, 0xC0, 0x00)) + # Turn Ice Tomato Orange + sizes = { + 0x1237: 700, + 0x1238: 1404, + 0x1239: 1372, + 0x123A: 1372, + 0x123B: 692, + 0x123C: 1372, + 0x123D: 1372, + 0x123E: 1372, + 0x123F: 1372, + 0x1240: 1372, + 0x1241: 1404, + } + for img in range(0x1237, 0x1241 + 1): + hueShiftImageContainer(25, img, 1, sizes[img], TextureFormat.RGBA5551, 240) + elif HOLIDAY == Holidays.Anniv25: + changeBarrelColor((0xFF, 0xFF, 0x00), None, True) + sticker_im = getImageFile(25, getBonusSkinOffset(ExtraTextures.Anniv25Sticker), True, 1, 1372, TextureFormat.RGBA5551) + vanilla_sticker_im = getImageFile(25, 0xB7D, True, 1, 1372, TextureFormat.RGBA5551) + sticker_im_snipped = sticker_im.crop((0, 0, 1, 1360)) + writeColorImageToROM(sticker_im_snipped, 25, 0xB7D, 1, 1360, False, TextureFormat.RGBA5551) + vanilla_sticker_portion = vanilla_sticker_im.crop((0, 1360, 1, 1372)) + new_im = Image.new(mode="RGBA", size=(1, 1372)) + new_im.paste(sticker_im_snipped, (0, 0), sticker_im_snipped) + new_im.paste(vanilla_sticker_portion, (0, 1360), vanilla_sticker_portion) + writeColorImageToROM(new_im, 25, 0x1266, 1, 1372, False, TextureFormat.RGBA5551) + applyCelebrationRims(0, [False, True, True, True, True]) \ No newline at end of file diff --git a/randomizer/Patching/Cosmetics/Krusha.py b/randomizer/Patching/Cosmetics/Krusha.py new file mode 100644 index 000000000..3652dca9d --- /dev/null +++ b/randomizer/Patching/Cosmetics/Krusha.py @@ -0,0 +1,181 @@ +import js +import zlib +import gzip +from typing import TYPE_CHECKING +from randomizer.Settings import Settings +from randomizer.Enums.Settings import ColorblindMode +from randomizer.Patching.Lib import TableNames, getObjectAddress, float_to_hex, intf_to_float, int_to_list +from randomizer.Patching.Patcher import LocalROM +from randomizer.Patching.LibImage import ( + writeColorImageToROM, + getImageFile, + getBonusSkinOffset, + ExtraTextures, + TextureFormat, +) +from randomizer.Enums.Kongs import Kongs + +if TYPE_CHECKING: + from PIL.Image import Image + +DK_SCALE = 0.75 +GENERIC_SCALE = 0.49 +krusha_scaling = [ + # [x, y, z, xz, y] + # DK + [ + lambda x: x * DK_SCALE, + lambda x: x * DK_SCALE, + lambda x: x * GENERIC_SCALE, + lambda x: x * DK_SCALE, + lambda x: x * DK_SCALE, + ], + # Diddy + [ + lambda x: (x * 1.043) - 41.146, + lambda x: (x * 9.893) - 8.0, + lambda x: x * GENERIC_SCALE, + lambda x: (x * 1.103) - 14.759, + lambda x: (x * 0.823) + 35.220, + ], + # Lanky + [ + lambda x: (x * 0.841) - 17.231, + lambda x: (x * 6.925) - 2.0, + lambda x: x * GENERIC_SCALE, + lambda x: (x * 0.680) - 18.412, + lambda x: (x * 0.789) + 42.138, + ], + # Tiny + [ + lambda x: (x * 0.632) + 7.590, + lambda x: (x * 6.925) + 0.0, + lambda x: x * GENERIC_SCALE, + lambda x: (x * 1.567) - 21.676, + lambda x: (x * 0.792) + 41.509, + ], + # Chunky + [lambda x: x, lambda x: x, lambda x: x, lambda x: x, lambda x: x], +] + +def readListAsInt(arr: list, start: int, size: int) -> int: + """Read list and convert to int.""" + val = 0 + for i in range(size): + val = (val * 256) + arr[start + i] + return val + +kong_index_mapping = { + # Regular model, instrument model + Kongs.donkey: (3, None), + Kongs.diddy: (0, 1), + Kongs.lanky: (5, 6), + Kongs.tiny: (8, 9), + Kongs.chunky: (11, 12), +} + +def fixModelSmallKongCollision(kong_index: int): + """Modify Krusha Model to be smaller to enable him to fit through smaller gaps.""" + for x in range(2): + file = kong_index_mapping[kong_index][x] + if file is None: + continue + krusha_model_start = js.pointer_addresses[5]["entries"][file]["pointing_to"] + krusha_model_finish = js.pointer_addresses[5]["entries"][file + 1]["pointing_to"] + krusha_model_size = krusha_model_finish - krusha_model_start + ROM_COPY = LocalROM() + ROM_COPY.seek(krusha_model_start) + indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") + ROM_COPY.seek(krusha_model_start) + data = ROM_COPY.readBytes(krusha_model_size) + if indicator == 0x1F8B: + data = zlib.decompress(data, (15 + 32)) + num_data = [] # data, but represented as nums rather than b strings + for d in data: + num_data.append(d) + head = readListAsInt(num_data, 0, 4) + ptr = readListAsInt(num_data, 0xC, 4) + base = (ptr - head) + 0x28 + 8 + count_0 = readListAsInt(num_data, base, 4) + changes = krusha_scaling[kong_index][:3] + changes_0 = [ + krusha_scaling[kong_index][3], + krusha_scaling[kong_index][4], + krusha_scaling[kong_index][3], + ] + for i in range(count_0): + i_start = base + 4 + (i * 0x14) + for coord_index, change in enumerate(changes): + val_i = readListAsInt(num_data, i_start + (4 * coord_index) + 4, 4) + val_f = change(intf_to_float(val_i)) + val_i = int(float_to_hex(val_f), 16) + for di, d in enumerate(int_to_list(val_i, 4)): + num_data[i_start + (4 * coord_index) + 4 + di] = d + section_2_start = base + 4 + (count_0 * 0x14) + count_1 = readListAsInt(num_data, section_2_start, 4) + for i in range(count_1): + i_start = section_2_start + 4 + (i * 0x10) + for coord_index, change in enumerate(changes_0): + val_i = readListAsInt(num_data, i_start + (4 * coord_index), 4) + val_f = change(intf_to_float(val_i)) + val_i = int(float_to_hex(val_f), 16) + for di, d in enumerate(int_to_list(val_i, 4)): + num_data[i_start + (4 * coord_index) + di] = d + data = bytearray(num_data) # convert num_data back to binary string + if indicator == 0x1F8B: + data = gzip.compress(data, compresslevel=9) + LocalROM().seek(krusha_model_start) + LocalROM().writeBytes(data) + +def fixBaboonBlasts(): + """Fix various baboon blasts to work for Krusha.""" + # Fungi Baboon Blast + ROM_COPY = LocalROM() + for id in (2, 5): + item_start = getObjectAddress(0xBC, id, "actor") + if item_start is not None: + ROM_COPY.seek(item_start + 0x14) + ROM_COPY.writeMultipleBytes(0xFFFFFFEC, 4) + ROM_COPY.seek(item_start + 0x1B) + ROM_COPY.writeMultipleBytes(0, 1) + # Caves Baboon Blast + item_start = getObjectAddress(0xBA, 4, "actor") + if item_start is not None: + ROM_COPY.seek(item_start + 0x4) + ROM_COPY.writeMultipleBytes(int(float_to_hex(510), 16), 4) + item_start = getObjectAddress(0xBA, 12, "actor") + if item_start is not None: + ROM_COPY.seek(item_start + 0x4) + ROM_COPY.writeMultipleBytes(int(float_to_hex(333), 16), 4) + # Castle Baboon Blast + item_start = getObjectAddress(0xBB, 4, "actor") + if item_start is not None: + ROM_COPY.seek(item_start + 0x0) + ROM_COPY.writeMultipleBytes(int(float_to_hex(2472), 16), 4) + ROM_COPY.seek(item_start + 0x8) + ROM_COPY.writeMultipleBytes(int(float_to_hex(1980), 16), 4) + +def placeKrushaHead(settings: Settings, slot): + """Replace a kong's face with the Krusha face.""" + if settings.colorblind_mode != ColorblindMode.off: + return + + kong_face_textures = [[0x27C, 0x27B], [0x279, 0x27A], [0x277, 0x278], [0x276, 0x275], [0x273, 0x274]] + unc_face_textures = [[579, 586], [580, 587], [581, 588], [582, 589], [577, 578]] + krushaFace64 = getImageFile(TableNames.TexturesGeometry, getBonusSkinOffset(ExtraTextures.KrushaFace1 + slot), True, 64, 64, TextureFormat.RGBA5551) + krushaFace64Left = krushaFace64.crop([0, 0, 32, 64]) + krushaFace64Right = krushaFace64.crop([32, 0, 64, 64]) + # Used in File Select, Pause Menu, Tag Barrels, Switches, Transformation Barrels + writeColorImageToROM(krushaFace64Left, 25, kong_face_textures[slot][0], 32, 64, False, TextureFormat.RGBA5551) + writeColorImageToROM(krushaFace64Right, 25, kong_face_textures[slot][1], 32, 64, False, TextureFormat.RGBA5551) + # Used in Troff and Scoff + writeColorImageToROM(krushaFace64Left, 7, unc_face_textures[slot][0], 32, 64, False, TextureFormat.RGBA5551) + writeColorImageToROM(krushaFace64Right, 7, unc_face_textures[slot][1], 32, 64, False, TextureFormat.RGBA5551) + + krushaFace32 = krushaFace64.resize((32, 32)) + krushaFace32 = krushaFace32.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + krushaFace32RBGA32 = getImageFile(TableNames.TexturesGeometry, getBonusSkinOffset(ExtraTextures.KrushaFace321 + slot), True, 32, 32, TextureFormat.RGBA32) + # Used in the DPad Selection Menu + writeColorImageToROM(krushaFace32, 14, 190 + slot, 32, 32, False, TextureFormat.RGBA5551) + # Used in Shops Previews + writeColorImageToROM(krushaFace32RBGA32, 14, 197 + slot, 32, 32, False, TextureFormat.RGBA32) \ No newline at end of file diff --git a/randomizer/Patching/Cosmetics/ModelSwaps.py b/randomizer/Patching/Cosmetics/ModelSwaps.py new file mode 100644 index 000000000..b466651c1 --- /dev/null +++ b/randomizer/Patching/Cosmetics/ModelSwaps.py @@ -0,0 +1,333 @@ +from randomizer.Enums.Models import Model, Sprite + +turtle_models = [ + Model.Diddy, # Diddy + Model.DK, # DK + Model.Lanky, # Lanky + Model.Tiny, # Tiny + Model.Chunky, # Regular Chunky + Model.ChunkyDisco, # Disco Chunky + Model.Cranky, # Cranky + Model.Funky, # Funky + Model.Candy, # Candy + Model.Seal, # Seal + Model.Enguarde, # Enguarde + Model.BeaverBlue_LowPoly, # Beaver + Model.Squawks_28, # Squawks + Model.KlaptrapGreen, # Klaptrap Green + Model.KlaptrapPurple, # Klaptrap Purple + Model.KlaptrapRed, # Klaptrap Red + Model.KlaptrapTeeth, # Klaptrap Teeth + Model.SirDomino, # Sir Domino + Model.MrDice_41, # Mr Dice + Model.Beetle, # Beetle + Model.NintendoLogo, # N64 Logo + Model.MechanicalFish, # Mech Fish + Model.ToyCar, # Toy Car + Model.BananaFairy, # Fairy + Model.Shuri, # Starfish + Model.Gimpfish, # Gimpfish + Model.Spider, # Spider + Model.Rabbit, # Rabbit + Model.KRoolCutscene, # K Rool + Model.SkeletonHead, # Skeleton Head + Model.Vulture_76, # Vulture + Model.Vulture_77, # Racing Vulture + Model.Tomato, # Tomato + Model.Fly, # Fly + Model.SpotlightFish, # Spotlight Fish + Model.Puftup, # Pufftup + Model.CuckooBird, # Cuckoo Bird + Model.IceTomato, # Ice Tomato + Model.Boombox, # Boombox + Model.KRoolFight, # K Rool (Boxing) + Model.Microphone, # Microbuffer + Model.DeskKRool, # K Rool's Desk + Model.Bell, # Bell + Model.BonusBarrel, # Bonus Barrel + Model.HunkyChunkyBarrel, # HC Barrel + Model.MiniMonkeyBarrel, # MM Barrel + Model.TNTBarrel, # TNT Barrel + Model.Rocketbarrel, # RB Barrel + Model.StrongKongBarrel, # SK Barrel + Model.OrangstandSprintBarrel, # OSS Barrel + Model.BBBSlot_143, # BBB Slot + Model.PlayerCar, # Tiny Car + Model.Boulder, # Boulder + Model.Boat_158, # Boat + Model.Potion, # Potion + Model.ArmyDilloMissle, # AD Missile + Model.TagBarrel, # Tag Barrel + Model.QuestionMark, # Question Mark + Model.Krusha, # Krusha + Model.BananaPeel, # Banana Peel + Model.Butterfly, # Butterfly + Model.FunkyGun, # Funky's Gun +] + +panic_models = [ + Model.Diddy, # Diddy + Model.DK, # DK + Model.Lanky, # Lanky + Model.Tiny, # Tiny + Model.Chunky, # Regular Chunky + Model.ChunkyDisco, # Disco Chunky + Model.Cranky, # Cranky + Model.Funky, # Funky + Model.Candy, # Candy + Model.Seal, # Seal + Model.Enguarde, # Enguarde + Model.BeaverBlue_LowPoly, # Beaver + Model.Squawks_28, # Squawks + Model.KlaptrapGreen, # Klaptrap Green + Model.KlaptrapPurple, # Klaptrap Purple + Model.KlaptrapRed, # Klaptrap Red + Model.MadJack, # Mad Jack + Model.Troff, # Troff + Model.SirDomino, # Sir Domino + Model.MrDice_41, # Mr Dice + Model.RoboKremling, # Robo Kremling + Model.Scoff, # Scoff + Model.Beetle, # Beetle + Model.NintendoLogo, # N64 Logo + Model.MechanicalFish, # Mech Fish + Model.ToyCar, # Toy Car + Model.Klump, # Klump + Model.Dogadon, # Dogadon + Model.BananaFairy, # Fairy + Model.Guard, # Guard + Model.Shuri, # Starfish + Model.Gimpfish, # Gimpfish + Model.KLumsy, # K Lumsy + Model.Spider, # Spider + Model.Rabbit, # Rabbit + # Model.Beanstalk, # Beanstalk + Model.KRoolCutscene, # K Rool + Model.SkeletonHead, # Skeleton Head + Model.Vulture_76, # Vulture + Model.Vulture_77, # Racing Vulture + Model.Ghost, # Ghost + Model.Fly, # Fly + Model.FlySwatter_83, # Fly Swatter + Model.Owl, # Owl + Model.Book, # Book + Model.SpotlightFish, # Spotlight Fish + Model.Puftup, # Pufftup + Model.Mermaid, # Mermaid + Model.Mushroom, # Mushroom Man + Model.Worm, # Worm + Model.EscapeShip, # Escape Ship + Model.KRoolFight, # K Rool (Boxing) + Model.Microphone, # Microbuffer + Model.BonusBarrel, # Bonus Barrel + Model.HunkyChunkyBarrel, # HC Barrel + Model.MiniMonkeyBarrel, # MM Barrel + Model.TNTBarrel, # TNT Barrel + Model.Rocketbarrel, # RB Barrel + Model.StrongKongBarrel, # SK Barrel + Model.OrangstandSprintBarrel, # OSS Barrel + Model.PlayerCar, # Tiny Car + Model.Boulder, # Boulder + Model.VaseCircle, # Vase + Model.VaseColon, # Vase + Model.VaseTriangle, # Vase + Model.VasePlus, # Vase + Model.ArmyDilloMissle, # AD Missile + Model.TagBarrel, # Tag Barrel + Model.QuestionMark, # Question Mark + Model.Krusha, # Krusha + Model.Light, # Light + Model.BananaPeel, # Banana Peel + Model.FunkyGun, # Funky's Gun +] + +bother_models = [ + Model.BeaverBlue_LowPoly, # Beaver + Model.Klobber, # Klobber + Model.Kaboom, # Kaboom + Model.KlaptrapGreen, # Green Klap + Model.KlaptrapPurple, # Purple Klap + Model.KlaptrapRed, # Red Klap + Model.KlaptrapTeeth, # Klap Teeth + Model.Krash, # Krash + Model.Troff, # Troff + Model.NintendoLogo, # N64 Logo + Model.MechanicalFish, # Mech Fish + Model.Krossbones, # Krossbones + Model.Rabbit, # Rabbit + Model.SkeletonHead, # Minecart Skeleton Head + Model.Tomato, # Tomato + Model.IceTomato, # Ice Tomato + Model.GoldenBanana_104, # Golden Banana + Model.Microphone, # Microbuffer + Model.Bell, # Bell + Model.Missile, # Missile (Car Race) + Model.Buoy, # Red Buoy + Model.BuoyGreen, # Green Buoy + Model.RarewareLogo, # Rareware Logo +] + +piano_models = [ + Model.Krash, + Model.RoboKremling, + Model.KoshKremling, + Model.KoshKremlingRed, + Model.Kasplat, + Model.Guard, + Model.Krossbones, + Model.Mermaid, + Model.Mushroom, + Model.GoldenBanana_104, + Model.FlySwatter_83, + Model.Ruler, +] +piano_extreme_model = [ + Model.SkeletonHead, + Model.Owl, + Model.Kosha, + # Model.Beanstalk, +] + +spotlight_fish_models = [ + # Model.Turtle, # Lighting Bug + Model.Seal, + Model.BeaverBlue, + Model.BeaverGold, + Model.Zinger, + Model.Squawks_28, + Model.Klobber, + Model.Kaboom, + Model.KlaptrapGreen, + Model.KlaptrapPurple, + Model.KlaptrapRed, + Model.Krash, + # Model.SirDomino, # Lighting issue + # Model.MrDice_41, # Lighting issue + # Model.Ruler, # Lighting issue + # Model.RoboKremling, # Lighting issue + Model.NintendoLogo, + Model.MechanicalFish, + Model.ToyCar, + Model.Kasplat, + Model.BananaFairy, + Model.Guard, + Model.Gimpfish, + # Model.Shuri, # Lighting issue + Model.Spider, + Model.Rabbit, + Model.KRoolCutscene, + Model.KRoolFight, + # Model.SkeletonHead, # Lighting bug + # Model.Vulture_76, # Lighting bug + # Model.Vulture_77, # Lighting bug + # Model.Bat, # Lighting bug + # Model.Tomato, # Lighting bug + # Model.IceTomato, # Lighting bug + # Model.FlySwatter_83, # Lighting bug + Model.SpotlightFish, + Model.Microphone, + # Model.Rocketbarrel, # Model too big, obstructs view + # Model.StrongKongBarrel, # Model too big, obstructs view + # Model.OrangstandSprintBarrel, # Model too big, obstructs view + # Model.MiniMonkeyBarrel, # Model too big, obstructs view + # Model.HunkyChunkyBarrel, # Model too big, obstructs view +] +candy_cutscene_models = [ + Model.Cranky, + # Model.Funky, # Disappears with collision + Model.Candy, + Model.Snide, + Model.Seal, + Model.BeaverBlue, + Model.BeaverGold, + Model.Klobber, + Model.Kaboom, + Model.Krash, + Model.Troff, + Model.Scoff, + Model.RoboKremling, + Model.Beetle, + Model.MrDice_41, + Model.MrDice_56, + Model.BananaFairy, + Model.Rabbit, + Model.KRoolCutscene, + Model.KRoolFight, + Model.Vulture_76, + Model.Vulture_77, + Model.Tomato, + Model.IceTomato, + Model.FlySwatter_83, + Model.Microphone, + Model.StrongKongBarrel, + Model.Rocketbarrel, + Model.OrangstandSprintBarrel, + Model.MiniMonkeyBarrel, + Model.HunkyChunkyBarrel, + Model.RambiCrate, + Model.EnguardeCrate, + Model.Boulder, + Model.SteelKeg, + Model.GoldenBanana_104, +] + +funky_cutscene_models = [ + Model.Cranky, + Model.Candy, + Model.Funky, + Model.Troff, + Model.Scoff, + Model.Ruler, + Model.RoboKremling, + Model.KRoolCutscene, + Model.KRoolFight, + Model.Microphone, +] + +# Not holding gun +funky_cutscene_models_extreme = [ + Model.BeaverBlue, + Model.BeaverGold, + Model.Klobber, + Model.Kaboom, + Model.SirDomino, + Model.MechanicalFish, + Model.BananaFairy, + Model.SkeletonHand, + Model.IceTomato, + Model.Tomato, +] + +boot_cutscene_models = [ + Model.Turtle, + Model.Enguarde, + Model.BeaverBlue, + Model.BeaverGold, + Model.Zinger, + Model.Squawks_28, + Model.KlaptrapGreen, + Model.KlaptrapPurple, + Model.KlaptrapRed, + Model.BananaFairy, + Model.Spider, + Model.Bat, + Model.KRoolGlove, +] + +melon_random_sprites = [ + Sprite.BouncingMelon, + Sprite.BouncingOrange, + Sprite.Coconut, + Sprite.Peanut, + Sprite.Grape, + Sprite.Feather, + Sprite.Pineapple, + Sprite.CrystalCoconut0, + Sprite.DKCoin, + Sprite.DiddyCoin, + Sprite.LankyCoin, + Sprite.TinyCoin, + Sprite.ChunkyCoin, + Sprite.Fairy, + Sprite.RaceCoin, +] \ No newline at end of file diff --git a/randomizer/Patching/Cosmetics/Puzzles.py b/randomizer/Patching/Cosmetics/Puzzles.py new file mode 100644 index 000000000..cd8d8df70 --- /dev/null +++ b/randomizer/Patching/Cosmetics/Puzzles.py @@ -0,0 +1,122 @@ +from typing import TYPE_CHECKING + +from randomizer.Settings import Settings +from randomizer.Patching.LibImage import writeColorImageToROM, TextureFormat, getImageFile, getNumberImage + +if TYPE_CHECKING: + from PIL.Image import Image + +def updateMillLeverTexture(settings: Settings) -> None: + """Update the 21132 texture.""" + if settings.mill_levers[0] > 0: + # Get Number bounds + base_num_texture = getImageFile(table_index=25, file_index=0x7CA, compressed=True, width=64, height=32, format=TextureFormat.RGBA5551) + number_textures = [None, None, None] + number_x_bounds = ( + (18, 25), + (5, 16), + (36, 47), + ) + modified_tex = getImageFile(table_index=25, file_index=0x7CA, compressed=True, width=64, height=32, format=TextureFormat.RGBA5551) + for tex in range(3): + number_textures[tex] = base_num_texture.crop((number_x_bounds[tex][0], 7, number_x_bounds[tex][1], 25)) + total_width = 0 + for x in range(5): + if settings.mill_levers[x] > 0: + idx = settings.mill_levers[x] - 1 + total_width += number_x_bounds[idx][1] - number_x_bounds[idx][0] + # Overwrite old panel + overwrite_panel = Image.new(mode="RGBA", size=(58, 26), color=(131, 65, 24)) + modified_tex.paste(overwrite_panel, (3, 3), overwrite_panel) + # Generate new number texture + new_num_texture = Image.new(mode="RGBA", size=(total_width, 18)) + x_pos = 0 + for num in range(5): + if settings.mill_levers[num] > 0: + num_val = settings.mill_levers[num] - 1 + new_num_texture.paste(number_textures[num_val], (x_pos, 0), number_textures[num_val]) + x_pos += number_x_bounds[num_val][1] - number_x_bounds[num_val][0] + scale_x = 58 / total_width + scale_y = 26 / 18 + scale = min(scale_x, scale_y) + x_size = int(total_width * scale) + y_size = int(18 * scale) + new_num_texture = new_num_texture.resize((x_size, y_size)) + x_offset = int((58 - x_size) / 2) + modified_tex.paste(new_num_texture, (3 + x_offset, 3), new_num_texture) + writeColorImageToROM(modified_tex, 25, 0x7CA, 64, 32, False, TextureFormat.RGBA5551) + + +def updateDiddyDoors(settings: Settings): + """Update the textures for the doors.""" + enable_code = False + for code in settings.diddy_rnd_doors: + if sum(code) > 0: # Has a non-zero element + enable_code = True + SEG_WIDTH = 48 + SEG_HEIGHT = 42 + NUMBERS_START = (27, 33) + if enable_code: + # Order: 4231, 3124, 1342 + starts = (0xCE8, 0xCE4, 0xCE0) + for index, code in enumerate(settings.diddy_rnd_doors): + start = starts[index] + total = Image.new(mode="RGBA", size=(SEG_WIDTH * 2, SEG_HEIGHT * 2)) + for img_index in range(4): + img = getImageFile(25, start + img_index, True, SEG_WIDTH, SEG_HEIGHT, TextureFormat.RGBA5551) + x_offset = SEG_WIDTH * (img_index & 1) + y_offset = SEG_HEIGHT * ((img_index & 2) >> 1) + total.paste(img, (x_offset, y_offset), img) + total = total.transpose(Image.FLIP_TOP_BOTTOM) + # Overlay color + cover = Image.new(mode="RGBA", size=(42, 20), color=(115, 98, 65)) + total.paste(cover, NUMBERS_START, cover) + # Paste numbers + number_images = [] + number_offsets = [] + total_length = 0 + for num in code: + num_img = getNumberImage(num + 1) + w, h = num_img.size + number_offsets.append(total_length) + total_length += w + number_images.append(num_img) + total_numbers = Image.new(mode="RGBA", size=(total_length, 24)) + for img_index, img in enumerate(number_images): + total_numbers.paste(img, (number_offsets[img_index], 0), img) + total.paste(total_numbers, (SEG_WIDTH - int(total_length / 2), SEG_HEIGHT - 12), total_numbers) + total = total.transpose(Image.FLIP_TOP_BOTTOM) + for img_index in range(4): + x_offset = SEG_WIDTH * (img_index & 1) + y_offset = SEG_HEIGHT * ((img_index & 2) >> 1) + sub_img = total.crop((x_offset, y_offset, x_offset + SEG_WIDTH, y_offset + SEG_HEIGHT)) + writeColorImageToROM(sub_img, 25, start + img_index, SEG_WIDTH, SEG_HEIGHT, False, TextureFormat.RGBA5551) + + +def updateCryptLeverTexture(settings: Settings) -> None: + """Update the two textures for Donkey Minecart entry.""" + if settings.crypt_levers[0] > 0: + # Get a blank texture + texture_0 = getImageFile(table_index=25, file_index=0x999, compressed=True, width=32, height=64, format=TextureFormat.RGBA5551) + blank = texture_0.crop((8, 5, 23, 22)) + texture_0.paste(blank, (8, 42), blank) + texture_1 = texture_0.copy() + for xi, x in enumerate(settings.crypt_levers): + corrected = x - 1 + y_slot = corrected % 3 + num = getNumberImage(xi + 1) + num = num.transpose(Image.FLIP_TOP_BOTTOM) + w, h = num.size + scale = 2 / 3 + y_offset = int((h * scale) / 2) + x_offset = int((w * scale) / 2) + num = num.resize((int(w * scale), int(h * scale))) + y_pos = (51, 33, 14) + tl_y = y_pos[y_slot] - y_offset + tl_x = 16 - x_offset + if corrected < 3: + texture_0.paste(num, (tl_x, tl_y), num) + else: + texture_1.paste(num, (tl_x, tl_y), num) + writeColorImageToROM(texture_0, 25, 0x99A, 32, 64, False, TextureFormat.RGBA5551) + writeColorImageToROM(texture_1, 25, 0x999, 32, 64, False, TextureFormat.RGBA5551) \ No newline at end of file diff --git a/randomizer/Patching/Cosmetics/TextRando.py b/randomizer/Patching/Cosmetics/TextRando.py new file mode 100644 index 000000000..aba25b0d3 --- /dev/null +++ b/randomizer/Patching/Cosmetics/TextRando.py @@ -0,0 +1,315 @@ +import random +from randomizer.Patching.Patcher import LocalROM +from randomizer.Patching.Lib import writeText, grabText + +boot_phrases = ( + "Removing Lanky Kong", + "Telling 2dos to play DK64", + "Locking K. Lumsy in a cage", + "Stealing the Banana Hoard", + "Finishing the game in a cave", + "Becoming the peak of randomizers", + "Giving kops better eyesight", + "Patching in the glitches", + "Enhancing Cfox Luck", + "Finding Rareware GB in Galleon", + "Resurrecting Chunky Kong", + "Shouting out Grant Kirkhope", + "Crediting L. Godfrey", + "Removing Stop n Swop", + "Assembling the scraps", + "Blowing in the cartridge", + "Backflipping in Chunky Phase", + "Hiding 20 fairies", + "Randomizing collision normals", + "Removing hit detection", + "Compressing K Rools Voice Lines", + "Checking divide by 0 doesnt work", + "Adding every move to Isles", + "Segueing in dk64randomizer.com", + "Removing lag. Or am I?", + "Hiding a dirt patch under grass", + "Giving Wrinkly the spoiler log", + "Questioning sub 2:30 in LUA Rando", + "Chasing Lanky in Fungi Forest", + "Banning Potions from Candys Shop", + "Finding someone who can help you", + "Messing up your seed", + "Crashing Krem Isle", + "Increasing Robot Punch Resistance", + "Caffeinating banana fairies", + "Bothering Beavers", + "Inflating Banana Balloons", + "Counting to 16", + "Removing Walls", + "Taking it to the fridge", + "Brewing potions", + "Reticulating Splines", # SimCity 2000 + "Ironing Donks", + "Replacing mentions of Hero with Hoard", + "Suggesting you also try BK Randomizer", + "Scattering 3500 Bananas", + "Stealing ideas from other randomizers", + "Fixing Krushas Collision", + "Falling on 75m", + "Summoning Salt", + "Combing Chunkys Afro", + "Asking what you gonna do", + "Thinking with portals", + "Reminding you to hydrate", + "Injecting lag", + "Turning Sentient", + "Performing for you", + "Charging 2 coins per save", + "Loading in Beavers", + "Lifting Boulders with Relative Ease", + "Doing Monkey Science Probably", + "Telling Killi to eventually play DK64", + "Crediting Grant Kirkhope", + "Dropping Crayons", + "Saying Hello when others wont", + "Mangling Music", + "Killing Speedrunning", + "Enhancing Cfox Luck Voice Linesmizers", + "Enforcing the law of the Jungle", + "Saving 20 frames", + "Reporting bugs. Unlike some", + "Color-coding Krusha for convenience", +) + +crown_heads = ( + # Object + "Arena", + "Beaver", + "Bish Bash", + "Forest", + "Kamikaze", + "Kritter", + "Pinnacle", + "Plinth", + "Shockwave", + "Bean", + "Dogadon", + "Banana", + "Squawks", + "Lanky", + "Diddy", + "Tiny", + "Chunky", + "DK", + "Krusha", + "Kosha", + "Klaptrap", + "Zinger", + "Gnawty", + "Kasplat", + "Pufftup", + "Shuri", + "Krossbones", + "Caves", + "Castle", + "Helm", + "Japes", + "Jungle", + "Angry", + "Aztec", + "Frantic", + "Factory", + "Gloomy", + "Galleon", + "Crystal", + "Creepy", + "Hideout", + "Cranky", + "Funky", + "Candy", + "Kong", + "Monkey", + "Amazing", + "Incredible", + "Ultimate", + "Wrinkly", + "Heroic", + "Final", + "Fantastic", + "Krazy", + "Komplete", + "Unhinted", + "Unstable", + "Extreme", + "Royal", + "Monster", + "Primate", + "Baboon", + "Walnut", + "Peanut", + "Coconut", + "Feather", + "Grape", + "Pineapple", + "Barrel", + "Monkeyport", + "Kalamity", + "Kaboom", + "Magic", + "Fairy", + "Karnivorous", + "Krispy", + "Kooky", + "Cookin", + "Klutz", + "Kingdom", + "Super Duper", + "Rainbow", + "Bongo", + "Guitar", + "Trombone", + "Saxophone", + "Triangle", + "Dixie", + "Gorilla", + "Chimpy", + "Museum", + "Ballroom", + "Winch", + "Shipyard", + "Hillside", + "Oasis", + "Arcade", + "Mushroom", + "Igloo", + "Stupid", + "Spicy", + "Dizzy", + "Slot Car", + "Minecart", + "Rambi", + "Enguarde", + "Reptile", + "Bramble", + "Toxic", + "Rabbit", + "Beetle", + "Vulture", + "Boulder", +) + +crown_tails = ( + # Synonym for brawl/similar + "Ambush", + "Brawl", + "Fracas", + "Karnage", + "Kremlings", + "Palaver", + "Panic", + "Showdown", + "Slam", + "Melee", + "Tussle", + "Altercation", + "Wrangle", + "Clash", + "Free for All", + "Skirmish", + "Scrap", + "Fight", + "Rumpus", + "Fray", + "Wrestle", + "Brouhaha", + "Commotion", + "Uproar", + "Rough and Tumble", + "Broil", + "Argy Bargy", + "Bother", + "Mayhem", + "Bonanza", + "Battle", + "Kerfuffle", + "Rumble", + "Fisticuffs", + "Ruckus", + "Scrimmage", + "Strife", + "Dog and Duck", + "Joust", + "Scuffle", + "Hootenanny", + "Blitz", + "Tourney", + "Explosion", + "Contest", + "Chaos", + "Combat", + "Knockdown", + "Demolition", + "Capture", + "Storm", + "Earthquake", + "Charge", + "Tremor", + "Trample", + "Gauntlet", + "Challenge", + "Blowout", + "Riot", + "Buffoonery", + "Hijinxs", + "Frenzy", + "Rampage", + "Antics", + "Trouble", + "Revenge", + "Klamber", + "Wreckage", + "Quarrel", + "Feud", + "Thwack", + "Wallop", + "Donnybrook", + "Tangle", + "Crossfire", + "Royale", +) + + +def getCrownNames() -> list: + """Get crown names from head and tail pools.""" + # Get 10 names for heads just in case "Forest" and "Fracas" show up + heads = random.sample(crown_heads, 10) + tails = random.sample(crown_tails, 9) + # Remove "Forest" if both "Forest" and "Fracas" show up + if "Forest" in heads and "Fracas" in tails: + heads.remove("Forest") + # Only get 9 names, Forest Fracas can't be overwritten without having negative impacts + names = [] + for x in range(9): + head = heads[x] + tail = tails[x] + if head[0] == "K" and tail[0] == "C": + split_tail = list(tail) + split_tail[0] = "K" + tail = "".join(split_tail) + names.append(f"{head} {tail}!".upper()) + names.append("Forest Fracas!".upper()) + return names + + +def writeCrownNames(): + """Write Crown Names to ROM.""" + names = getCrownNames() + old_text = grabText(35, True) + for name_index, name in enumerate(names): + old_text[0x1E + name_index] = ({"text": [name]},) + writeText(35, old_text, True) + + +def writeBootMessages() -> None: + """Write boot messages into ROM.""" + ROM_COPY = LocalROM() + placed_messages = random.sample(boot_phrases, 4) + for message_index, message in enumerate(placed_messages): + ROM_COPY.seek(0x1FFD000 + (0x40 * message_index)) + ROM_COPY.writeBytes(message.upper().encode("ascii")) \ No newline at end of file diff --git a/randomizer/Patching/Cosmetics/__init__.py b/randomizer/Patching/Cosmetics/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/randomizer/Patching/LibImage.py b/randomizer/Patching/LibImage.py index 50a49d78b..a046530a7 100644 --- a/randomizer/Patching/LibImage.py +++ b/randomizer/Patching/LibImage.py @@ -6,8 +6,11 @@ import gzip import math from enum import IntEnum, auto -from PIL import Image +from PIL import Image, ImageEnhance from randomizer.Patching.Patcher import ROM, LocalROM +from randomizer.Settings import ColorblindMode +from randomizer.Enums.Kongs import Kongs +from typing import Tuple class TextureFormat(IntEnum): @@ -257,3 +260,219 @@ def imageToCI(ROM_COPY: ROM, im_f, ci_index: int, tex_index: int, pal_index: int ROM_COPY.write(tex_bin_file) ROM_COPY.seek(pal_start) ROM_COPY.write(pal_bin_file) + +def writeColorImageToROM( + im_f, + table_index: int, + file_index: int, + width: int, + height: int, + transparent_border: bool, + format: TextureFormat, +) -> None: + """Write texture to ROM.""" + file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] + file_end = js.pointer_addresses[table_index]["entries"][file_index + 1]["pointing_to"] + file_size = file_end - file_start + try: + LocalROM().seek(file_start) + except Exception: + ROM().seek(file_start) + pix = im_f.load() + width, height = im_f.size + bytes_array = [] + border = 1 + right_border = 3 + for y in range(height): + for x in range(width): + if transparent_border: + if ((x < border) or (y < border) or (x >= (width - border)) or (y >= (height - border))) or (x == (width - right_border)): + pix_data = [0, 0, 0, 0] + else: + pix_data = list(pix[x, y]) + else: + pix_data = list(pix[x, y]) + if format == TextureFormat.RGBA32: + bytes_array.extend(pix_data) + elif format == TextureFormat.RGBA5551: + red = int((pix_data[0] >> 3) << 11) + green = int((pix_data[1] >> 3) << 6) + blue = int((pix_data[2] >> 3) << 1) + alpha = int(pix_data[3] != 0) + value = red | green | blue | alpha + bytes_array.extend([(value >> 8) & 0xFF, value & 0xFF]) + elif format == TextureFormat.IA4: + intensity = pix_data[0] >> 5 + alpha = 0 if pix_data[3] == 0 else 1 + data = ((intensity << 1) | alpha) & 0xF + bytes_array.append(data) + bytes_per_px = 2 + if format == TextureFormat.IA4: + temp_ba = bytes_array.copy() + bytes_array = [] + value_storage = 0 + bytes_per_px = 0.5 + for idx, val in enumerate(temp_ba): + polarity = idx % 2 + if polarity == 0: + value_storage = val << 4 + else: + value_storage |= val + bytes_array.append(value_storage) + data = bytearray(bytes_array) + if format == TextureFormat.RGBA32: + bytes_per_px = 4 + if len(data) > (bytes_per_px * width * height): + print(f"Image too big error: {table_index} > {file_index}") + if table_index in (14, 25): + data = gzip.compress(data, compresslevel=9) + if len(data) > file_size: + print(f"File too big error: {table_index} > {file_index}") + try: + LocalROM().writeBytes(data) + except Exception: + ROM().writeBytes(data) + +def getNumberImage(number: int): + """Get Number Image from number.""" + if number < 5: + num_0_bounds = [0, 20, 30, 45, 58, 76] + x = number + return getImageFile(14, 15, True, 76, 24, TextureFormat.RGBA5551).crop((num_0_bounds[x], 0, num_0_bounds[x + 1], 24)) + num_1_bounds = [0, 15, 28, 43, 58, 76] + x = number - 5 + return getImageFile(14, 16, True, 76, 24, TextureFormat.RGBA5551).crop((num_1_bounds[x], 0, num_1_bounds[x + 1], 24)) + + +def numberToImage(number: int, dim: Tuple[int, int]): + """Convert multi-digit number to image.""" + digits = 1 + if number < 10: + digits = 1 + elif number < 100: + digits = 2 + else: + digits = 3 + current = number + nums = [] + total_width = 0 + max_height = 0 + sep_dist = 1 + for _ in range(digits): + base = getNumberImage(current % 10) + bbox = base.getbbox() + base = base.crop(bbox) + nums.append(base) + base_w, base_h = base.size + max_height = max(max_height, base_h) + total_width += base_w + current = int(current / 10) + nums.reverse() + total_width += (digits - 1) * sep_dist + base = Image.new(mode="RGBA", size=(total_width, max_height)) + pos = 0 + for num in nums: + base.paste(num, (pos, 0), num) + num_w, num_h = num.size + pos += num_w + sep_dist + output = Image.new(mode="RGBA", size=dim) + xScale = dim[0] / total_width + yScale = dim[1] / max_height + scale = xScale + if yScale < xScale: + scale = yScale + new_w = int(total_width * scale) + new_h = int(max_height * scale) + x_offset = int((dim[0] - new_w) / 2) + y_offset = int((dim[1] - new_h) / 2) + new_dim = (new_w, new_h) + base = base.resize(new_dim) + output.paste(base, (x_offset, y_offset), base) + return output + +def getRGBFromHash(hash: str): + """Convert hash RGB code to rgb array.""" + red = int(hash[1:3], 16) + green = int(hash[3:5], 16) + blue = int(hash[5:7], 16) + return [red, green, blue] + +def maskImageWithColor(im_f: Image, mask: tuple): + """Apply rgb mask to image using a rgb color tuple.""" + w, h = im_f.size + converter = ImageEnhance.Color(im_f) + im_f = converter.enhance(0) + im_dupe = im_f.copy() + brightener = ImageEnhance.Brightness(im_dupe) + im_dupe = brightener.enhance(2) + im_f.paste(im_dupe, (0, 0), im_dupe) + pix = im_f.load() + w, h = im_f.size + for x in range(w): + for y in range(h): + base = list(pix[x, y]) + if base[3] > 0: + for channel in range(3): + base[channel] = int(mask[channel] * (base[channel] / 255)) + pix[x, y] = (base[0], base[1], base[2], base[3]) + return im_f + +def getColorBase(mode: ColorblindMode) -> list[str]: + """Get the color base array.""" + if mode == ColorblindMode.prot: + return ["#000000", "#0072FF", "#766D5A", "#FFFFFF", "#FDE400"] + elif mode == ColorblindMode.deut: + return ["#000000", "#318DFF", "#7F6D59", "#FFFFFF", "#E3A900"] + elif mode == ColorblindMode.trit: + return ["#000000", "#C72020", "#13C4D8", "#FFFFFF", "#FFA4A4"] + return ["#FFD700", "#FF0000", "#1699FF", "#B045FF", "#41FF25"] + +def getKongItemColor(mode: ColorblindMode, kong: Kongs) -> str: + """Get the color assigned to a kong.""" + return getColorBase(mode)[kong] + +def maskImage(im_f, base_index, min_y, keep_dark=False, mode = ColorblindMode.off): + """Apply RGB mask to image.""" + w, h = im_f.size + converter = ImageEnhance.Color(im_f) + im_f = converter.enhance(0) + im_dupe = im_f.crop((0, min_y, w, h)) + if keep_dark is False: + brightener = ImageEnhance.Brightness(im_dupe) + im_dupe = brightener.enhance(2) + im_f.paste(im_dupe, (0, min_y), im_dupe) + pix = im_f.load() + mask = getRGBFromHash(getKongItemColor(mode, base_index)) + w, h = im_f.size + for x in range(w): + for y in range(min_y, h): + base = list(pix[x, y]) + if base[3] > 0: + for channel in range(3): + base[channel] = int(mask[channel] * (base[channel] / 255)) + pix[x, y] = (base[0], base[1], base[2], base[3]) + return im_f + +def hueShiftImageContainer(table: int, image: int, width: int, height: int, format: TextureFormat, shift: int): + """Load an image, shift the hue and rewrite it back to ROM.""" + loaded_im = getImageFile(table, image, table != 7, width, height, format) + loaded_im = hueShift(loaded_im, shift) + loaded_px = loaded_im.load() + bytes_array = [] + for y in range(height): + for x in range(width): + pix_data = list(loaded_px[x, y]) + if format == TextureFormat.RGBA32: + bytes_array.extend(pix_data) + elif format == TextureFormat.RGBA5551: + red = int((pix_data[0] >> 3) << 11) + green = int((pix_data[1] >> 3) << 6) + blue = int((pix_data[2] >> 3) << 1) + alpha = int(pix_data[3] != 0) + value = red | green | blue | alpha + bytes_array.extend([(value >> 8) & 0xFF, value & 0xFF]) + px_data = bytearray(bytes_array) + if table != 7: + px_data = gzip.compress(px_data, compresslevel=9) + ROM().seek(js.pointer_addresses[table]["entries"][image]["pointing_to"]) + ROM().writeBytes(px_data) \ No newline at end of file From 19aecb690e9a03f1c27e00c988b817f9f6924b1c Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Thu, 26 Dec 2024 21:06:21 -0600 Subject: [PATCH 02/13] Refactor p2 --- randomizer/Patching/ApplyLocal.py | 2 +- randomizer/Patching/CosmeticColors.py | 1226 +---------------- randomizer/Patching/Cosmetics/Colorblind.py | 32 +- .../Patching/Cosmetics/CustomTextures.py | 6 +- randomizer/Patching/Cosmetics/EnemyColors.py | 716 ++++++++++ randomizer/Patching/Cosmetics/Holiday.py | 1 + randomizer/Patching/Cosmetics/KongColor.py | 264 ++++ randomizer/Patching/Cosmetics/Krusha.py | 5 +- randomizer/Patching/Cosmetics/ModelSwaps.py | 167 ++- randomizer/Patching/Cosmetics/Puzzles.py | 7 +- randomizer/Patching/Cosmetics/TextRando.py | 1 + randomizer/Patching/Lib.py | 9 + randomizer/Patching/LibImage.py | 74 +- 13 files changed, 1264 insertions(+), 1246 deletions(-) create mode 100644 randomizer/Patching/Cosmetics/EnemyColors.py create mode 100644 randomizer/Patching/Cosmetics/KongColor.py diff --git a/randomizer/Patching/ApplyLocal.py b/randomizer/Patching/ApplyLocal.py index 87306ba7e..eeff17ec6 100644 --- a/randomizer/Patching/ApplyLocal.py +++ b/randomizer/Patching/ApplyLocal.py @@ -17,10 +17,10 @@ from randomizer.Lists.Songs import ExcludedSongsSelector from randomizer.Patching.Cosmetics.TextRando import writeCrownNames from randomizer.Patching.Cosmetics.Holiday import applyHolidayMode +from randomizer.Patching.Cosmetics.EnemyColors import writeMiscCosmeticChanges from randomizer.Patching.CosmeticColors import ( apply_cosmetic_colors, overwrite_object_colors, - writeMiscCosmeticChanges, darkenDPad, darkenPauseBubble, ) diff --git a/randomizer/Patching/CosmeticColors.py b/randomizer/Patching/CosmeticColors.py index b9b7bd9b4..36bb49530 100644 --- a/randomizer/Patching/CosmeticColors.py +++ b/randomizer/Patching/CosmeticColors.py @@ -4,8 +4,6 @@ import gzip import random -import zlib -from random import randint from typing import TYPE_CHECKING, List, Tuple from io import BytesIO @@ -13,11 +11,9 @@ import js from randomizer.Enums.Kongs import Kongs -from randomizer.Enums.Settings import CharacterColors, ColorblindMode, RandomModels, KongModels, WinConditionComplex -from randomizer.Enums.Models import Model, Sprite +from randomizer.Enums.Settings import CharacterColors, ColorblindMode, KongModels, WinConditionComplex from randomizer.Enums.Maps import Maps from randomizer.Enums.Types import BarrierItems -from randomizer.Patching.generate_kong_color_images import convertColors from randomizer.Patching.Cosmetics.CustomTextures import writeTransition, writeCustomPaintings, writeCustomPortal from randomizer.Patching.Cosmetics.Krusha import placeKrushaHead, fixBaboonBlasts, kong_index_mapping, fixModelSmallKongCollision from randomizer.Patching.Cosmetics.Colorblind import ( @@ -36,47 +32,28 @@ recolorRotatingRoomTiles, ) from randomizer.Patching.Lib import ( - PaletteFillType, - SpawnerChange, - applyCharacterSpawnerChanges, compatible_background_textures, TableNames, - getRawFile, ) from randomizer.Patching.LibImage import ( getImageFile, - TextureFormat, - getRandomHueShift, - hueShift, + TextureFormat, ExtraTextures, getBonusSkinOffset, writeColorImageToROM, - numberToImage, - getRGBFromHash, + numberToImage, maskImage, maskImageWithColor, getKongItemColor, - hueShiftImageContainer, + hueShiftColor, ) from randomizer.Patching.Patcher import ROM, LocalROM from randomizer.Settings import Settings from randomizer.Patching.Cosmetics.ModelSwaps import ( - turtle_models, - panic_models, - bother_models, - piano_models, - piano_extreme_model, - spotlight_fish_models, - candy_cutscene_models, - funky_cutscene_models, - funky_cutscene_models_extreme, - boot_cutscene_models, - melon_random_sprites, + model_mapping, + applyCosmeticModelSwaps, ) - -if TYPE_CHECKING: - from PIL.Image import Image - +from randomizer.Patching.Cosmetics.KongColor import writeKongColors, changeModelTextures class HelmDoorSetting: """Class to store information regarding helm doors.""" @@ -109,113 +86,8 @@ def __init__( self.dimensions = dimensions self.format = format - - - -model_mapping = { - KongModels.default: 0, - KongModels.disco_chunky: 6, - KongModels.krusha: 7, - KongModels.krool_cutscene: 9, - KongModels.krool_fight: 8, - KongModels.cranky: 10, - KongModels.candy: 11, - KongModels.funky: 12, -} -krusha_texture_replacement = { - # Textures Krusha can use when he replaces various kongs (Main color, belt color) - Kongs.donkey: (3724, 0x177D), - Kongs.diddy: (4971, 4966), - Kongs.lanky: (3689, 0xE9A), - Kongs.tiny: (6014, 0xE68), - Kongs.chunky: (3687, 3778), -} -model_texture_sections = { - KongModels.krusha: { - "skin": [0x4738, 0x2E96, 0x3A5E], - "kong": [0x3126, 0x354E, 0x37FE, 0x41E6], - }, - KongModels.krool_fight: { - "skin": [ - 0x61D6, - 0x63FE, - 0x6786, - 0x7DD6, - 0x7E8E, - 0x7F3E, - 0x7FEE, - 0x5626, - 0x56E6, - 0x5A86, - 0x5BAE, - 0x5D46, - 0x5E2E, - 0x5FAE, - 0x69BE, - 0x735E, - 0x7C5E, - 0x7E4E, - 0x7EF6, - 0x7FA6, - 0x8056, - ], - "kong": [0x607E, 0x7446, 0x7D46, 0x80FE], - }, - # KongModels.krool_cutscene: { - # "skin": [0x4A6E, 0x4CBE, 0x52AE, 0x55BE, 0x567E, 0x57E6, 0x5946, 0x5AA6, 0x5E06, 0x5EC6, 0x6020, 0x618E, 0x62F6, 0x6946, 0x6A6E, 0x6C5E, 0x6D86, 0x6F76, 0x702E, 0x70DE, 0x718E, 0x72FE, 0x4FBE, 0x51FE, 0x5C26, 0x6476, 0x6826, 0x6B26, 0x6E3E, 0x6FE6, 0x7096, 0x7146, 0x71F6, 0x733E, 0x743E], - # "kong": [], - # } -} - - -class KongPalette: - """Class to store information regarding a kong palette.""" - - def __init__(self, name: str, image: int, fill_type: PaletteFillType, alt_name: str = None): - """Initialize with given parameters.""" - self.name = name - self.image = image - self.fill_type = fill_type - self.alt_name = alt_name - if alt_name is None: - self.alt_name = name - - -class KongPaletteSetting: - """Class to store information regarding the kong palette setting.""" - - def __init__(self, kong: str, kong_index: int, palettes: list[KongPalette]): - """Initialize with given parameters.""" - self.kong = kong - self.kong_index = kong_index - self.palettes = palettes.copy() - self.setting_kong = kong - - -def getKongColor(settings: Settings, index: int): - """Get color index for a kong.""" - kong_colors = ["#ffd700", "#ff0000", "#1699ff", "#B045ff", "#41ff25"] - mode = settings.colorblind_mode - if mode != ColorblindMode.off and settings.override_cosmetics: - if mode == ColorblindMode.prot: - kong_colors = ["#000000", "#0072FF", "#766D5A", "#FFFFFF", "#FDE400"] - elif mode == ColorblindMode.deut: - kong_colors = ["#000000", "#318DFF", "#7F6D59", "#FFFFFF", "#E3A900"] - elif mode == ColorblindMode.trit: - kong_colors = ["#000000", "#C72020", "#13C4D8", "#FFFFFF", "#FFA4A4"] - return kong_colors[index] - - -DEFAULT_COLOR = "#000000" -KLAPTRAPS = [Model.KlaptrapGreen, Model.KlaptrapPurple, Model.KlaptrapRed] RECOLOR_MEDAL_RIM = False - -def getRandomKlaptrapModel() -> Model: - """Get random klaptrap model.""" - return random.choice(KLAPTRAPS) - - def changePatchFace(settings: Settings): """Change the top of the dirt patch image.""" if not settings.better_dirt_patch_cosmetic: @@ -238,103 +110,14 @@ def changePatchFace(settings: Settings): def apply_cosmetic_colors(settings: Settings): """Apply cosmetic skins to kongs.""" - bother_model_index = Model.KlaptrapGreen - panic_fairy_model_index = Model.BananaFairy - panic_klap_model_index = Model.KlaptrapGreen - turtle_model_index = Model.Turtle - sseek_klap_model_index = Model.KlaptrapGreen - fungi_tomato_model_index = Model.Tomato - caves_tomato_model_index = Model.IceTomato - racer_beetle = Model.Beetle - racer_rabbit = Model.Rabbit - piano_burper = Model.KoshKremlingRed - spotlight_fish_model_index = Model.SpotlightFish - candy_model_index = Model.Candy - funky_model_index = Model.Funky - boot_model_index = Model.Boot - melon_sprite = Sprite.BouncingMelon - swap_bitfield = 0 - + ROM_COPY = ROM() sav = settings.rom_data + applyCosmeticModelSwaps(settings, ROM_COPY) changePatchFace(settings) - - model_inverse_mapping = {} - for model in model_mapping: - val = model_mapping[model] - model_inverse_mapping[val] = model - ROM_COPY.seek(settings.rom_data + 0x1B8) - settings.kong_model_dk = model_inverse_mapping[int.from_bytes(ROM_COPY.readBytes(1), "big")] - settings.kong_model_diddy = model_inverse_mapping[int.from_bytes(ROM_COPY.readBytes(1), "big")] - settings.kong_model_lanky = model_inverse_mapping[int.from_bytes(ROM_COPY.readBytes(1), "big")] - settings.kong_model_tiny = model_inverse_mapping[int.from_bytes(ROM_COPY.readBytes(1), "big")] - settings.kong_model_chunky = model_inverse_mapping[int.from_bytes(ROM_COPY.readBytes(1), "big")] - if settings.override_cosmetics: - model_setting = RandomModels[js.document.getElementById("random_models").value] - else: - model_setting = settings.random_models - if model_setting == RandomModels.random: - bother_model_index = getRandomKlaptrapModel() - elif model_setting == RandomModels.extreme: - bother_model_index = random.choice(bother_models) - racer_beetle = random.choice([Model.Beetle, Model.Rabbit]) - racer_rabbit = random.choice([Model.Beetle, Model.Rabbit]) - if racer_rabbit == Model.Beetle: - spawner_changes = [] - # Fungi - rabbit_race_fungi_change = SpawnerChange(Maps.FungiForest, 2) - rabbit_race_fungi_change.new_scale = 50 - rabbit_race_fungi_change.new_speed_0 = 70 - rabbit_race_fungi_change.new_speed_1 = 136 - spawner_changes.append(rabbit_race_fungi_change) - # Caves - rabbit_caves_change = SpawnerChange(Maps.CavesChunkyIgloo, 1) - rabbit_caves_change.new_scale = 40 - spawner_changes.append(rabbit_caves_change) - applyCharacterSpawnerChanges(spawner_changes) - if model_setting != RandomModels.off: - panic_fairy_model_index = random.choice(panic_models) - turtle_model_index = random.choice(turtle_models) - panic_klap_model_index = getRandomKlaptrapModel() - sseek_klap_model_index = getRandomKlaptrapModel() - fungi_tomato_model_index = random.choice([Model.Tomato, Model.IceTomato]) - caves_tomato_model_index = random.choice([Model.Tomato, Model.IceTomato]) - referenced_piano_models = piano_models.copy() - referenced_funky_models = funky_cutscene_models.copy() - if model_setting == RandomModels.extreme: - referenced_piano_models.extend(piano_extreme_model) - spotlight_fish_model_index = random.choice(spotlight_fish_models) - referenced_funky_models.extend(funky_cutscene_models_extreme) - boot_model_index = random.choice(boot_cutscene_models) - piano_burper = random.choice(referenced_piano_models) - candy_model_index = random.choice(candy_cutscene_models) - funky_model_index = random.choice(funky_cutscene_models) - settings.bother_klaptrap_model = bother_model_index - settings.beetle_model = racer_beetle - settings.rabbit_model = racer_rabbit - settings.panic_fairy_model = panic_fairy_model_index - settings.turtle_model = turtle_model_index - settings.panic_klaptrap_model = panic_klap_model_index - settings.seek_klaptrap_model = sseek_klap_model_index - settings.fungi_tomato_model = fungi_tomato_model_index - settings.caves_tomato_model = caves_tomato_model_index - settings.piano_burp_model = piano_burper - settings.spotlight_fish_model = spotlight_fish_model_index - settings.candy_cutscene_model = candy_model_index - settings.funky_cutscene_model = funky_model_index - settings.boot_cutscene_model = boot_model_index - settings.wrinkly_rgb = [255, 255, 255] - # Compute swap bitfield - swap_bitfield |= 0x10 if settings.rabbit_model == Model.Beetle else 0 - swap_bitfield |= 0x20 if settings.beetle_model == Model.Rabbit else 0 - swap_bitfield |= 0x40 if settings.fungi_tomato_model == Model.IceTomato else 0 - swap_bitfield |= 0x80 if settings.caves_tomato_model == Model.Tomato else 0 - # Write Models - ROM_COPY.seek(sav + 0x1B5) - ROM_COPY.writeMultipleBytes(settings.panic_fairy_model + 1, 1) # Still needed for end seq fairy swap - ROM_COPY.seek(sav + 0x1E2) - ROM_COPY.write(swap_bitfield) + writeKongColors(settings) + settings.jetman_color = [0xFF, 0xFF, 0xFF] if settings.misc_cosmetics and settings.override_cosmetics: ROM_COPY.seek(sav + 0x196) @@ -358,185 +141,17 @@ def apply_cosmetic_colors(settings: Settings): value = random.randint(brightness_threshold, 0xFF) jetman_color[channel] = value settings.jetman_color = jetman_color.copy() - melon_sprite = random.choice(melon_random_sprites) - settings.minigame_melon_sprite = melon_sprite - color_palettes = [] - color_obj = {} - colors_dict = {} - kong_settings = [ - KongPaletteSetting( - "dk", - 0, - [ - KongPalette("fur", 3724, PaletteFillType.block), - KongPalette("tie", 0x177D, PaletteFillType.block), - KongPalette("tie", 0xE8D, PaletteFillType.patch), - ], - ), - KongPaletteSetting( - "diddy", - 1, - [ - KongPalette("clothes", 3686, PaletteFillType.block), - ], - ), - KongPaletteSetting( - "lanky", - 2, - [ - KongPalette("clothes", 3689, PaletteFillType.block), - KongPalette("clothes", 3734, PaletteFillType.patch), - KongPalette("fur", 0xE9A, PaletteFillType.block), - KongPalette("fur", 0xE94, PaletteFillType.block), - ], - ), - KongPaletteSetting( - "tiny", - 3, - [ - KongPalette("clothes", 6014, PaletteFillType.block), - KongPalette("hair", 0xE68, PaletteFillType.block), - ], - ), - KongPaletteSetting( - "chunky", - 4, - [ - KongPalette("main", 3769, PaletteFillType.checkered, "other"), - KongPalette("main", 3687, PaletteFillType.block), - ], - ), - KongPaletteSetting( - "rambi", - 5, - [ - KongPalette("skin", 3826, PaletteFillType.block), - ], - ), - KongPaletteSetting( - "enguarde", - 6, - [ - KongPalette("skin", 3847, PaletteFillType.block), - ], - ), - ] - - KONG_ZONES = { - "DK": ["Fur", "Tie"], - "Diddy": ["Clothes"], - "Lanky": ["Clothes", "Fur"], - "Tiny": ["Clothes", "Hair"], - "Chunky": ["Main", "Other"], - "Rambi": ["Skin"], - "Enguarde": ["Skin"], - } - + if js.document.getElementById("override_cosmetics").checked or True: writeTransition(settings) writeCustomPortal(settings) writeCustomPaintings(settings) # randomizePlants(ROM_COPY, settings) # Not sure how much I like how this feels - if js.document.getElementById("random_colors").checked: - for kong in KONG_ZONES: - for zone in KONG_ZONES[kong]: - settings.__setattr__(f"{kong.lower()}_{zone.lower()}_colors", CharacterColors.randomized) - else: - for kong in KONG_ZONES: - for zone in KONG_ZONES[kong]: - settings.__setattr__( - f"{kong.lower()}_{zone.lower()}_colors", - CharacterColors[js.document.getElementById(f"{kong.lower()}_{zone.lower()}_colors").value], - ) - settings.__setattr__( - f"{kong.lower()}_{zone.lower()}_custom_color", - js.document.getElementById(f"{kong.lower()}_{zone.lower()}_custom_color").value, - ) settings.gb_colors = CharacterColors[js.document.getElementById("gb_colors").value] settings.gb_custom_color = js.document.getElementById("gb_custom_color").value else: - if settings.random_colors: - for kong in KONG_ZONES: - for zone in KONG_ZONES[kong]: - settings.__setattr__(f"{kong.lower()}_{zone.lower()}_colors", CharacterColors.randomized) settings.gb_colors = CharacterColors.randomized - - colors_dict = {} - for kong in KONG_ZONES: - for zone in KONG_ZONES[kong]: - colors_dict[f"{kong.lower()}_{zone.lower()}_colors"] = settings.__getattribute__(f"{kong.lower()}_{zone.lower()}_colors") - colors_dict[f"{kong.lower()}_{zone.lower()}_custom_color"] = settings.__getattribute__(f"{kong.lower()}_{zone.lower()}_custom_color") - for kong in kong_settings: - if kong.kong_index == 4: - if settings.kong_model_chunky == KongModels.disco_chunky: - kong.palettes = [ - KongPalette("main", 3777, PaletteFillType.sparkle), - KongPalette("other", 3778, PaletteFillType.sparkle), - ] - settings_values = [ - settings.kong_model_dk, - settings.kong_model_diddy, - settings.kong_model_lanky, - settings.kong_model_tiny, - settings.kong_model_chunky, - ] - if kong.kong_index >= 0 and kong.kong_index < len(settings_values): - if settings_values[kong.kong_index] in model_texture_sections: - base_setting = kong.palettes[0].name - kong.palettes = [ - KongPalette(base_setting, krusha_texture_replacement[kong.kong_index][0], PaletteFillType.block), # krusha_skin - KongPalette(base_setting, krusha_texture_replacement[kong.kong_index][1], PaletteFillType.kong), # krusha_indicator - ] - base_obj = {"kong": kong.kong, "zones": []} - zone_to_colors = {} - for palette in kong.palettes: - arr = [DEFAULT_COLOR] - if palette.fill_type == PaletteFillType.checkered: - arr = ["#FFFF00", "#00FF00"] - elif palette.fill_type == PaletteFillType.kong: - arr = [getKongColor(settings, kong.kong_index)] - zone_data = { - "zone": palette.name, - "image": palette.image, - "fill_type": palette.fill_type, - "colors": arr, - } - for index in range(len(arr)): - base_setting = f"{kong.kong}_{palette.name}_colors" - custom_setting = f"{kong.kong}_{palette.name}_custom_color" - if index == 1: # IS THE CHECKERED PATTERN - base_setting = f"{kong.kong}_{palette.alt_name}_colors" - custom_setting = f"{kong.kong}_{palette.alt_name}_custom_color" - if (settings.override_cosmetics and colors_dict[base_setting] != CharacterColors.vanilla) or (palette.fill_type == PaletteFillType.kong): - color = None - # if this palette color is randomized, and isn't krusha's kong indicator: - if colors_dict[base_setting] == CharacterColors.randomized and palette.fill_type != PaletteFillType.kong: - if base_setting in zone_to_colors: - color = zone_to_colors[base_setting] - else: - color = f"#{format(randint(0, 0xFFFFFF), '06x')}" - zone_to_colors[base_setting] = color - # if this palette color is not randomized (but might be a custom color) and isn't krusha's kong indicator: - elif palette.fill_type != PaletteFillType.kong: - color = colors_dict[custom_setting] - if not color: - color = DEFAULT_COLOR - # if this is krusha's kong indicator: - else: - color = getKongColor(settings, kong.kong_index) - if color is not None: - zone_data["colors"][index] = color - base_obj["zones"].append(zone_data) - color_palettes.append(base_obj) - color_obj[f"{kong.kong} {palette.name}"] = color - settings.colors = color_obj - if len(color_palettes) > 0: - # this is just to prune the duplicates that appear. someone should probably fix the root of the dupe issue tbh - new_color_palettes = [] - for pal in color_palettes: - if pal not in new_color_palettes: - new_color_palettes.append(pal) - convertColors(new_color_palettes) + # GB Shine if settings.override_cosmetics and settings.gb_colors != CharacterColors.vanilla: channels = [] @@ -678,60 +293,6 @@ def maskImageGBSpin(im_f, color: tuple, image_index: int): px_0[point[0], point[1]] = px[point[0], point[1]] return masked_im -def hueShiftColor(color: tuple, amount: int, head_ratio: int = None) -> tuple: - """Apply a hue shift to a color.""" - # RGB -> HSV Conversion - red_ratio = color[0] / 255 - green_ratio = color[1] / 255 - blue_ratio = color[2] / 255 - color_max = max(red_ratio, green_ratio, blue_ratio) - color_min = min(red_ratio, green_ratio, blue_ratio) - color_delta = color_max - color_min - hue = 0 - if color_delta != 0: - if color_max == red_ratio: - hue = 60 * (((green_ratio - blue_ratio) / color_delta) % 6) - elif color_max == green_ratio: - hue = 60 * (((blue_ratio - red_ratio) / color_delta) + 2) - else: - hue = 60 * (((red_ratio - green_ratio) / color_delta) + 4) - sat = 0 if color_max == 0 else color_delta / color_max - val = color_max - # Adjust Hue - if head_ratio is not None and sat != 0: - amount = head_ratio / (sat * 100) - hue = (hue + amount) % 360 - # HSV -> RGB Conversion - c = val * sat - x = c * (1 - abs(((hue / 60) % 2) - 1)) - m = val - c - if hue < 60: - red_ratio = c - green_ratio = x - blue_ratio = 0 - elif hue < 120: - red_ratio = x - green_ratio = c - blue_ratio = 0 - elif hue < 180: - red_ratio = 0 - green_ratio = c - blue_ratio = x - elif hue < 240: - red_ratio = 0 - green_ratio = x - blue_ratio = c - elif hue < 300: - red_ratio = x - green_ratio = 0 - blue_ratio = c - else: - red_ratio = c - green_ratio = 0 - blue_ratio = x - return (int((red_ratio + m) * 255), int((green_ratio + m) * 255), int((blue_ratio + m) * 255)) - - def maskImageWithOutline(im_f, base_index, min_y, colorblind_mode, type=""): """Apply RGB mask to image with an Outline in a different color.""" w, h = im_f.size @@ -743,12 +304,11 @@ def maskImageWithOutline(im_f, base_index, min_y, colorblind_mode, type=""): im_dupe = brightener.enhance(2) im_f.paste(im_dupe, (0, min_y), im_dupe) pix = im_f.load() - mask = getRGBFromHash(getKongItemColor(colorblind_mode, base_index)) + mask = getKongItemColor(colorblind_mode, base_index, True) if base_index == 2 or (base_index == 0 and colorblind_mode == ColorblindMode.trit): # lanky or (DK in tritanopia mode) - border_color = getKongItemColor(colorblind_mode, Kongs.chunky) + mask2 = getKongItemColor(colorblind_mode, Kongs.chunky, True) else: - border_color = getKongItemColor(colorblind_mode, Kongs.diddy) - mask2 = getRGBFromHash(border_color) + mask2 = getKongItemColor(colorblind_mode, Kongs.diddy, True) contrast = False if base_index == 0: contrast = True @@ -1003,49 +563,7 @@ def applyKongModelSwaps(settings: Settings) -> None: base_im.paste(orange_im, (dim_offset, dim_offset), orange_im) writeColorImageToROM(base_im, 25, switch_faces[index], 32, 32, False, TextureFormat.RGBA5551) -def changeModelTextures(settings: Settings, kong_index: int): - """Change the textures associated with a model.""" - settings_values = [ - settings.kong_model_dk, - settings.kong_model_diddy, - settings.kong_model_lanky, - settings.kong_model_tiny, - settings.kong_model_chunky, - ] - if kong_index < 0 or kong_index >= len(settings_values): - return - model = settings_values[kong_index] - if model not in model_texture_sections: - return - for x in range(2): - file = kong_index_mapping[kong_index][x] - if file is None: - continue - krusha_model_start = js.pointer_addresses[5]["entries"][file]["pointing_to"] - krusha_model_finish = js.pointer_addresses[5]["entries"][file + 1]["pointing_to"] - krusha_model_size = krusha_model_finish - krusha_model_start - ROM_COPY = LocalROM() - ROM_COPY.seek(krusha_model_start) - indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") - ROM_COPY.seek(krusha_model_start) - data = ROM_COPY.readBytes(krusha_model_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) - num_data = [] # data, but represented as nums rather than b strings - for d in data: - num_data.append(d) - # Retexture for colors - for tex_idx in model_texture_sections[model]["skin"]: - for di, d in enumerate(int_to_list(krusha_texture_replacement[kong_index][0], 2)): # Main - num_data[tex_idx + di] = d - for tex_idx in model_texture_sections[model]["kong"]: - for di, d in enumerate(int_to_list(krusha_texture_replacement[kong_index][1], 2)): # Belt - num_data[tex_idx + di] = d - data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - LocalROM().seek(krusha_model_start) - LocalROM().writeBytes(data) + def darkenDPad(): """Change the DPad cross texture for the DPad HUD.""" @@ -1074,716 +592,6 @@ def darkenDPad(): ROM().seek(js.pointer_addresses[14]["entries"][187]["pointing_to"]) ROM().writeBytes(px_data) -def getValueFromByteArray(ba: bytearray, offset: int, size: int) -> int: - """Get value from byte array given an offset and size.""" - value = 0 - for x in range(size): - local_value = ba[offset + x] - value <<= 8 - value += local_value - return value - - -def getEnemySwapColor(channel_min: int = 0, channel_max: int = 255, min_channel_variance: int = 0) -> int: - """Get an RGB color compatible with enemy swaps.""" - channels = [] - for _ in range(2): - channels.append(random.randint(channel_min, channel_max)) - min_channel = min(channels[0], channels[1]) - max_channel = max(channels[0], channels[1]) - bounds = [] - if (min_channel - channel_min) >= min_channel_variance: - bounds.append([channel_min, min_channel]) - if (channel_max - max_channel) >= min_channel_variance: - bounds.append([max_channel, channel_max]) - if (len(bounds) == 0) or ((max_channel - min_channel) >= min_channel_variance): - # Default to random number pick - channels.append(random.randint(channel_min, channel_max)) - else: - selected_bound = random.choice(bounds) - channels.append(random.randint(selected_bound[0], selected_bound[1])) - random.shuffle(channels) - value = 0 - for x in range(3): - value <<= 8 - value += channels[x] - return value - - -class EnemyColorSwap: - """Class to store information regarding an enemy color swap.""" - - def __init__(self, search_for: list, forced_color: int = None): - """Initialize with given parameters.""" - self.search_for = search_for.copy() - total_channels = [0] * 3 - for color in self.search_for: - for channel in range(3): - shift = 8 * (2 - channel) - value = (color >> shift) & 0xFF - total_channels[channel] += value - average_channels = [int(x / len(self.search_for)) for x in total_channels] - self.average_color = 0 - for x in average_channels: - self.average_color <<= 8 - self.average_color += x - self.replace_with = forced_color - if forced_color is None: - self.replace_with = getEnemySwapColor(80, min_channel_variance=80) - - def getOutputColor(self, color: int): - """Get output color based on randomization.""" - if color not in self.search_for: - return color - if color == self.search_for[0]: - return self.replace_with - new_color = 0 - total_boost = 0 - for x in range(3): - shift = 8 * (2 - x) - provided_channel = (color >> shift) & 0xFF - primary_channel = (self.search_for[0] >> shift) & 0xFF - boost = 1 # Failsafe for div by 0 - if primary_channel != 0: - boost = provided_channel / primary_channel - total_boost += boost # Used to get an average - for x in range(3): - shift = 8 * (2 - x) - replacement_channel = (self.replace_with >> shift) & 0xFF - replacement_channel = int(replacement_channel * (total_boost / 3)) - if replacement_channel > 255: - replacement_channel = 255 - elif replacement_channel < 0: - replacement_channel = 0 - new_color <<= 8 - new_color += replacement_channel - return new_color - - -def convertColorIntToTuple(color: int) -> tuple: - """Convert color stored as 3-byte int to tuple.""" - return ((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF) - - -def getLuma(color: tuple) -> float: - """Get the luma value of a color.""" - return (0.299 * color[0]) + (0.587 * color[1]) + (0.114 * color[2]) - - -def adjustFungiMushVertexColor(shift: int): - """Adjust the special vertex coloring on Fungi Giant Mushroom.""" - fungi_geo = bytearray(getRawFile(TableNames.MapGeometry, Maps.FungiForest, True)) - DEFAULT_MUSHROOM_COLOR = (255, 90, 82) - NEW_MUSHROOM_COLOR = hueShiftColor(DEFAULT_MUSHROOM_COLOR, shift) - for x in range(0x27DA, 0x2839): - start = 0x25140 + (x * 0x10) + 0xC - channels = [] - is_zero = True - for y in range(3): - val = fungi_geo[start + y] - if val != 0: - is_zero = False - channels.append(val) - if is_zero: - continue - visual_color = [int((x / 255) * DEFAULT_MUSHROOM_COLOR[xi]) for xi, x in enumerate(channels)] - luma = int(getLuma(visual_color)) - # Diversify shading - luma -= 128 - luma = int(luma * 1.2) - luma += 128 - # Brighten - luma += 60 - # Clamp - if luma < 0: - luma = 0 - elif luma > 255: - luma = 255 - # Apply shading - for y in range(3): - fungi_geo[start + y] = luma - fungi_geo[start + 3] = 0xFF - file_data = gzip.compress(fungi_geo, compresslevel=9) - ROM().seek(js.pointer_addresses[TableNames.MapGeometry]["entries"][Maps.FungiForest]["pointing_to"]) - ROM().writeBytes(file_data) - - -def writeMiscCosmeticChanges(settings): - """Write miscellaneous changes to the cosmetic colors.""" - if settings.override_cosmetics: - enemy_setting = RandomModels[js.document.getElementById("random_enemy_colors").value] - else: - enemy_setting = settings.random_enemy_colors - if settings.misc_cosmetics: - # Melon HUD - data = { - 7: [[0x13C, 0x147]], - 14: [[0x5A, 0x5D]], - 25: [ - [getBonusSkinOffset(ExtraTextures.MelonSurface), getBonusSkinOffset(ExtraTextures.MelonSurface)], - [0x144B, 0x1452], - ], - } - shift = getRandomHueShift() - for table in data: - table_data = data[table] - for set in table_data: - for img in range(set[0], set[1] + 1): - if table == 25: - dims = (32, 32) - else: - dims = (48, 42) - melon_im = getImageFile(table, img, table != 7, dims[0], dims[1], TextureFormat.RGBA5551) - melon_im = hueShift(melon_im, shift) - melon_px = melon_im.load() - bytes_array = [] - for y in range(dims[1]): - for x in range(dims[0]): - pix_data = list(melon_px[x, y]) - red = int((pix_data[0] >> 3) << 11) - green = int((pix_data[1] >> 3) << 6) - blue = int((pix_data[2] >> 3) << 1) - alpha = int(pix_data[3] != 0) - value = red | green | blue | alpha - bytes_array.extend([(value >> 8) & 0xFF, value & 0xFF]) - px_data = bytearray(bytes_array) - if table != 7: - px_data = gzip.compress(px_data, compresslevel=9) - ROM().seek(js.pointer_addresses[table]["entries"][img]["pointing_to"]) - ROM().writeBytes(px_data) - - # Shockwave Particles - shockwave_shift = getRandomHueShift() - for img_index in range(0x174F, 0x1757): - hueShiftImageContainer(25, img_index, 16, 16, TextureFormat.RGBA32, shockwave_shift) - if settings.colorblind_mode == ColorblindMode.off: - # Fire-based sprites - fire_shift = getRandomHueShift() - fires = ( - [0x1539, 0x1553, 32], # Fireball. RGBA32 32x32 - [0x14B6, 0x14F5, 32], # Fireball. RGBA32 32x32 - [0x1554, 0x155B, 16], # Small Fireball. RGBA32 16x16 - [0x1654, 0x1683, 32], # Fire Wall. RGBA32 32x32 - [0x1495, 0x14A0, 32], # Small Explosion, RGBA32 32x32 - [0x13B9, 0x13C3, 32], # Small Explosion, RGBA32 32x32 - ) - for sprite_data in fires: - for img_index in range(sprite_data[0], sprite_data[1] + 1): - dim = sprite_data[2] - hueShiftImageContainer(25, img_index, dim, dim, TextureFormat.RGBA32, fire_shift) - for img_index in range(0x29, 0x32 + 1): - hueShiftImageContainer(7, img_index, 32, 32, TextureFormat.RGBA32, fire_shift) - for img_index in range(0x250, 0x26F + 1): - hueShiftImageContainer(7, img_index, 32, 32, TextureFormat.RGBA32, fire_shift) - for img_index in range(0xA0, 0xA7 + 1): - hueShiftImageContainer(7, img_index, 32, 32, TextureFormat.RGBA5551, fire_shift) - # Blue Fire - for img_index in range(129, 138 + 1): - hueShiftImageContainer(7, img_index, 32, 32, TextureFormat.RGBA32, fire_shift) - # Number Game Numbers - COLOR_COUNT = 2 # 2 or 16 - colors = [getRandomHueShift() for x in range(16)] - # vanilla_green = [2, 4, 5, 7, 9, 10, 12, 13] - vanilla_blue = [1, 3, 6, 8, 11, 14, 15, 16] - for x in range(16): - number_hue_shift = colors[0] - if COLOR_COUNT == 2: - if (x + 1) in vanilla_blue: - number_hue_shift = colors[1] - else: - number_hue_shift = colors[x] - for sub_img in range(2): - img_index = 0x1FE + (2 * x) + sub_img - hueShiftImageContainer(7, img_index, 32, 32, TextureFormat.RGBA5551, number_hue_shift) - if COLOR_COUNT == 2: - hueShiftImageContainer(25, 0xC2D, 32, 32, TextureFormat.RGBA5551, colors[1]) - hueShiftImageContainer(25, 0xC2E, 32, 32, TextureFormat.RGBA5551, colors[0]) - boulder_shift = getRandomHueShift() - hueShiftImageContainer(25, 0x12F4, 1, 1372, TextureFormat.RGBA5551, boulder_shift) - for img_index in range(2): - hueShiftImageContainer(25, 0xDE1 + img_index, 32, 64, TextureFormat.RGBA5551, boulder_shift) - - if enemy_setting != RandomModels.off: - # Barrel Enemy Skins - Random - klobber_shift = getRandomHueShift(0, 300) - kaboom_shift = getRandomHueShift() - for img_index in range(3): - px_count = 1404 if img_index < 2 else 1372 - hueShiftImageContainer(25, 0xF12 + img_index, 1, px_count, TextureFormat.RGBA5551, klobber_shift) - hueShiftImageContainer(25, 0xF22 + img_index, 1, px_count, TextureFormat.RGBA5551, kaboom_shift) - if img_index < 2: - hueShiftImageContainer(25, 0xF2B + img_index, 1, px_count, TextureFormat.RGBA5551, kaboom_shift) - # Klump - klump_jacket_shift = getRandomHueShift() - klump_hatammo_shift = getRandomHueShift() - jacket_images = [ - {"image": 0x104D, "px": 1372}, - {"image": 0x1058, "px": 1372}, - {"image": 0x1059, "px": 176}, - ] - hatammo_images = [ - {"image": 0x104E, "px": 1372}, - {"image": 0x104F, "px": 1372}, - {"image": 0x1050, "px": 1372}, - {"image": 0x1051, "px": 700}, - {"image": 0x1052, "px": 348}, - {"image": 0x1053, "px": 348}, - ] - for img_data in jacket_images: - hueShiftImageContainer(25, img_data["image"], 1, img_data["px"], TextureFormat.RGBA5551, klump_jacket_shift) - for img_data in hatammo_images: - hueShiftImageContainer(25, img_data["image"], 1, img_data["px"], TextureFormat.RGBA5551, klump_hatammo_shift) - # Kosha - kosha_shift = getRandomHueShift() - hueShiftImageContainer(25, 0x1232, 1, 348, TextureFormat.RGBA5551, kosha_shift) - hueShiftImageContainer(25, 0x1235, 1, 348, TextureFormat.RGBA5551, kosha_shift) - if enemy_setting == RandomModels.extreme: - kosha_helmet_int = getEnemySwapColor(80, min_channel_variance=80) - kosha_helmet_list = [ - (kosha_helmet_int >> 16) & 0xFF, - (kosha_helmet_int >> 8) & 0xFF, - kosha_helmet_int & 0xFF, - ] - kosha_club_int = getEnemySwapColor(80, min_channel_variance=80) - kosha_club_list = [(kosha_club_int >> 16) & 0xFF, (kosha_club_int >> 8) & 0xFF, kosha_club_int & 0xFF] - for img in range(0x122E, 0x1230): - kosha_im = getImageFile(25, img, True, 1, 1372, TextureFormat.RGBA5551) - kosha_im = maskImageWithColor(kosha_im, tuple(kosha_helmet_list)) - writeColorImageToROM(kosha_im, 25, img, 1, 1372, False, TextureFormat.RGBA5551) - for img in range(0x1229, 0x122C): - kosha_im = getImageFile(25, img, True, 1, 1372, TextureFormat.RGBA5551) - kosha_im = maskImageWithColor(kosha_im, tuple(kosha_club_list)) - writeColorImageToROM(kosha_im, 25, img, 1, 1372, False, TextureFormat.RGBA5551) - if settings.colorblind_mode == ColorblindMode.off: - # Kremling - kremling_dimensions = [ - [32, 64], # FCE - [64, 24], # FCF - [1, 1372], # fd0 - [32, 32], # fd1 - [24, 8], # fd2 - [24, 8], # fd3 - [24, 8], # fd4 - [24, 24], # fd5 - [32, 32], # fd6 - [32, 64], # fd7 - [32, 64], # fd8 - [36, 16], # fd9 - [20, 28], # fda - [32, 32], # fdb - [32, 32], # fdc - [12, 28], # fdd - [64, 24], # fde - [32, 32], # fdf - ] - while True: - kremling_shift = getRandomHueShift() - # Block red coloring - if kremling_shift > 290: - break - if kremling_shift > -70 and kremling_shift < 228: - break - if kremling_shift < -132: - break - for dim_index, dims in enumerate(kremling_dimensions): - if dims is not None: - hueShiftImageContainer(25, 0xFCE + dim_index, dims[0], dims[1], TextureFormat.RGBA5551, kremling_shift) - # Rabbit - rabbit_dimensions = [ - [1, 1372], # 111A - [1, 1372], # 111B - [1, 700], # 111C - [1, 700], # 111D - [1, 1372], # 111E - [1, 1372], # 111F - [1, 1372], # 1120 - [1, 1404], # 1121 - [1, 348], # 1122 - [32, 64], # 1123 - [1, 688], # 1124 - [64, 32], # 1125 - ] - rabbit_shift = getRandomHueShift() - for dim_index, dims in enumerate(rabbit_dimensions): - if dims is not None: - hueShiftImageContainer(25, 0x111A + dim_index, dims[0], dims[1], TextureFormat.RGBA5551, rabbit_shift) - # Snake - snake_shift = getRandomHueShift() - for x in range(2): - hueShiftImageContainer(25, 0xEF7 + x, 32, 32, TextureFormat.RGBA5551, snake_shift) - # Headphones Sprite - headphones_shift = getRandomHueShift() - for x in range(8): - hueShiftImageContainer(7, 0x3D3 + x, 40, 40, TextureFormat.RGBA5551, headphones_shift) - # Instruments - trombone_sax_shift = getRandomHueShift() - hueShiftImageContainer(25, 0xEA2, 32, 32, TextureFormat.RGBA5551, trombone_sax_shift) # Shine - hueShiftImageContainer(25, 0x15AF, 40, 40, TextureFormat.RGBA5551, trombone_sax_shift) # Trombone Icon - hueShiftImageContainer(25, 0x15AD, 40, 40, TextureFormat.RGBA5551, trombone_sax_shift) # Sax Icon - hueShiftImageContainer(25, 0xBCC, 32, 64, TextureFormat.RGBA5551, trombone_sax_shift) # Sax (Pad) - hueShiftImageContainer(25, 0xBCD, 32, 64, TextureFormat.RGBA5551, trombone_sax_shift) # Sax (Pad) - hueShiftImageContainer(25, 0xBD0, 32, 64, TextureFormat.RGBA5551, trombone_sax_shift) # Trombone (Pad) - hueShiftImageContainer(25, 0xBD1, 32, 64, TextureFormat.RGBA5551, trombone_sax_shift) # Trombone (Pad) - triangle_shift = getRandomHueShift() - hueShiftImageContainer(25, 0xEBF, 32, 32, TextureFormat.RGBA5551, triangle_shift) # Shine - hueShiftImageContainer(25, 0x15AE, 40, 40, TextureFormat.RGBA5551, triangle_shift) # Triangle Icon - hueShiftImageContainer(25, 0xBCE, 32, 64, TextureFormat.RGBA5551, triangle_shift) # Triangle (Pad) - hueShiftImageContainer(25, 0xBCF, 32, 64, TextureFormat.RGBA5551, triangle_shift) # Triangle (Pad) - bongo_shift = getRandomHueShift() - hueShiftImageContainer(25, 0x1317, 1, 1372, TextureFormat.RGBA5551, bongo_shift) # Skin - hueShiftImageContainer(25, 0x1318, 1, 1404, TextureFormat.RGBA5551, bongo_shift) # Side - hueShiftImageContainer(25, 0x1319, 1, 1404, TextureFormat.RGBA5551, bongo_shift) # Side 2 - hueShiftImageContainer(25, 0x15AC, 40, 40, TextureFormat.RGBA5551, bongo_shift) # Bongo Icon - hueShiftImageContainer(25, 0xBC8, 32, 64, TextureFormat.RGBA5551, bongo_shift) # Bongo (Pad) - hueShiftImageContainer(25, 0xBC9, 32, 64, TextureFormat.RGBA5551, bongo_shift) # Bongo (Pad) - if enemy_setting == RandomModels.extreme: - # Beanstalk - beanstalk_unc_size = [ - 0x480, - 0x480, - 0x480, - 0x2B8, - 0xAC0, - 0xAB8, - 0xAB8, - 0xAB8, - 0xAB8, - 0xAB8, - 0xAB8, - 0xAB8, - 0xAB8, - 0xAF8, - 0xAB8, - 0xAB8, - 0xAB8, - 0xAF8, - 0x578, - 0xAB8, - 0x578, - 0x5F8, - 0xAB8, - 0xAB8, - 0xAB8, - 0xAB8, - 0x578, - 0xAB8, - 0xAF8, - 0xAB8, - 0xAB8, - 0x560, - 0xAB8, - 0x2B8, - ] - beanstalk_shift = getRandomHueShift() - for index, size in enumerate(beanstalk_unc_size): - hueShiftImageContainer(25, 0x1126 + index, 1, int(size >> 1), TextureFormat.RGBA5551, beanstalk_shift) - # Fairy Particles Sprites - fairy_particles_shift = getRandomHueShift() - for x in range(0xB): - hueShiftImageContainer(25, 0x138D + x, 32, 32, TextureFormat.RGBA32, fairy_particles_shift) - race_coin_shift = getRandomHueShift() - for x in range(8): - hueShiftImageContainer(7, 0x1F0 + x, 48, 42, TextureFormat.RGBA5551, race_coin_shift) - scoff_shift = getRandomHueShift() - troff_shift = getRandomHueShift() - scoff_data = { - 0xFB8: 0x55C, - 0xFB9: 0x800, - 0xFBA: 0x40, - 0xFBB: 0x800, - 0xFBC: 0x240, - 0xFBD: 0x480, - 0xFBE: 0x80, - 0xFBF: 0x800, - 0xFC0: 0x200, - 0xFC1: 0x240, - 0xFC2: 0x100, - 0xFB2: 0x240, - 0xFB3: 0x800, - 0xFB4: 0x800, - 0xFB5: 0x200, - 0xFB6: 0x200, - 0xFB7: 0x200, - } - troff_data = { - 0xF78: 0x800, - 0xF79: 0x800, - 0xF7A: 0x800, - 0xF7B: 0x800, - 0xF7C: 0x800, - 0xF7D: 0x400, - 0xF7E: 0x600, - 0xF7F: 0x400, - 0xF80: 0x800, - 0xF81: 0x600, - 0xF82: 0x400, - 0xF83: 0x400, - 0xF84: 0x800, - 0xF85: 0x800, - 0xF86: 0x280, - 0xF87: 0x180, - 0xF88: 0x800, - 0xF89: 0x800, - 0xF8A: 0x400, - 0xF8B: 0x300, - 0xF8C: 0x800, - 0xF8D: 0x400, - 0xF8E: 0x500, - 0xF8F: 0x180, - } - for img in scoff_data: - hueShiftImageContainer(25, img, 1, scoff_data[img], TextureFormat.RGBA5551, scoff_shift) - - # Scoff had too many bananas, and passed potassium poisoning onto Troff - # https://i.imgur.com/WFDLSzA.png - # for img in troff_data: - # hueShiftImageContainer(25, img, 1, troff_data[img], TextureFormat.RGBA5551, troff_shift) - # Krobot - spinner_shift = getRandomHueShift() - hueShiftImageContainer(25, 0xFA9, 1, 1372, TextureFormat.RGBA5551, spinner_shift) - krobot_textures = [[[1, 1372], [0xFAF, 0xFAA, 0xFA8, 0xFAB, 0xFAD]], [[32, 32], [0xFAC, 0xFB1, 0xFAE, 0xFB0]]] - krobot_color_int = getEnemySwapColor(80, min_channel_variance=80) - krobot_color_list = [(krobot_color_int >> 16) & 0xFF, (krobot_color_int >> 8) & 0xFF, krobot_color_int & 0xFF] - for tex_set in krobot_textures: - for tex in tex_set[1]: - krobot_im = getImageFile(25, tex, True, tex_set[0][0], tex_set[0][1], TextureFormat.RGBA5551) - krobot_im = maskImageWithColor(krobot_im, tuple(krobot_color_list)) - writeColorImageToROM(krobot_im, 25, tex, tex_set[0][0], tex_set[0][1], False, TextureFormat.RGBA5551) - # Jetman - for xi, x in enumerate(settings.jetman_color): - ROM().seek(settings.rom_data + 0x1E8 + xi) - ROM().writeMultipleBytes(x, 1) - # Blast Barrels - blast_shift = getRandomHueShift() - hueShiftImageContainer(25, 0x127E, 1, 1372, TextureFormat.RGBA5551, blast_shift) - for x in range(4): - hueShiftImageContainer(25, 0x127F + x, 16, 64, TextureFormat.RGBA5551, blast_shift) - hueShiftImageContainer(25, getBonusSkinOffset(ExtraTextures.BlastTop), 1, 1372, TextureFormat.RGBA5551, blast_shift) - # K Rool - red_cs_im = Image.new(mode="RGBA", size=(32, 32), color=convertColorIntToTuple(getEnemySwapColor())) - shorts_im = Image.new(mode="RGBA", size=(32, 32), color=convertColorIntToTuple(getEnemySwapColor())) - glove_im = Image.new(mode="RGBA", size=(32, 32), color=convertColorIntToTuple(getEnemySwapColor())) - krool_data = { - 0x1149: red_cs_im, - 0x1261: shorts_im, - 0xDA8: glove_im, - } - if enemy_setting == RandomModels.extreme: - skin_im = Image.new(mode="RGBA", size=(32, 32), color=convertColorIntToTuple(getEnemySwapColor(80, min_channel_variance=80))) - krool_data[0x114A] = skin_im - krool_data[0x114D] = skin_im - for index in krool_data: - writeColorImageToROM(krool_data[index], 25, index, 32, 32, False, TextureFormat.RGBA5551) - toe_shift = getRandomHueShift() - hueShiftImageContainer(25, 0x126E, 1, 1372, TextureFormat.RGBA5551, toe_shift) - hueShiftImageContainer(25, 0x126F, 1, 1372, TextureFormat.RGBA5551, toe_shift) - if enemy_setting == RandomModels.extreme: - gold_shift = getRandomHueShift() - hueShiftImageContainer(25, 0x1265, 32, 32, TextureFormat.RGBA5551, gold_shift) - hueShiftImageContainer(25, 0x1148, 32, 32, TextureFormat.RGBA5551, gold_shift) - # Ghost - ghost_shift = getRandomHueShift() - for img in range(0x119D, 0x11AF): - px_count = 1372 - if img == 0x119E: - px_count = 176 - elif img == 0x11AC: - px_count = 688 - hueShiftImageContainer(25, img, 1, px_count, TextureFormat.RGBA5551, ghost_shift) - # Funky - funky_shift = getRandomHueShift() - hueShiftImageContainer(25, 0xECF, 1, 1372, TextureFormat.RGBA5551, funky_shift) - hueShiftImageContainer(25, 0xED6, 1, 1372, TextureFormat.RGBA5551, funky_shift) - hueShiftImageContainer(25, 0xEDF, 1, 1372, TextureFormat.RGBA5551, funky_shift) - # Zinger - zinger_shift = getRandomHueShift() - zinger_color = hueShiftColor((0xFF, 0xFF, 0x0A), zinger_shift) - zinger_color_int = (zinger_color[0] << 16) | (zinger_color[1] << 8) | (zinger_color[2]) - hueShiftImageContainer(25, 0xF0A, 1, 1372, TextureFormat.RGBA5551, zinger_shift) - # Mechazinger, use zinger color - for img_index in (0x10A0, 0x10A2, 0x10A4, 0x10A5): - hueShiftImageContainer(25, img_index, 1, 1372, TextureFormat.RGBA5551, zinger_shift) - hueShiftImageContainer(25, 0x10A3, 32, 32, TextureFormat.RGBA32, zinger_shift) - # Rings/DK Star - ring_shift = getRandomHueShift() - for x in range(2): - hueShiftImageContainer(25, 0xE1C + x, 1, 344, TextureFormat.RGBA5551, ring_shift) - hueShiftImageContainer(25, 0xD38 + x, 64, 32, TextureFormat.RGBA5551, ring_shift) - hueShiftImageContainer(7, 0x2EB, 32, 32, TextureFormat.RGBA5551, ring_shift) - # Buoys - for x in range(2): - hueShiftImageContainer(25, 0x133A + x, 1, 1372, TextureFormat.RGBA5551, getRandomHueShift()) - # Trap Bubble - hueShiftImageContainer(25, 0x134C, 32, 32, TextureFormat.RGBA5551, getRandomHueShift()) - # Spider - spider_shift = getRandomHueShift() - spider_dims = { - 0x110A: (32, 64), - 0x110B: (32, 64), - 0x110C: (32, 64), - 0x110D: (64, 16), - 0x110E: (32, 64), - 0x110F: (32, 64), - 0x1110: (32, 64), - 0x1111: (32, 64), - 0x1112: (32, 64), - 0x1113: (16, 32), - 0x1114: (32, 32), - 0x1115: (32, 32), - 0x1116: (32, 32), - 0x1117: (64, 16), - 0x1118: (64, 32), - 0x1119: (64, 32), - } - for img_index in spider_dims: - hueShiftImageContainer( - 25, - img_index, - spider_dims[img_index][0], - spider_dims[img_index][1], - TextureFormat.RGBA5551, - spider_shift, - ) - - if enemy_setting == RandomModels.extreme: - # Army Dillo - dillo_px_count = { - 0x102D: 64 * 32, - 0x103A: 16 * 16, - 0x102A: 24 * 24, - 0x102B: 24 * 24, - 0x102C: 1372, - 0x103D: 688, - 0x103E: 688, - } - dillo_shift = getRandomHueShift() - for img, px_count in dillo_px_count.items(): - hueShiftImageContainer(25, img, 1, px_count, TextureFormat.RGBA5551, dillo_shift) - - # Mushrooms - mush_man_shift = getRandomHueShift() - for img_index in (0x11FC, 0x11FD, 0x11FE, 0x11FF, 0x1200, 0x1209, 0x120A, 0x120B): - hueShiftImageContainer(25, img_index, 1, 1372, TextureFormat.RGBA5551, mush_man_shift) - for img_index in (0x11F8, 0x1205): - hueShiftImageContainer(25, img_index, 1, 692, TextureFormat.RGBA5551, mush_man_shift) - for img_index in (0x67F, 0x680): - hueShiftImageContainer(25, img_index, 32, 64, TextureFormat.RGBA5551, mush_man_shift) - hueShiftImageContainer(25, 0x6F3, 4, 4, TextureFormat.RGBA5551, mush_man_shift) - adjustFungiMushVertexColor(mush_man_shift) - - # Enemy Vertex Swaps - blue_beaver_color = getEnemySwapColor(80, min_channel_variance=80) - enemy_changes = { - Model.BeaverBlue_LowPoly: EnemyColorSwap([0xB2E5FF, 0x65CCFF, 0x00ABE8, 0x004E82, 0x008BD1, 0x001333, 0x1691CE], blue_beaver_color), # Primary - Model.BeaverBlue: EnemyColorSwap([0xB2E5FF, 0x65CCFF, 0x00ABE8, 0x004E82, 0x008BD1, 0x001333, 0x1691CE], blue_beaver_color), # Primary - Model.BeaverGold: EnemyColorSwap([0xFFE5B2, 0xFFCC65, 0xE8AB00, 0x824E00, 0xD18B00, 0x331300, 0xCE9116]), # Primary - Model.Zinger: EnemyColorSwap([0xFFFF0A, 0xFF7F00], zinger_color_int), # Legs - Model.RoboZinger: EnemyColorSwap([0xFFFF00, 0xFF5500], zinger_color_int), # Legs - Model.Candy: EnemyColorSwap( - [ - 0xFF96EB, - 0x572C58, - 0xB86CAA, - 0xEB4C91, - 0x8B2154, - 0xD13B80, - 0xFF77C1, - 0xFF599E, - 0x7F1E4C, - 0x61173A, - 0x902858, - 0xA42E64, - 0x791C49, - 0x67183E, - 0x9E255C, - 0xC12E74, - 0x572C58, - 0xFF96EB, - 0xB86CAA, - ] - ), - Model.Laser: EnemyColorSwap([0xF30000]), - Model.Kasplat: EnemyColorSwap([0x8FD8FF, 0x182A4F, 0x0B162C, 0x7A98D3, 0x3F6CC4, 0x8FD8FF, 0x284581]), - # Model.BananaFairy: EnemyColorSwap([0xFFD400, 0xFFAA00, 0xFCD200, 0xD68F00, 0xD77D0A, 0xe49800, 0xdf7f1f, 0xa26c00, 0xd6b200, 0xdf9f1f]) - } - if enemy_setting == RandomModels.extreme: - enemy_changes[Model.Klump] = EnemyColorSwap([0xE66B78, 0x621738, 0x300F20, 0xD1426F, 0xA32859]) - dogadon_color = getEnemySwapColor(80, 160, min_channel_variance=80) - enemy_changes[Model.Dogadon] = EnemyColorSwap( - [ - 0xFF0000, - 0xFF7F00, - 0x450A1F, - 0xB05800, - 0xFF3200, - 0xFFD400, - 0x4F260D, - 0x600F00, - 0x6A1400, - 0xAA0000, - 0xDF3F1F, - 0xFF251F, - 0x8F4418, - 0x522900, - 0xDF9F1F, - 0x3B0606, - 0x91121E, - 0x700C0D, - 0xFF5900, - 0xFF7217, - 0xFF7425, - 0xFF470B, - 0xA82100, - 0x4A0D18, - 0x580E00, - 0x461309, - 0x4C1503, - 0x780D0E, - 0xFFA74A, - 0x7E120F, - 0x700000, - 0xB64D19, - 0x883A13, - 0xBD351A, - 0xD42900, - 0xFF2A00, - 0x921511, - 0x9C662D, - 0xDF5F1F, - 0x9B1112, - 0x461F0A, - 0x4B0808, - 0x500809, - 0xA42000, - 0x5F0B13, - 0xBF6A3F, - 0x602E10, - 0x971414, - 0x422C15, - 0xFC5800, - 0x5C0D0B, - ], - dogadon_color, - ) - for enemy in enemy_changes: - file_data = bytearray(getRawFile(5, enemy, True)) - vert_start = 0x28 - file_head = getValueFromByteArray(file_data, 0, 4) - disp_list_end = (getValueFromByteArray(file_data, 4, 4) - file_head) + 0x28 - vert_end = (getValueFromByteArray(file_data, disp_list_end, 4) - file_head) + 0x28 - vert_count = int((vert_end - vert_start) / 0x10) - for vert in range(vert_count): - local_start = 0x28 + (0x10 * vert) - test_rgb = getValueFromByteArray(file_data, local_start + 0xC, 3) - new_rgb = enemy_changes[enemy].getOutputColor(test_rgb) - for x in range(3): - shift = 8 * (2 - x) - channel = (new_rgb >> shift) & 0xFF - file_data[local_start + 0xC + x] = channel - file_data = gzip.compress(file_data, compresslevel=9) - ROM().seek(js.pointer_addresses[5]["entries"][enemy]["pointing_to"]) - ROM().writeBytes(file_data) - def applyHelmDoorCosmetics(settings: Settings) -> None: """Apply Helm Door Cosmetic Changes.""" crown_door_required_item = settings.crown_door_item diff --git a/randomizer/Patching/Cosmetics/Colorblind.py b/randomizer/Patching/Cosmetics/Colorblind.py index 93249aa1a..31fdfa410 100644 --- a/randomizer/Patching/Cosmetics/Colorblind.py +++ b/randomizer/Patching/Cosmetics/Colorblind.py @@ -1,3 +1,4 @@ +"""All code associated with colorblind mode.""" import js import gzip import zlib @@ -193,7 +194,7 @@ def maskBlueprintImage(im_f, base_index, mode: ColorblindMode): im_f.paste(im_dupe, (0, 0), im_dupe) pix = im_f.load() pix2 = im_f_original.load() - mask = getRGBFromHash(getKongItemColor(mode, base_index)) + mask = getKongItemColor(mode, base_index, True) if max(mask[0], max(mask[1], mask[2])) < 39: for channel in range(3): mask[channel] = max(39, mask[channel]) # Too black is bad for these items @@ -227,7 +228,7 @@ def maskLaserImage(im_f, base_index, mode: ColorblindMode): im_f.paste(im_dupe, (0, 0), im_dupe) pix = im_f.load() pix2 = im_f_original.load() - mask = getRGBFromHash(getKongItemColor(mode, base_index)) + mask = getKongItemColor(mode, base_index, True) w, h = im_f.size for x in range(w): for y in range(h): @@ -549,12 +550,9 @@ def recolorSlamSwitches(galleon_switch_value, ROM_COPY: ROM, mode: ColorblindMod num_data.append(d) # Figure out which colors to use and where to put them color_offsets = [1828, 1844, 1860, 1876, 1892, 1908] - green_switch_str = getKongItemColor(mode, Kongs.chunky) - blue_switch_str = getKongItemColor(mode, Kongs.lanky) - red_switch_str = getKongItemColor(mode, Kongs.diddy) - new_color1 = getRGBFromHash(green_switch_str) # chunky's color - new_color2 = getRGBFromHash(blue_switch_str) # lanky's color - new_color3 = getRGBFromHash(red_switch_str) # diddy's color + new_color1 = getKongItemColor(mode, Kongs.chunky, True) + new_color2 = getKongItemColor(mode, Kongs.lanky, True) + new_color3 = getKongItemColor(mode, Kongs.diddy, True) # Green switches if switch < 5: @@ -608,7 +606,7 @@ def recolorBlueprintModelTwo(mode: ColorblindMode): color1_offsets = [0x52C, 0x54C, 0x57C, 0x58C, 0x5AC, 0x5CC, 0x5FC, 0x61C] color2_offsets = [0x53C, 0x55C, 0x5EC, 0x60C] color3_offsets = [0x56C, 0x59C, 0x5BC, 0x5DC] - new_color = getRGBFromHash(getKongItemColor(mode, kong)) + new_color = getKongItemColor(mode, kong, True) if kong == 0: for channel in range(3): new_color[channel] = max(39, new_color[channel]) # Too black is bad, because anything times 0 is 0 @@ -663,14 +661,14 @@ def maskImageRotatingRoomTile(im_f, im_mask, paste_coords, image_color_index, ti if coord[3] > 0: mask_coords.append([(x + paste_coords[0]), (y + paste_coords[1])]) if image_color_index < 5: - mask = getRGBFromHash(getKongItemColor(mode, image_color_index)) + mask = getKongItemColor(mode, image_color_index, True) for channel in range(3): mask[channel] = max(39, mask[channel]) # Too dark looks bad else: - mask = getRGBFromHash(getKongItemColor(mode, Kongs.lanky)) - mask2 = getRGBFromHash("#000000") + mask = getKongItemColor(mode, Kongs.lanky, True) + mask2 = [0x00, 0x00, 0x00] if image_color_index == 0: - mask2 = getRGBFromHash("#FFFFFF") + mask2 = [0xFF, 0xFF, 0xFF] for x in range(w): for y in range(h): base = list(pix[x, y]) @@ -778,8 +776,8 @@ def recolorBells(): # Figure out which colors to use and where to put them color1_offsets = [0x214, 0x244, 0x264, 0x274, 0x284] color2_offsets = [0x224, 0x234, 0x254] - new_color1 = getRGBFromHash("#0066FF") - new_color2 = getRGBFromHash("#0000FF") + new_color1 = [0x00, 0x66, 0xFF] + new_color2 = [0x00, 0x00, 0xFF] # Recolor the bell for offset in color1_offsets: @@ -856,7 +854,7 @@ def recolorPotions(colorblind_mode): color5_offsets = [0xB4, 0xC4, 0xD4] # color6_offsets = [0xF4, 0x104, 0x114, 0x124, 0x134, 0x144, 0x154, 0x164] if potion_color < 5: - new_color = getRGBFromHash(getKongItemColor(colorblind_mode, potion_color)) + new_color = getKongItemColor(colorblind_mode, potion_color, True) else: new_color = getRGBFromHash("#FFFFFF") @@ -931,7 +929,7 @@ def recolorPotions(colorblind_mode): color5_offsets = [0x1C4, 0x1D4, 0x1E4] # color6_offsets = [0x204, 0x214, 0x224, 0x234, 0x244, 0x254, 0x264, 0x274] if potion_color < 5: - new_color = getRGBFromHash(getKongItemColor(colorblind_mode, potion_color)) + new_color = getKongItemColor(colorblind_mode, potion_color, True) else: new_color = getRGBFromHash("#FFFFFF") diff --git a/randomizer/Patching/Cosmetics/CustomTextures.py b/randomizer/Patching/Cosmetics/CustomTextures.py index fa3b77c99..af600c99e 100644 --- a/randomizer/Patching/Cosmetics/CustomTextures.py +++ b/randomizer/Patching/Cosmetics/CustomTextures.py @@ -1,14 +1,12 @@ +"""Code associated with custom textures that can be applied through the cosmetic pack.""" import js import random import math from io import BytesIO -from typing import TYPE_CHECKING from randomizer.Settings import Settings from randomizer.Patching.LibImage import writeColorImageToROM, TextureFormat, getImageFile - -if TYPE_CHECKING: - from PIL.Image import Image +from PIL import Image def writeTransition(settings: Settings) -> None: """Write transition cosmetic to ROM.""" diff --git a/randomizer/Patching/Cosmetics/EnemyColors.py b/randomizer/Patching/Cosmetics/EnemyColors.py new file mode 100644 index 000000000..6b51a0f63 --- /dev/null +++ b/randomizer/Patching/Cosmetics/EnemyColors.py @@ -0,0 +1,716 @@ +"""All code changes associated with enemy color rando.""" +import js +import gzip +import random +from randomizer.Enums.Maps import Maps +from randomizer.Enums.Models import Model +from randomizer.Enums.Settings import RandomModels, ColorblindMode +from randomizer.Patching.LibImage import ( + getBonusSkinOffset, + ExtraTextures, + getRandomHueShift, + getImageFile, + TextureFormat, + hueShift, + hueShiftImageContainer, + maskImageWithColor, + writeColorImageToROM, + getLuma, + hueShiftColor, +) +from randomizer.Patching.Lib import getRawFile, TableNames, getValueFromByteArray +from randomizer.Patching.Patcher import ROM +from PIL import Image + +def getEnemySwapColor(channel_min: int = 0, channel_max: int = 255, min_channel_variance: int = 0) -> int: + """Get an RGB color compatible with enemy swaps.""" + channels = [] + for _ in range(2): + channels.append(random.randint(channel_min, channel_max)) + min_channel = min(channels[0], channels[1]) + max_channel = max(channels[0], channels[1]) + bounds = [] + if (min_channel - channel_min) >= min_channel_variance: + bounds.append([channel_min, min_channel]) + if (channel_max - max_channel) >= min_channel_variance: + bounds.append([max_channel, channel_max]) + if (len(bounds) == 0) or ((max_channel - min_channel) >= min_channel_variance): + # Default to random number pick + channels.append(random.randint(channel_min, channel_max)) + else: + selected_bound = random.choice(bounds) + channels.append(random.randint(selected_bound[0], selected_bound[1])) + random.shuffle(channels) + value = 0 + for x in range(3): + value <<= 8 + value += channels[x] + return value + +class EnemyColorSwap: + """Class to store information regarding an enemy color swap.""" + + def __init__(self, search_for: list, forced_color: int = None): + """Initialize with given parameters.""" + self.search_for = search_for.copy() + total_channels = [0] * 3 + for color in self.search_for: + for channel in range(3): + shift = 8 * (2 - channel) + value = (color >> shift) & 0xFF + total_channels[channel] += value + average_channels = [int(x / len(self.search_for)) for x in total_channels] + self.average_color = 0 + for x in average_channels: + self.average_color <<= 8 + self.average_color += x + self.replace_with = forced_color + if forced_color is None: + self.replace_with = getEnemySwapColor(80, min_channel_variance=80) + + def getOutputColor(self, color: int): + """Get output color based on randomization.""" + if color not in self.search_for: + return color + if color == self.search_for[0]: + return self.replace_with + new_color = 0 + total_boost = 0 + for x in range(3): + shift = 8 * (2 - x) + provided_channel = (color >> shift) & 0xFF + primary_channel = (self.search_for[0] >> shift) & 0xFF + boost = 1 # Failsafe for div by 0 + if primary_channel != 0: + boost = provided_channel / primary_channel + total_boost += boost # Used to get an average + for x in range(3): + shift = 8 * (2 - x) + replacement_channel = (self.replace_with >> shift) & 0xFF + replacement_channel = int(replacement_channel * (total_boost / 3)) + if replacement_channel > 255: + replacement_channel = 255 + elif replacement_channel < 0: + replacement_channel = 0 + new_color <<= 8 + new_color += replacement_channel + return new_color + +# Enemy texture data +FIRE_TEXTURES = ( + [0x1539, 0x1553, 32], # Fireball. RGBA32 32x32 + [0x14B6, 0x14F5, 32], # Fireball. RGBA32 32x32 + [0x1554, 0x155B, 16], # Small Fireball. RGBA32 16x16 + [0x1654, 0x1683, 32], # Fire Wall. RGBA32 32x32 + [0x1495, 0x14A0, 32], # Small Explosion, RGBA32 32x32 + [0x13B9, 0x13C3, 32], # Small Explosion, RGBA32 32x32 +) +KLUMP_JACKET_TEXTURES = [ + {"image": 0x104D, "px": 1372}, + {"image": 0x1058, "px": 1372}, + {"image": 0x1059, "px": 176}, +] +KLUMP_HAT_AMMO_TEXTURES = [ + {"image": 0x104E, "px": 1372}, + {"image": 0x104F, "px": 1372}, + {"image": 0x1050, "px": 1372}, + {"image": 0x1051, "px": 700}, + {"image": 0x1052, "px": 348}, + {"image": 0x1053, "px": 348}, +] +KREMLING_TEXTURE_DIMENSIONS = [ + [32, 64], # FCE + [64, 24], # FCF + [1, 1372], # fd0 + [32, 32], # fd1 + [24, 8], # fd2 + [24, 8], # fd3 + [24, 8], # fd4 + [24, 24], # fd5 + [32, 32], # fd6 + [32, 64], # fd7 + [32, 64], # fd8 + [36, 16], # fd9 + [20, 28], # fda + [32, 32], # fdb + [32, 32], # fdc + [12, 28], # fdd + [64, 24], # fde + [32, 32], # fdf +] +RABBIT_TEXTURE_DIMENSIONS = [ + [1, 1372], # 111A + [1, 1372], # 111B + [1, 700], # 111C + [1, 700], # 111D + [1, 1372], # 111E + [1, 1372], # 111F + [1, 1372], # 1120 + [1, 1404], # 1121 + [1, 348], # 1122 + [32, 64], # 1123 + [1, 688], # 1124 + [64, 32], # 1125 +] +BEANSTALK_TEXTURE_FILE_SIZES = [ + 0x480, + 0x480, + 0x480, + 0x2B8, + 0xAC0, + 0xAB8, + 0xAB8, + 0xAB8, + 0xAB8, + 0xAB8, + 0xAB8, + 0xAB8, + 0xAB8, + 0xAF8, + 0xAB8, + 0xAB8, + 0xAB8, + 0xAF8, + 0x578, + 0xAB8, + 0x578, + 0x5F8, + 0xAB8, + 0xAB8, + 0xAB8, + 0xAB8, + 0x578, + 0xAB8, + 0xAF8, + 0xAB8, + 0xAB8, + 0x560, + 0xAB8, + 0x2B8, +] +SCOFF_TEXTURE_DATA = { + 0xFB8: 0x55C, + 0xFB9: 0x800, + 0xFBA: 0x40, + 0xFBB: 0x800, + 0xFBC: 0x240, + 0xFBD: 0x480, + 0xFBE: 0x80, + 0xFBF: 0x800, + 0xFC0: 0x200, + 0xFC1: 0x240, + 0xFC2: 0x100, + 0xFB2: 0x240, + 0xFB3: 0x800, + 0xFB4: 0x800, + 0xFB5: 0x200, + 0xFB6: 0x200, + 0xFB7: 0x200, +} +TROFF_TEXTURE_DATA = { + 0xF78: 0x800, + 0xF79: 0x800, + 0xF7A: 0x800, + 0xF7B: 0x800, + 0xF7C: 0x800, + 0xF7D: 0x400, + 0xF7E: 0x600, + 0xF7F: 0x400, + 0xF80: 0x800, + 0xF81: 0x600, + 0xF82: 0x400, + 0xF83: 0x400, + 0xF84: 0x800, + 0xF85: 0x800, + 0xF86: 0x280, + 0xF87: 0x180, + 0xF88: 0x800, + 0xF89: 0x800, + 0xF8A: 0x400, + 0xF8B: 0x300, + 0xF8C: 0x800, + 0xF8D: 0x400, + 0xF8E: 0x500, + 0xF8F: 0x180, +} +SPIDER_TEXTURE_DIMENSIONS = { + 0x110A: (32, 64), + 0x110B: (32, 64), + 0x110C: (32, 64), + 0x110D: (64, 16), + 0x110E: (32, 64), + 0x110F: (32, 64), + 0x1110: (32, 64), + 0x1111: (32, 64), + 0x1112: (32, 64), + 0x1113: (16, 32), + 0x1114: (32, 32), + 0x1115: (32, 32), + 0x1116: (32, 32), + 0x1117: (64, 16), + 0x1118: (64, 32), + 0x1119: (64, 32), +} + +def convertColorIntToTuple(color: int) -> tuple: + """Convert color stored as 3-byte int to tuple.""" + return ((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF) + +def adjustFungiMushVertexColor(shift: int): + """Adjust the special vertex coloring on Fungi Giant Mushroom.""" + fungi_geo = bytearray(getRawFile(TableNames.MapGeometry, Maps.FungiForest, True)) + DEFAULT_MUSHROOM_COLOR = (255, 90, 82) + for x in range(0x27DA, 0x2839): + start = 0x25140 + (x * 0x10) + 0xC + channels = [] + is_zero = True + for y in range(3): + val = fungi_geo[start + y] + if val != 0: + is_zero = False + channels.append(val) + if is_zero: + continue + visual_color = [int((x / 255) * DEFAULT_MUSHROOM_COLOR[xi]) for xi, x in enumerate(channels)] + luma = int(getLuma(visual_color)) + # Diversify shading + luma -= 128 + luma = int(luma * 1.2) + luma += 128 + # Brighten + luma += 60 + # Clamp + if luma < 0: + luma = 0 + elif luma > 255: + luma = 255 + # Apply shading + for y in range(3): + fungi_geo[start + y] = luma + fungi_geo[start + 3] = 0xFF + file_data = gzip.compress(fungi_geo, compresslevel=9) + ROM_COPY = ROM() + ROM_COPY.seek(js.pointer_addresses[TableNames.MapGeometry]["entries"][Maps.FungiForest]["pointing_to"]) + ROM_COPY.writeBytes(file_data) + +def writeMiscCosmeticChanges(settings): + """Write miscellaneous changes to the cosmetic colors.""" + ROM_COPY = ROM() + if settings.override_cosmetics: + enemy_setting = RandomModels[js.document.getElementById("random_enemy_colors").value] + else: + enemy_setting = settings.random_enemy_colors + if settings.misc_cosmetics: + # Melon HUD + data = { + 7: [[0x13C, 0x147]], + 14: [[0x5A, 0x5D]], + 25: [ + [getBonusSkinOffset(ExtraTextures.MelonSurface), getBonusSkinOffset(ExtraTextures.MelonSurface)], + [0x144B, 0x1452], + ], + } + shift = getRandomHueShift() + for table in data: + table_data = data[table] + for set in table_data: + for img in range(set[0], set[1] + 1): + if table == 25: + dims = (32, 32) + else: + dims = (48, 42) + melon_im = getImageFile(table, img, table != 7, dims[0], dims[1], TextureFormat.RGBA5551) + melon_im = hueShift(melon_im, shift) + melon_px = melon_im.load() + bytes_array = [] + for y in range(dims[1]): + for x in range(dims[0]): + pix_data = list(melon_px[x, y]) + red = int((pix_data[0] >> 3) << 11) + green = int((pix_data[1] >> 3) << 6) + blue = int((pix_data[2] >> 3) << 1) + alpha = int(pix_data[3] != 0) + value = red | green | blue | alpha + bytes_array.extend([(value >> 8) & 0xFF, value & 0xFF]) + px_data = bytearray(bytes_array) + if table != 7: + px_data = gzip.compress(px_data, compresslevel=9) + ROM_COPY.seek(js.pointer_addresses[table]["entries"][img]["pointing_to"]) + ROM_COPY.writeBytes(px_data) + + # Shockwave Particles + shockwave_shift = getRandomHueShift() + for img_index in range(0x174F, 0x1757): + hueShiftImageContainer(25, img_index, 16, 16, TextureFormat.RGBA32, shockwave_shift, ROM_COPY) + if settings.colorblind_mode == ColorblindMode.off: + # Fire-based sprites + fire_shift = getRandomHueShift() + for sprite_data in FIRE_TEXTURES: + for img_index in range(sprite_data[0], sprite_data[1] + 1): + dim = sprite_data[2] + hueShiftImageContainer(25, img_index, dim, dim, TextureFormat.RGBA32, fire_shift, ROM_COPY) + for img_index in range(0x29, 0x32 + 1): + hueShiftImageContainer(7, img_index, 32, 32, TextureFormat.RGBA32, fire_shift, ROM_COPY) + for img_index in range(0x250, 0x26F + 1): + hueShiftImageContainer(7, img_index, 32, 32, TextureFormat.RGBA32, fire_shift, ROM_COPY) + for img_index in range(0xA0, 0xA7 + 1): + hueShiftImageContainer(7, img_index, 32, 32, TextureFormat.RGBA5551, fire_shift, ROM_COPY) + # Blue Fire + for img_index in range(129, 138 + 1): + hueShiftImageContainer(7, img_index, 32, 32, TextureFormat.RGBA32, fire_shift, ROM_COPY) + # Number Game Numbers + colors = [getRandomHueShift() for _ in range(2)] + vanilla_blue = [1, 3, 6, 8, 11, 14, 15, 16] + for x in range(16): + number_hue_shift = colors[0] + if (x + 1) in vanilla_blue: + number_hue_shift = colors[1] + for sub_img in range(2): + img_index = 0x1FE + (2 * x) + sub_img + hueShiftImageContainer(7, img_index, 32, 32, TextureFormat.RGBA5551, number_hue_shift, ROM_COPY) + hueShiftImageContainer(25, 0xC2D, 32, 32, TextureFormat.RGBA5551, colors[1], ROM_COPY) + hueShiftImageContainer(25, 0xC2E, 32, 32, TextureFormat.RGBA5551, colors[0], ROM_COPY) + boulder_shift = getRandomHueShift() + hueShiftImageContainer(25, 0x12F4, 1, 1372, TextureFormat.RGBA5551, boulder_shift, ROM_COPY) + for img_index in range(2): + hueShiftImageContainer(25, 0xDE1 + img_index, 32, 64, TextureFormat.RGBA5551, boulder_shift, ROM_COPY) + + if enemy_setting != RandomModels.off: + # Barrel Enemy Skins - Random + klobber_shift = getRandomHueShift(0, 300) + kaboom_shift = getRandomHueShift() + for img_index in range(3): + px_count = 1404 if img_index < 2 else 1372 + hueShiftImageContainer(25, 0xF12 + img_index, 1, px_count, TextureFormat.RGBA5551, klobber_shift, ROM_COPY) + hueShiftImageContainer(25, 0xF22 + img_index, 1, px_count, TextureFormat.RGBA5551, kaboom_shift, ROM_COPY) + if img_index < 2: + hueShiftImageContainer(25, 0xF2B + img_index, 1, px_count, TextureFormat.RGBA5551, kaboom_shift, ROM_COPY) + # Klump + klump_jacket_shift = getRandomHueShift() + klump_hatammo_shift = getRandomHueShift() + + for img_data in KLUMP_JACKET_TEXTURES: + hueShiftImageContainer(25, img_data["image"], 1, img_data["px"], TextureFormat.RGBA5551, klump_jacket_shift, ROM_COPY) + for img_data in KLUMP_HAT_AMMO_TEXTURES: + hueShiftImageContainer(25, img_data["image"], 1, img_data["px"], TextureFormat.RGBA5551, klump_hatammo_shift, ROM_COPY) + # Kosha + kosha_shift = getRandomHueShift() + hueShiftImageContainer(25, 0x1232, 1, 348, TextureFormat.RGBA5551, kosha_shift, ROM_COPY) + hueShiftImageContainer(25, 0x1235, 1, 348, TextureFormat.RGBA5551, kosha_shift, ROM_COPY) + if enemy_setting == RandomModels.extreme: + kosha_helmet_int = getEnemySwapColor(80, min_channel_variance=80) + kosha_helmet_list = [ + (kosha_helmet_int >> 16) & 0xFF, + (kosha_helmet_int >> 8) & 0xFF, + kosha_helmet_int & 0xFF, + ] + kosha_club_int = getEnemySwapColor(80, min_channel_variance=80) + kosha_club_list = [(kosha_club_int >> 16) & 0xFF, (kosha_club_int >> 8) & 0xFF, kosha_club_int & 0xFF] + for img in range(0x122E, 0x1230): + kosha_im = getImageFile(25, img, True, 1, 1372, TextureFormat.RGBA5551) + kosha_im = maskImageWithColor(kosha_im, tuple(kosha_helmet_list)) + writeColorImageToROM(kosha_im, 25, img, 1, 1372, False, TextureFormat.RGBA5551) + for img in range(0x1229, 0x122C): + kosha_im = getImageFile(25, img, True, 1, 1372, TextureFormat.RGBA5551) + kosha_im = maskImageWithColor(kosha_im, tuple(kosha_club_list)) + writeColorImageToROM(kosha_im, 25, img, 1, 1372, False, TextureFormat.RGBA5551) + if settings.colorblind_mode == ColorblindMode.off: + # Kremling + + while True: + kremling_shift = getRandomHueShift() + # Block red coloring + if kremling_shift > 290: + break + if kremling_shift > -70 and kremling_shift < 228: + break + if kremling_shift < -132: + break + for dim_index, dims in enumerate(KREMLING_TEXTURE_DIMENSIONS): + if dims is not None: + hueShiftImageContainer(25, 0xFCE + dim_index, dims[0], dims[1], TextureFormat.RGBA5551, kremling_shift, ROM_COPY) + # Rabbit + + rabbit_shift = getRandomHueShift() + for dim_index, dims in enumerate(RABBIT_TEXTURE_DIMENSIONS): + if dims is not None: + hueShiftImageContainer(25, 0x111A + dim_index, dims[0], dims[1], TextureFormat.RGBA5551, rabbit_shift, ROM_COPY) + # Snake + snake_shift = getRandomHueShift() + for x in range(2): + hueShiftImageContainer(25, 0xEF7 + x, 32, 32, TextureFormat.RGBA5551, snake_shift, ROM_COPY) + # Headphones Sprite + headphones_shift = getRandomHueShift() + for x in range(8): + hueShiftImageContainer(7, 0x3D3 + x, 40, 40, TextureFormat.RGBA5551, headphones_shift, ROM_COPY) + # Instruments + trombone_sax_shift = getRandomHueShift() + hueShiftImageContainer(25, 0xEA2, 32, 32, TextureFormat.RGBA5551, trombone_sax_shift) # Shin, ROM_COPYe + hueShiftImageContainer(25, 0x15AF, 40, 40, TextureFormat.RGBA5551, trombone_sax_shift) # Trombone Ico, ROM_COPYn + hueShiftImageContainer(25, 0x15AD, 40, 40, TextureFormat.RGBA5551, trombone_sax_shift) # Sax Ico, ROM_COPYn + hueShiftImageContainer(25, 0xBCC, 32, 64, TextureFormat.RGBA5551, trombone_sax_shift) # Sax (Pad, ROM_COPY) + hueShiftImageContainer(25, 0xBCD, 32, 64, TextureFormat.RGBA5551, trombone_sax_shift) # Sax (Pad, ROM_COPY) + hueShiftImageContainer(25, 0xBD0, 32, 64, TextureFormat.RGBA5551, trombone_sax_shift) # Trombone (Pad, ROM_COPY) + hueShiftImageContainer(25, 0xBD1, 32, 64, TextureFormat.RGBA5551, trombone_sax_shift) # Trombone (Pad, ROM_COPY) + triangle_shift = getRandomHueShift() + hueShiftImageContainer(25, 0xEBF, 32, 32, TextureFormat.RGBA5551, triangle_shift) # Shin, ROM_COPYe + hueShiftImageContainer(25, 0x15AE, 40, 40, TextureFormat.RGBA5551, triangle_shift) # Triangle Ico, ROM_COPYn + hueShiftImageContainer(25, 0xBCE, 32, 64, TextureFormat.RGBA5551, triangle_shift) # Triangle (Pad, ROM_COPY) + hueShiftImageContainer(25, 0xBCF, 32, 64, TextureFormat.RGBA5551, triangle_shift) # Triangle (Pad, ROM_COPY) + bongo_shift = getRandomHueShift() + hueShiftImageContainer(25, 0x1317, 1, 1372, TextureFormat.RGBA5551, bongo_shift) # Ski, ROM_COPYn + hueShiftImageContainer(25, 0x1318, 1, 1404, TextureFormat.RGBA5551, bongo_shift) # Sid, ROM_COPYe + hueShiftImageContainer(25, 0x1319, 1, 1404, TextureFormat.RGBA5551, bongo_shift) # Side , ROM_COPY2 + hueShiftImageContainer(25, 0x15AC, 40, 40, TextureFormat.RGBA5551, bongo_shift) # Bongo Ico, ROM_COPYn + hueShiftImageContainer(25, 0xBC8, 32, 64, TextureFormat.RGBA5551, bongo_shift) # Bongo (Pad, ROM_COPY) + hueShiftImageContainer(25, 0xBC9, 32, 64, TextureFormat.RGBA5551, bongo_shift) # Bongo (Pad, ROM_COPY) + if enemy_setting == RandomModels.extreme: + # Beanstalk + beanstalk_shift = getRandomHueShift() + for index, size in enumerate(BEANSTALK_TEXTURE_FILE_SIZES): + hueShiftImageContainer(25, 0x1126 + index, 1, int(size >> 1), TextureFormat.RGBA5551, beanstalk_shift, ROM_COPY) + # Fairy Particles Sprites + fairy_particles_shift = getRandomHueShift() + for x in range(0xB): + hueShiftImageContainer(25, 0x138D + x, 32, 32, TextureFormat.RGBA32, fairy_particles_shift, ROM_COPY) + race_coin_shift = getRandomHueShift() + for x in range(8): + hueShiftImageContainer(7, 0x1F0 + x, 48, 42, TextureFormat.RGBA5551, race_coin_shift, ROM_COPY) + scoff_shift = getRandomHueShift() + troff_shift = getRandomHueShift() + + for img in SCOFF_TEXTURE_DATA: + hueShiftImageContainer(25, img, 1, SCOFF_TEXTURE_DATA[img], TextureFormat.RGBA5551, scoff_shift, ROM_COPY) + + # Scoff had too many bananas, and passed potassium poisoning onto Troff + # https://i.imgur.com/WFDLSzA.png + # for img in TROFF_TEXTURE_DATA: + # hueShiftImageContainer(25, img, 1, TROFF_TEXTURE_DATA[img], TextureFormat.RGBA5551, troff_shift, ROM_COPY) + # Krobot + spinner_shift = getRandomHueShift() + hueShiftImageContainer(25, 0xFA9, 1, 1372, TextureFormat.RGBA5551, spinner_shift, ROM_COPY) + krobot_textures = [[[1, 1372], [0xFAF, 0xFAA, 0xFA8, 0xFAB, 0xFAD]], [[32, 32], [0xFAC, 0xFB1, 0xFAE, 0xFB0]]] + krobot_color_int = getEnemySwapColor(80, min_channel_variance=80) + krobot_color_list = [(krobot_color_int >> 16) & 0xFF, (krobot_color_int >> 8) & 0xFF, krobot_color_int & 0xFF] + for tex_set in krobot_textures: + for tex in tex_set[1]: + krobot_im = getImageFile(25, tex, True, tex_set[0][0], tex_set[0][1], TextureFormat.RGBA5551) + krobot_im = maskImageWithColor(krobot_im, tuple(krobot_color_list)) + writeColorImageToROM(krobot_im, 25, tex, tex_set[0][0], tex_set[0][1], False, TextureFormat.RGBA5551) + # Jetman + for xi, x in enumerate(settings.jetman_color): + ROM_COPY.seek(settings.rom_data + 0x1E8 + xi) + ROM_COPY.writeMultipleBytes(x, 1) + # Blast Barrels + blast_shift = getRandomHueShift() + hueShiftImageContainer(25, 0x127E, 1, 1372, TextureFormat.RGBA5551, blast_shift, ROM_COPY) + for x in range(4): + hueShiftImageContainer(25, 0x127F + x, 16, 64, TextureFormat.RGBA5551, blast_shift, ROM_COPY) + hueShiftImageContainer(25, getBonusSkinOffset(ExtraTextures.BlastTop), 1, 1372, TextureFormat.RGBA5551, blast_shift, ROM_COPY) + # K Rool + red_cs_im = Image.new(mode="RGBA", size=(32, 32), color=convertColorIntToTuple(getEnemySwapColor())) + shorts_im = Image.new(mode="RGBA", size=(32, 32), color=convertColorIntToTuple(getEnemySwapColor())) + glove_im = Image.new(mode="RGBA", size=(32, 32), color=convertColorIntToTuple(getEnemySwapColor())) + krool_data = { + 0x1149: red_cs_im, + 0x1261: shorts_im, + 0xDA8: glove_im, + } + if enemy_setting == RandomModels.extreme: + skin_im = Image.new(mode="RGBA", size=(32, 32), color=convertColorIntToTuple(getEnemySwapColor(80, min_channel_variance=80))) + krool_data[0x114A] = skin_im + krool_data[0x114D] = skin_im + for index in krool_data: + writeColorImageToROM(krool_data[index], 25, index, 32, 32, False, TextureFormat.RGBA5551) + toe_shift = getRandomHueShift() + hueShiftImageContainer(25, 0x126E, 1, 1372, TextureFormat.RGBA5551, toe_shift, ROM_COPY) + hueShiftImageContainer(25, 0x126F, 1, 1372, TextureFormat.RGBA5551, toe_shift, ROM_COPY) + if enemy_setting == RandomModels.extreme: + gold_shift = getRandomHueShift() + hueShiftImageContainer(25, 0x1265, 32, 32, TextureFormat.RGBA5551, gold_shift, ROM_COPY) + hueShiftImageContainer(25, 0x1148, 32, 32, TextureFormat.RGBA5551, gold_shift, ROM_COPY) + # Ghost + ghost_shift = getRandomHueShift() + for img in range(0x119D, 0x11AF): + px_count = 1372 + if img == 0x119E: + px_count = 176 + elif img == 0x11AC: + px_count = 688 + hueShiftImageContainer(25, img, 1, px_count, TextureFormat.RGBA5551, ghost_shift, ROM_COPY) + # Funky + funky_shift = getRandomHueShift() + hueShiftImageContainer(25, 0xECF, 1, 1372, TextureFormat.RGBA5551, funky_shift, ROM_COPY) + hueShiftImageContainer(25, 0xED6, 1, 1372, TextureFormat.RGBA5551, funky_shift, ROM_COPY) + hueShiftImageContainer(25, 0xEDF, 1, 1372, TextureFormat.RGBA5551, funky_shift, ROM_COPY) + # Zinger + zinger_shift = getRandomHueShift() + zinger_color = hueShiftColor((0xFF, 0xFF, 0x0A), zinger_shift) + zinger_color_int = (zinger_color[0] << 16) | (zinger_color[1] << 8) | (zinger_color[2]) + hueShiftImageContainer(25, 0xF0A, 1, 1372, TextureFormat.RGBA5551, zinger_shift, ROM_COPY) + # Mechazinger, use zinger color + for img_index in (0x10A0, 0x10A2, 0x10A4, 0x10A5): + hueShiftImageContainer(25, img_index, 1, 1372, TextureFormat.RGBA5551, zinger_shift, ROM_COPY) + hueShiftImageContainer(25, 0x10A3, 32, 32, TextureFormat.RGBA32, zinger_shift, ROM_COPY) + # Rings/DK Star + ring_shift = getRandomHueShift() + for x in range(2): + hueShiftImageContainer(25, 0xE1C + x, 1, 344, TextureFormat.RGBA5551, ring_shift, ROM_COPY) + hueShiftImageContainer(25, 0xD38 + x, 64, 32, TextureFormat.RGBA5551, ring_shift, ROM_COPY) + hueShiftImageContainer(7, 0x2EB, 32, 32, TextureFormat.RGBA5551, ring_shift, ROM_COPY) + # Buoys + for x in range(2): + hueShiftImageContainer(25, 0x133A + x, 1, 1372, TextureFormat.RGBA5551, getRandomHueShift(), ROM_COPY) + # Trap Bubble + hueShiftImageContainer(25, 0x134C, 32, 32, TextureFormat.RGBA5551, getRandomHueShift(), ROM_COPY) + # Spider + spider_shift = getRandomHueShift() + + for img_index in SPIDER_TEXTURE_DIMENSIONS: + hueShiftImageContainer, ROM_COPY( + 25, + img_index, + SPIDER_TEXTURE_DIMENSIONS[img_index][0], + SPIDER_TEXTURE_DIMENSIONS[img_index][1], + TextureFormat.RGBA5551, + spider_shift, + ) + + if enemy_setting == RandomModels.extreme: + # Army Dillo + dillo_px_count = { + 0x102D: 64 * 32, + 0x103A: 16 * 16, + 0x102A: 24 * 24, + 0x102B: 24 * 24, + 0x102C: 1372, + 0x103D: 688, + 0x103E: 688, + } + dillo_shift = getRandomHueShift() + for img, px_count in dillo_px_count.items(): + hueShiftImageContainer(25, img, 1, px_count, TextureFormat.RGBA5551, dillo_shift, ROM_COPY) + + # Mushrooms + mush_man_shift = getRandomHueShift() + for img_index in (0x11FC, 0x11FD, 0x11FE, 0x11FF, 0x1200, 0x1209, 0x120A, 0x120B): + hueShiftImageContainer(25, img_index, 1, 1372, TextureFormat.RGBA5551, mush_man_shift, ROM_COPY) + for img_index in (0x11F8, 0x1205): + hueShiftImageContainer(25, img_index, 1, 692, TextureFormat.RGBA5551, mush_man_shift, ROM_COPY) + for img_index in (0x67F, 0x680): + hueShiftImageContainer(25, img_index, 32, 64, TextureFormat.RGBA5551, mush_man_shift, ROM_COPY) + hueShiftImageContainer(25, 0x6F3, 4, 4, TextureFormat.RGBA5551, mush_man_shift, ROM_COPY) + adjustFungiMushVertexColor(mush_man_shift) + + # Enemy Vertex Swaps + blue_beaver_color = getEnemySwapColor(80, min_channel_variance=80) + enemy_changes = { + Model.BeaverBlue_LowPoly: EnemyColorSwap([0xB2E5FF, 0x65CCFF, 0x00ABE8, 0x004E82, 0x008BD1, 0x001333, 0x1691CE], blue_beaver_color), # Primary + Model.BeaverBlue: EnemyColorSwap([0xB2E5FF, 0x65CCFF, 0x00ABE8, 0x004E82, 0x008BD1, 0x001333, 0x1691CE], blue_beaver_color), # Primary + Model.BeaverGold: EnemyColorSwap([0xFFE5B2, 0xFFCC65, 0xE8AB00, 0x824E00, 0xD18B00, 0x331300, 0xCE9116]), # Primary + Model.Zinger: EnemyColorSwap([0xFFFF0A, 0xFF7F00], zinger_color_int), # Legs + Model.RoboZinger: EnemyColorSwap([0xFFFF00, 0xFF5500], zinger_color_int), # Legs + Model.Candy: EnemyColorSwap( + [ + 0xFF96EB, + 0x572C58, + 0xB86CAA, + 0xEB4C91, + 0x8B2154, + 0xD13B80, + 0xFF77C1, + 0xFF599E, + 0x7F1E4C, + 0x61173A, + 0x902858, + 0xA42E64, + 0x791C49, + 0x67183E, + 0x9E255C, + 0xC12E74, + 0x572C58, + 0xFF96EB, + 0xB86CAA, + ] + ), + Model.Laser: EnemyColorSwap([0xF30000]), + Model.Kasplat: EnemyColorSwap([0x8FD8FF, 0x182A4F, 0x0B162C, 0x7A98D3, 0x3F6CC4, 0x8FD8FF, 0x284581]), + # Model.BananaFairy: EnemyColorSwap([0xFFD400, 0xFFAA00, 0xFCD200, 0xD68F00, 0xD77D0A, 0xe49800, 0xdf7f1f, 0xa26c00, 0xd6b200, 0xdf9f1f]) + } + if enemy_setting == RandomModels.extreme: + enemy_changes[Model.Klump] = EnemyColorSwap([0xE66B78, 0x621738, 0x300F20, 0xD1426F, 0xA32859]) + dogadon_color = getEnemySwapColor(80, 160, min_channel_variance=80) + enemy_changes[Model.Dogadon] = EnemyColorSwap( + [ + 0xFF0000, + 0xFF7F00, + 0x450A1F, + 0xB05800, + 0xFF3200, + 0xFFD400, + 0x4F260D, + 0x600F00, + 0x6A1400, + 0xAA0000, + 0xDF3F1F, + 0xFF251F, + 0x8F4418, + 0x522900, + 0xDF9F1F, + 0x3B0606, + 0x91121E, + 0x700C0D, + 0xFF5900, + 0xFF7217, + 0xFF7425, + 0xFF470B, + 0xA82100, + 0x4A0D18, + 0x580E00, + 0x461309, + 0x4C1503, + 0x780D0E, + 0xFFA74A, + 0x7E120F, + 0x700000, + 0xB64D19, + 0x883A13, + 0xBD351A, + 0xD42900, + 0xFF2A00, + 0x921511, + 0x9C662D, + 0xDF5F1F, + 0x9B1112, + 0x461F0A, + 0x4B0808, + 0x500809, + 0xA42000, + 0x5F0B13, + 0xBF6A3F, + 0x602E10, + 0x971414, + 0x422C15, + 0xFC5800, + 0x5C0D0B, + ], + dogadon_color, + ) + for enemy in enemy_changes: + file_data = bytearray(getRawFile(5, enemy, True)) + vert_start = 0x28 + file_head = getValueFromByteArray(file_data, 0, 4) + disp_list_end = (getValueFromByteArray(file_data, 4, 4) - file_head) + 0x28 + vert_end = (getValueFromByteArray(file_data, disp_list_end, 4) - file_head) + 0x28 + vert_count = int((vert_end - vert_start) / 0x10) + for vert in range(vert_count): + local_start = 0x28 + (0x10 * vert) + test_rgb = getValueFromByteArray(file_data, local_start + 0xC, 3) + new_rgb = enemy_changes[enemy].getOutputColor(test_rgb) + for x in range(3): + shift = 8 * (2 - x) + channel = (new_rgb >> shift) & 0xFF + file_data[local_start + 0xC + x] = channel + file_data = gzip.compress(file_data, compresslevel=9) + ROM_COPY.seek(js.pointer_addresses[5]["entries"][enemy]["pointing_to"]) + ROM_COPY.writeBytes(file_data) \ No newline at end of file diff --git a/randomizer/Patching/Cosmetics/Holiday.py b/randomizer/Patching/Cosmetics/Holiday.py index 221164431..80ce1ee62 100644 --- a/randomizer/Patching/Cosmetics/Holiday.py +++ b/randomizer/Patching/Cosmetics/Holiday.py @@ -1,3 +1,4 @@ +"""All code associated with temporary holiday-based cosmetic effects.""" import gzip import js from PIL import Image, ImageEnhance diff --git a/randomizer/Patching/Cosmetics/KongColor.py b/randomizer/Patching/Cosmetics/KongColor.py new file mode 100644 index 000000000..105121bc4 --- /dev/null +++ b/randomizer/Patching/Cosmetics/KongColor.py @@ -0,0 +1,264 @@ +"""All code related to changing the color of kongs.""" +import js +import zlib +import gzip +import random +from randomizer.Enums.Kongs import Kongs +from randomizer.Enums.Settings import CharacterColors, KongModels +from randomizer.Settings import Settings +from randomizer.Patching.Lib import PaletteFillType, int_to_list +from randomizer.Patching.LibImage import getKongItemColor +from randomizer.Patching.generate_kong_color_images import convertColors +from randomizer.Patching.Cosmetics.Krusha import kong_index_mapping +from randomizer.Patching.Cosmetics.ModelSwaps import model_texture_sections +from randomizer.Patching.Patcher import LocalROM + +DEFAULT_COLOR = "#000000" + +class KongPalette: + """Class to store information regarding a kong palette.""" + + def __init__(self, name: str, image: int, fill_type: PaletteFillType, alt_name: str = None): + """Initialize with given parameters.""" + self.name = name + self.image = image + self.fill_type = fill_type + self.alt_name = alt_name + if alt_name is None: + self.alt_name = name + + +class KongPaletteSetting: + """Class to store information regarding the kong palette setting.""" + + def __init__(self, kong: str, kong_index: int, palettes: list[KongPalette]): + """Initialize with given parameters.""" + self.kong = kong + self.kong_index = kong_index + self.palettes = palettes.copy() + self.setting_kong = kong + +krusha_texture_replacement = { + # Textures Krusha can use when he replaces various kongs (Main color, belt color) + Kongs.donkey: (3724, 0x177D), + Kongs.diddy: (4971, 4966), + Kongs.lanky: (3689, 0xE9A), + Kongs.tiny: (6014, 0xE68), + Kongs.chunky: (3687, 3778), +} + +KONG_ZONES = { + "DK": ["Fur", "Tie"], + "Diddy": ["Clothes"], + "Lanky": ["Clothes", "Fur"], + "Tiny": ["Clothes", "Hair"], + "Chunky": ["Main", "Other"], + "Rambi": ["Skin"], + "Enguarde": ["Skin"], +} + +def writeKongColors(settings: Settings): + color_palettes = [] + color_obj = {} + colors_dict = {} + kong_settings = [ + KongPaletteSetting( + "dk", + 0, + [ + KongPalette("fur", 3724, PaletteFillType.block), + KongPalette("tie", 0x177D, PaletteFillType.block), + KongPalette("tie", 0xE8D, PaletteFillType.patch), + ], + ), + KongPaletteSetting( + "diddy", + 1, + [ + KongPalette("clothes", 3686, PaletteFillType.block), + ], + ), + KongPaletteSetting( + "lanky", + 2, + [ + KongPalette("clothes", 3689, PaletteFillType.block), + KongPalette("clothes", 3734, PaletteFillType.patch), + KongPalette("fur", 0xE9A, PaletteFillType.block), + KongPalette("fur", 0xE94, PaletteFillType.block), + ], + ), + KongPaletteSetting( + "tiny", + 3, + [ + KongPalette("clothes", 6014, PaletteFillType.block), + KongPalette("hair", 0xE68, PaletteFillType.block), + ], + ), + KongPaletteSetting( + "chunky", + 4, + [ + KongPalette("main", 3769, PaletteFillType.checkered, "other"), + KongPalette("main", 3687, PaletteFillType.block), + ], + ), + KongPaletteSetting( + "rambi", + 5, + [ + KongPalette("skin", 3826, PaletteFillType.block), + ], + ), + KongPaletteSetting( + "enguarde", + 6, + [ + KongPalette("skin", 3847, PaletteFillType.block), + ], + ), + ] + + if js.document.getElementById("override_cosmetics").checked or True: + if js.document.getElementById("random_colors").checked: + for kong in KONG_ZONES: + for zone in KONG_ZONES[kong]: + settings.__setattr__(f"{kong.lower()}_{zone.lower()}_colors", CharacterColors.randomized) + else: + for kong in KONG_ZONES: + for zone in KONG_ZONES[kong]: + settings.__setattr__( + f"{kong.lower()}_{zone.lower()}_colors", + CharacterColors[js.document.getElementById(f"{kong.lower()}_{zone.lower()}_colors").value], + ) + settings.__setattr__( + f"{kong.lower()}_{zone.lower()}_custom_color", + js.document.getElementById(f"{kong.lower()}_{zone.lower()}_custom_color").value, + ) + else: + if settings.random_colors: + for kong in KONG_ZONES: + for zone in KONG_ZONES[kong]: + settings.__setattr__(f"{kong.lower()}_{zone.lower()}_colors", CharacterColors.randomized) + + colors_dict = {} + for kong in KONG_ZONES: + for zone in KONG_ZONES[kong]: + colors_dict[f"{kong.lower()}_{zone.lower()}_colors"] = settings.__getattribute__(f"{kong.lower()}_{zone.lower()}_colors") + colors_dict[f"{kong.lower()}_{zone.lower()}_custom_color"] = settings.__getattribute__(f"{kong.lower()}_{zone.lower()}_custom_color") + for kong in kong_settings: + if kong.kong_index == 4: + if settings.kong_model_chunky == KongModels.disco_chunky: + kong.palettes = [ + KongPalette("main", 3777, PaletteFillType.sparkle), + KongPalette("other", 3778, PaletteFillType.sparkle), + ] + settings_values = [ + settings.kong_model_dk, + settings.kong_model_diddy, + settings.kong_model_lanky, + settings.kong_model_tiny, + settings.kong_model_chunky, + ] + if kong.kong_index >= 0 and kong.kong_index < len(settings_values): + if settings_values[kong.kong_index] in model_texture_sections: + base_setting = kong.palettes[0].name + kong.palettes = [ + KongPalette(base_setting, krusha_texture_replacement[kong.kong_index][0], PaletteFillType.block), # krusha_skin + KongPalette(base_setting, krusha_texture_replacement[kong.kong_index][1], PaletteFillType.kong), # krusha_indicator + ] + base_obj = {"kong": kong.kong, "zones": []} + zone_to_colors = {} + for palette in kong.palettes: + arr = [DEFAULT_COLOR] + if palette.fill_type == PaletteFillType.checkered: + arr = ["#FFFF00", "#00FF00"] + elif palette.fill_type == PaletteFillType.kong: + arr = [getKongItemColor(settings.colorblind_mode, kong.kong_index)] + zone_data = { + "zone": palette.name, + "image": palette.image, + "fill_type": palette.fill_type, + "colors": arr, + } + for index in range(len(arr)): + base_setting = f"{kong.kong}_{palette.name}_colors" + custom_setting = f"{kong.kong}_{palette.name}_custom_color" + if index == 1: # IS THE CHECKERED PATTERN + base_setting = f"{kong.kong}_{palette.alt_name}_colors" + custom_setting = f"{kong.kong}_{palette.alt_name}_custom_color" + if (settings.override_cosmetics and colors_dict[base_setting] != CharacterColors.vanilla) or (palette.fill_type == PaletteFillType.kong): + color = None + # if this palette color is randomized, and isn't krusha's kong indicator: + if colors_dict[base_setting] == CharacterColors.randomized and palette.fill_type != PaletteFillType.kong: + if base_setting in zone_to_colors: + color = zone_to_colors[base_setting] + else: + color = f"#{format(random.randint(0, 0xFFFFFF), '06x')}" + zone_to_colors[base_setting] = color + # if this palette color is not randomized (but might be a custom color) and isn't krusha's kong indicator: + elif palette.fill_type != PaletteFillType.kong: + color = colors_dict[custom_setting] + if not color: + color = DEFAULT_COLOR + # if this is krusha's kong indicator: + else: + color = getKongItemColor(settings.colorblind_mode, kong.kong_index) + if color is not None: + zone_data["colors"][index] = color + base_obj["zones"].append(zone_data) + color_palettes.append(base_obj) + color_obj[f"{kong.kong} {palette.name}"] = color + settings.colors = color_obj + if len(color_palettes) > 0: + # this is just to prune the duplicates that appear. someone should probably fix the root of the dupe issue tbh + new_color_palettes = [] + for pal in color_palettes: + if pal not in new_color_palettes: + new_color_palettes.append(pal) + convertColors(new_color_palettes) + +def changeModelTextures(settings: Settings, kong_index: int): + """Change the textures associated with a model.""" + settings_values = [ + settings.kong_model_dk, + settings.kong_model_diddy, + settings.kong_model_lanky, + settings.kong_model_tiny, + settings.kong_model_chunky, + ] + if kong_index < 0 or kong_index >= len(settings_values): + return + model = settings_values[kong_index] + if model not in model_texture_sections: + return + ROM_COPY = LocalROM() + for x in range(2): + file = kong_index_mapping[kong_index][x] + if file is None: + continue + krusha_model_start = js.pointer_addresses[5]["entries"][file]["pointing_to"] + krusha_model_finish = js.pointer_addresses[5]["entries"][file + 1]["pointing_to"] + krusha_model_size = krusha_model_finish - krusha_model_start + ROM_COPY.seek(krusha_model_start) + indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") + ROM_COPY.seek(krusha_model_start) + data = ROM_COPY.readBytes(krusha_model_size) + if indicator == 0x1F8B: + data = zlib.decompress(data, (15 + 32)) + num_data = [] # data, but represented as nums rather than b strings + for d in data: + num_data.append(d) + # Retexture for colors + for tex_idx in model_texture_sections[model]["skin"]: + for di, d in enumerate(int_to_list(krusha_texture_replacement[kong_index][0], 2)): # Main + num_data[tex_idx + di] = d + for tex_idx in model_texture_sections[model]["kong"]: + for di, d in enumerate(int_to_list(krusha_texture_replacement[kong_index][1], 2)): # Belt + num_data[tex_idx + di] = d + data = bytearray(num_data) # convert num_data back to binary string + if indicator == 0x1F8B: + data = gzip.compress(data, compresslevel=9) + ROM_COPY.seek(krusha_model_start) + ROM_COPY.writeBytes(data) \ No newline at end of file diff --git a/randomizer/Patching/Cosmetics/Krusha.py b/randomizer/Patching/Cosmetics/Krusha.py index 3652dca9d..86e6544eb 100644 --- a/randomizer/Patching/Cosmetics/Krusha.py +++ b/randomizer/Patching/Cosmetics/Krusha.py @@ -1,3 +1,4 @@ +"""All code associated with Krusha.""" import js import zlib import gzip @@ -14,9 +15,7 @@ TextureFormat, ) from randomizer.Enums.Kongs import Kongs - -if TYPE_CHECKING: - from PIL.Image import Image +from PIL import Image DK_SCALE = 0.75 GENERIC_SCALE = 0.49 diff --git a/randomizer/Patching/Cosmetics/ModelSwaps.py b/randomizer/Patching/Cosmetics/ModelSwaps.py index b466651c1..6e97fd2ac 100644 --- a/randomizer/Patching/Cosmetics/ModelSwaps.py +++ b/randomizer/Patching/Cosmetics/ModelSwaps.py @@ -1,4 +1,12 @@ +"""All code associated with model swaps.""" +import random +import js from randomizer.Enums.Models import Model, Sprite +from randomizer.Enums.Maps import Maps +from randomizer.Enums.Settings import KongModels, RandomModels +from randomizer.Settings import Settings +from randomizer.Patching.Patcher import ROM +from randomizer.Patching.Lib import applyCharacterSpawnerChanges, SpawnerChange turtle_models = [ Model.Diddy, # Diddy @@ -330,4 +338,161 @@ Sprite.ChunkyCoin, Sprite.Fairy, Sprite.RaceCoin, -] \ No newline at end of file +] + +model_mapping = { + KongModels.default: 0, + KongModels.disco_chunky: 6, + KongModels.krusha: 7, + KongModels.krool_cutscene: 9, + KongModels.krool_fight: 8, + KongModels.cranky: 10, + KongModels.candy: 11, + KongModels.funky: 12, +} + +model_texture_sections = { + KongModels.krusha: { + "skin": [0x4738, 0x2E96, 0x3A5E], + "kong": [0x3126, 0x354E, 0x37FE, 0x41E6], + }, + KongModels.krool_fight: { + "skin": [ + 0x61D6, + 0x63FE, + 0x6786, + 0x7DD6, + 0x7E8E, + 0x7F3E, + 0x7FEE, + 0x5626, + 0x56E6, + 0x5A86, + 0x5BAE, + 0x5D46, + 0x5E2E, + 0x5FAE, + 0x69BE, + 0x735E, + 0x7C5E, + 0x7E4E, + 0x7EF6, + 0x7FA6, + 0x8056, + ], + "kong": [0x607E, 0x7446, 0x7D46, 0x80FE], + }, + # KongModels.krool_cutscene: { + # "skin": [0x4A6E, 0x4CBE, 0x52AE, 0x55BE, 0x567E, 0x57E6, 0x5946, 0x5AA6, 0x5E06, 0x5EC6, 0x6020, 0x618E, 0x62F6, 0x6946, 0x6A6E, 0x6C5E, 0x6D86, 0x6F76, 0x702E, 0x70DE, 0x718E, 0x72FE, 0x4FBE, 0x51FE, 0x5C26, 0x6476, 0x6826, 0x6B26, 0x6E3E, 0x6FE6, 0x7096, 0x7146, 0x71F6, 0x733E, 0x743E], + # "kong": [], + # } +} + +KLAPTRAPS = [Model.KlaptrapGreen, Model.KlaptrapPurple, Model.KlaptrapRed] + +def getRandomKlaptrapModel() -> Model: + """Get random klaptrap model.""" + return random.choice(KLAPTRAPS) + +def applyCosmeticModelSwaps(settings: Settings, ROM_COPY: ROM): + """Apply model swaps to the settings dict.""" + sav = settings.rom_data + + bother_model_index = Model.KlaptrapGreen + panic_fairy_model_index = Model.BananaFairy + panic_klap_model_index = Model.KlaptrapGreen + turtle_model_index = Model.Turtle + sseek_klap_model_index = Model.KlaptrapGreen + fungi_tomato_model_index = Model.Tomato + caves_tomato_model_index = Model.IceTomato + racer_beetle = Model.Beetle + racer_rabbit = Model.Rabbit + piano_burper = Model.KoshKremlingRed + spotlight_fish_model_index = Model.SpotlightFish + candy_model_index = Model.Candy + funky_model_index = Model.Funky + boot_model_index = Model.Boot + melon_sprite = Sprite.BouncingMelon + swap_bitfield = 0 + + model_inverse_mapping = {} + for model in model_mapping: + val = model_mapping[model] + model_inverse_mapping[val] = model + + ROM_COPY.seek(settings.rom_data + 0x1B8) + settings.kong_model_dk = model_inverse_mapping[int.from_bytes(ROM_COPY.readBytes(1), "big")] + settings.kong_model_diddy = model_inverse_mapping[int.from_bytes(ROM_COPY.readBytes(1), "big")] + settings.kong_model_lanky = model_inverse_mapping[int.from_bytes(ROM_COPY.readBytes(1), "big")] + settings.kong_model_tiny = model_inverse_mapping[int.from_bytes(ROM_COPY.readBytes(1), "big")] + settings.kong_model_chunky = model_inverse_mapping[int.from_bytes(ROM_COPY.readBytes(1), "big")] + + if settings.override_cosmetics: + model_setting = RandomModels[js.document.getElementById("random_models").value] + else: + model_setting = settings.random_models + if model_setting == RandomModels.random: + bother_model_index = getRandomKlaptrapModel() + elif model_setting == RandomModels.extreme: + bother_model_index = random.choice(bother_models) + racer_beetle = random.choice([Model.Beetle, Model.Rabbit]) + racer_rabbit = random.choice([Model.Beetle, Model.Rabbit]) + if racer_rabbit == Model.Beetle: + spawner_changes = [] + # Fungi + rabbit_race_fungi_change = SpawnerChange(Maps.FungiForest, 2) + rabbit_race_fungi_change.new_scale = 50 + rabbit_race_fungi_change.new_speed_0 = 70 + rabbit_race_fungi_change.new_speed_1 = 136 + spawner_changes.append(rabbit_race_fungi_change) + # Caves + rabbit_caves_change = SpawnerChange(Maps.CavesChunkyIgloo, 1) + rabbit_caves_change.new_scale = 40 + spawner_changes.append(rabbit_caves_change) + applyCharacterSpawnerChanges(spawner_changes) + if model_setting != RandomModels.off: + panic_fairy_model_index = random.choice(panic_models) + turtle_model_index = random.choice(turtle_models) + panic_klap_model_index = getRandomKlaptrapModel() + sseek_klap_model_index = getRandomKlaptrapModel() + fungi_tomato_model_index = random.choice([Model.Tomato, Model.IceTomato]) + caves_tomato_model_index = random.choice([Model.Tomato, Model.IceTomato]) + referenced_piano_models = piano_models.copy() + referenced_funky_models = funky_cutscene_models.copy() + if model_setting == RandomModels.extreme: + referenced_piano_models.extend(piano_extreme_model) + spotlight_fish_model_index = random.choice(spotlight_fish_models) + referenced_funky_models.extend(funky_cutscene_models_extreme) + boot_model_index = random.choice(boot_cutscene_models) + piano_burper = random.choice(referenced_piano_models) + candy_model_index = random.choice(candy_cutscene_models) + funky_model_index = random.choice(funky_cutscene_models) + settings.bother_klaptrap_model = bother_model_index + settings.beetle_model = racer_beetle + settings.rabbit_model = racer_rabbit + settings.panic_fairy_model = panic_fairy_model_index + settings.turtle_model = turtle_model_index + settings.panic_klaptrap_model = panic_klap_model_index + settings.seek_klaptrap_model = sseek_klap_model_index + settings.fungi_tomato_model = fungi_tomato_model_index + settings.caves_tomato_model = caves_tomato_model_index + settings.piano_burp_model = piano_burper + settings.spotlight_fish_model = spotlight_fish_model_index + settings.candy_cutscene_model = candy_model_index + settings.funky_cutscene_model = funky_model_index + settings.boot_cutscene_model = boot_model_index + settings.wrinkly_rgb = [255, 255, 255] + # Compute swap bitfield + swap_bitfield |= 0x10 if settings.rabbit_model == Model.Beetle else 0 + swap_bitfield |= 0x20 if settings.beetle_model == Model.Rabbit else 0 + swap_bitfield |= 0x40 if settings.fungi_tomato_model == Model.IceTomato else 0 + swap_bitfield |= 0x80 if settings.caves_tomato_model == Model.Tomato else 0 + if settings.misc_cosmetics and settings.override_cosmetics: + melon_sprite = random.choice(melon_random_sprites) + settings.wrinkly_rgb = [random.randint(0, 255) for _ in range(3)] + settings.minigame_melon_sprite = melon_sprite + # Write Models + ROM_COPY.seek(sav + 0x1B5) + ROM_COPY.writeMultipleBytes(settings.panic_fairy_model + 1, 1) # Still needed for end seq fairy swap + ROM_COPY.seek(sav + 0x1E2) + ROM_COPY.write(swap_bitfield) \ No newline at end of file diff --git a/randomizer/Patching/Cosmetics/Puzzles.py b/randomizer/Patching/Cosmetics/Puzzles.py index cd8d8df70..dad9addcd 100644 --- a/randomizer/Patching/Cosmetics/Puzzles.py +++ b/randomizer/Patching/Cosmetics/Puzzles.py @@ -1,10 +1,7 @@ -from typing import TYPE_CHECKING - +"""All code associated with updating textures for puzzles.""" from randomizer.Settings import Settings from randomizer.Patching.LibImage import writeColorImageToROM, TextureFormat, getImageFile, getNumberImage - -if TYPE_CHECKING: - from PIL.Image import Image +from PIL import Image def updateMillLeverTexture(settings: Settings) -> None: """Update the 21132 texture.""" diff --git a/randomizer/Patching/Cosmetics/TextRando.py b/randomizer/Patching/Cosmetics/TextRando.py index aba25b0d3..64343aa6d 100644 --- a/randomizer/Patching/Cosmetics/TextRando.py +++ b/randomizer/Patching/Cosmetics/TextRando.py @@ -1,3 +1,4 @@ +"""All code associated with cosmetic tweaks to text.""" import random from randomizer.Patching.Patcher import LocalROM from randomizer.Patching.Lib import writeText, grabText diff --git a/randomizer/Patching/Lib.py b/randomizer/Patching/Lib.py index 5a784edfd..3d13b16e7 100644 --- a/randomizer/Patching/Lib.py +++ b/randomizer/Patching/Lib.py @@ -1129,6 +1129,15 @@ def getProgHintBarrierItem(item: ProgressiveHintItem) -> BarrierItems: } return barrier_bijection[item] +def getValueFromByteArray(ba: bytearray, offset: int, size: int) -> int: + """Get value from byte array given an offset and size.""" + value = 0 + for x in range(size): + local_value = ba[offset + x] + value <<= 8 + value += local_value + return value + class Holidays(IntEnum): """Holiday Enum.""" diff --git a/randomizer/Patching/LibImage.py b/randomizer/Patching/LibImage.py index a046530a7..d582aeafa 100644 --- a/randomizer/Patching/LibImage.py +++ b/randomizer/Patching/LibImage.py @@ -427,9 +427,12 @@ def getColorBase(mode: ColorblindMode) -> list[str]: return ["#000000", "#C72020", "#13C4D8", "#FFFFFF", "#FFA4A4"] return ["#FFD700", "#FF0000", "#1699FF", "#B045FF", "#41FF25"] -def getKongItemColor(mode: ColorblindMode, kong: Kongs) -> str: +def getKongItemColor(mode: ColorblindMode, kong: Kongs, output_as_list: bool = False) -> str: """Get the color assigned to a kong.""" - return getColorBase(mode)[kong] + hash_str = getColorBase(mode)[kong] + if output_as_list: + return getRGBFromHash(hash_str) + return hash_str def maskImage(im_f, base_index, min_y, keep_dark=False, mode = ColorblindMode.off): """Apply RGB mask to image.""" @@ -442,7 +445,7 @@ def maskImage(im_f, base_index, min_y, keep_dark=False, mode = ColorblindMode.of im_dupe = brightener.enhance(2) im_f.paste(im_dupe, (0, min_y), im_dupe) pix = im_f.load() - mask = getRGBFromHash(getKongItemColor(mode, base_index)) + mask = getKongItemColor(mode, base_index, True) w, h = im_f.size for x in range(w): for y in range(min_y, h): @@ -453,7 +456,7 @@ def maskImage(im_f, base_index, min_y, keep_dark=False, mode = ColorblindMode.of pix[x, y] = (base[0], base[1], base[2], base[3]) return im_f -def hueShiftImageContainer(table: int, image: int, width: int, height: int, format: TextureFormat, shift: int): +def hueShiftImageContainer(table: int, image: int, width: int, height: int, format: TextureFormat, shift: int, ROM_COPY: ROM = None): """Load an image, shift the hue and rewrite it back to ROM.""" loaded_im = getImageFile(table, image, table != 7, width, height, format) loaded_im = hueShift(loaded_im, shift) @@ -474,5 +477,64 @@ def hueShiftImageContainer(table: int, image: int, width: int, height: int, form px_data = bytearray(bytes_array) if table != 7: px_data = gzip.compress(px_data, compresslevel=9) - ROM().seek(js.pointer_addresses[table]["entries"][image]["pointing_to"]) - ROM().writeBytes(px_data) \ No newline at end of file + if ROM_COPY is None: + ROM_COPY = ROM() + ROM_COPY.seek(js.pointer_addresses[table]["entries"][image]["pointing_to"]) + ROM_COPY.writeBytes(px_data) + +def getLuma(color: tuple) -> float: + """Get the luma value of a color.""" + return (0.299 * color[0]) + (0.587 * color[1]) + (0.114 * color[2]) + +def hueShiftColor(color: tuple, amount: int, head_ratio: int = None) -> tuple: + """Apply a hue shift to a color.""" + # RGB -> HSV Conversion + red_ratio = color[0] / 255 + green_ratio = color[1] / 255 + blue_ratio = color[2] / 255 + color_max = max(red_ratio, green_ratio, blue_ratio) + color_min = min(red_ratio, green_ratio, blue_ratio) + color_delta = color_max - color_min + hue = 0 + if color_delta != 0: + if color_max == red_ratio: + hue = 60 * (((green_ratio - blue_ratio) / color_delta) % 6) + elif color_max == green_ratio: + hue = 60 * (((blue_ratio - red_ratio) / color_delta) + 2) + else: + hue = 60 * (((red_ratio - green_ratio) / color_delta) + 4) + sat = 0 if color_max == 0 else color_delta / color_max + val = color_max + # Adjust Hue + if head_ratio is not None and sat != 0: + amount = head_ratio / (sat * 100) + hue = (hue + amount) % 360 + # HSV -> RGB Conversion + c = val * sat + x = c * (1 - abs(((hue / 60) % 2) - 1)) + m = val - c + if hue < 60: + red_ratio = c + green_ratio = x + blue_ratio = 0 + elif hue < 120: + red_ratio = x + green_ratio = c + blue_ratio = 0 + elif hue < 180: + red_ratio = 0 + green_ratio = c + blue_ratio = x + elif hue < 240: + red_ratio = 0 + green_ratio = x + blue_ratio = c + elif hue < 300: + red_ratio = x + green_ratio = 0 + blue_ratio = c + else: + red_ratio = c + green_ratio = 0 + blue_ratio = x + return (int((red_ratio + m) * 255), int((green_ratio + m) * 255), int((blue_ratio + m) * 255)) \ No newline at end of file From 965332fe8392dfb95931be695d58b9c7527e42cf Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Thu, 26 Dec 2024 21:06:36 -0600 Subject: [PATCH 03/13] Remove unnecessary prints --- randomizer/Patching/ASMPatcher.py | 2 -- randomizer/ShuffleDoors.py | 1 - 2 files changed, 3 deletions(-) diff --git a/randomizer/Patching/ASMPatcher.py b/randomizer/Patching/ASMPatcher.py index 8b49e57a4..a4a31f7f2 100644 --- a/randomizer/Patching/ASMPatcher.py +++ b/randomizer/Patching/ASMPatcher.py @@ -266,8 +266,6 @@ def getROMAddress(address: int, overlay: Overlay, offset_dict: dict) -> int: raise Exception(f"Seeking out of bounds for this overlay. Attempted to seek to {hex(address)} in overlay {overlay.name}") if address < rdram_start: raise Exception(f"Seeking out of bounds for this overlay. Attempted to seek to {hex(address)} in overlay {overlay.name}") - if overlay == Overlay.Boot: - print(hex(rdram_start), hex(overlay_start), hex(overlay_start + (address - rdram_start))) return overlay_start + (address - rdram_start) diff --git a/randomizer/ShuffleDoors.py b/randomizer/ShuffleDoors.py index b1ae14910..b0aa3ff9e 100644 --- a/randomizer/ShuffleDoors.py +++ b/randomizer/ShuffleDoors.py @@ -241,7 +241,6 @@ def ShuffleDoors(spoiler, vanilla_doors_placed: bool): shuffled_door_data[level].append((selected_door_index, DoorType.dk_portal)) # Track all touched doors in a variable and put it in the spoiler because changes to the static list do not save - print(shuffled_door_data) spoiler.shuffled_door_data = shuffled_door_data # Give human text to spoiler log if shuffle_wrinkly: From e25dbb73397bd54a7427eefcf7d687ca9aae1daf Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Thu, 26 Dec 2024 21:13:34 -0600 Subject: [PATCH 04/13] Remove some unnecessary initializations of ROM() --- randomizer/Patching/CosmeticColors.py | 14 +-- randomizer/Patching/Cosmetics/Colorblind.py | 114 ++++++++++---------- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/randomizer/Patching/CosmeticColors.py b/randomizer/Patching/CosmeticColors.py index 36bb49530..ec98edd2c 100644 --- a/randomizer/Patching/CosmeticColors.py +++ b/randomizer/Patching/CosmeticColors.py @@ -358,7 +358,7 @@ def overwrite_object_colors(settings, ROM_COPY: ROM): galleon_switch_value = int.from_bytes(ROM_COPY.readBytes(1), "big") if mode != ColorblindMode.off: if mode in (ColorblindMode.prot, ColorblindMode.deut): - recolorBells() + recolorBells(ROM_COPY) # Preload DK single cb image to paste onto balloons file = 175 dk_single = getImageFile(7, file, False, 44, 44, TextureFormat.RGBA5551) @@ -367,13 +367,13 @@ def overwrite_object_colors(settings, ROM_COPY: ROM): # Preload blueprint images. Lanky's blueprint image is so much easier to mask, because it is blue, and the frame is brown for file in range(8): blueprint_lanky.append(getImageFile(25, 5519 + (file), True, 48, 42, TextureFormat.RGBA5551)) - writeWhiteKasplatHairColorToROM("#FFFFFF", "#000000", 25, 4125, TextureFormat.RGBA5551) - recolorWrinklyDoors(mode) + writeWhiteKasplatHairColorToROM("#FFFFFF", "#000000", 25, 4125, TextureFormat.RGBA5551, ROM_COPY) + recolorWrinklyDoors(mode, ROM_COPY) recolorSlamSwitches(galleon_switch_value, ROM_COPY, mode) recolorRotatingRoomTiles(mode) - recolorBlueprintModelTwo(mode) - recolorKlaptraps(mode) - recolorPotions(mode) + recolorBlueprintModelTwo(mode, ROM_COPY) + recolorKlaptraps(mode, ROM_COPY) + recolorPotions(mode, ROM_COPY) recolorMushrooms(mode) for kong_index in range(5): # file = 4120 @@ -381,7 +381,7 @@ def overwrite_object_colors(settings, ROM_COPY: ROM): # hair_im = getFile(25, file, True, 32, 44, TextureFormat.RGBA5551) # hair_im = maskImage(hair_im, kong_index, 0) # writeColorImageToROM(hair_im, 25, [4124, 4122, 4123, 4120, 4121][kong_index], 32, 44, False) - writeKasplatHairColorToROM(getKongItemColor(mode, kong_index), 25, [4124, 4122, 4123, 4120, 4121][kong_index], TextureFormat.RGBA5551) + writeKasplatHairColorToROM(getKongItemColor(mode, kong_index), 25, [4124, 4122, 4123, 4120, 4121][kong_index], TextureFormat.RGBA5551, ROM_COPY) for file in range(5519, 5527): # Blueprint sprite blueprint_start = [5624, 5608, 5519, 5632, 5616] diff --git a/randomizer/Patching/Cosmetics/Colorblind.py b/randomizer/Patching/Cosmetics/Colorblind.py index 31fdfa410..c1a165b3e 100644 --- a/randomizer/Patching/Cosmetics/Colorblind.py +++ b/randomizer/Patching/Cosmetics/Colorblind.py @@ -18,7 +18,7 @@ from randomizer.Enums.Kongs import Kongs from PIL import ImageEnhance -def writeKasplatHairColorToROM(color, table_index, file_index, format: str): +def writeKasplatHairColorToROM(color, table_index, file_index, format: str, ROM_COPY: ROM): """Write color to ROM for kasplats.""" file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] mask = getRGBFromHash(color) @@ -46,11 +46,11 @@ def writeKasplatHairColorToROM(color, table_index, file_index, format: str): data = bytearray(bytes_array) if table_index == 25: data = gzip.compress(data, compresslevel=9) - ROM().seek(file_start) - ROM().writeBytes(data) + ROM_COPY.seek(file_start) + ROM_COPY.writeBytes(data) -def writeWhiteKasplatHairColorToROM(color1, color2, table_index, file_index, format: str): +def writeWhiteKasplatHairColorToROM(color1, color2, table_index, file_index, format: str, ROM_COPY: ROM): """Write color to ROM for white kasplats, giving them a black-white block pattern.""" file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] mask = getRGBFromHash(color1) @@ -89,11 +89,11 @@ def writeWhiteKasplatHairColorToROM(color1, color2, table_index, file_index, for data = bytearray(bytes_array) if table_index == 25: data = gzip.compress(data, compresslevel=9) - ROM().seek(file_start) - ROM().writeBytes(data) + ROM_COPY.seek(file_start) + ROM_COPY.writeBytes(data) -def writeKlaptrapSkinColorToROM(color_index, table_index, file_index, format: str, mode: ColorblindMode): +def writeKlaptrapSkinColorToROM(color_index, table_index, file_index, format: str, mode: ColorblindMode, ROM_COPY: ROM): """Write color to ROM for klaptraps.""" im_f = getImageFile(table_index, file_index, True, 32, 43, format) im_f = maskImage(im_f, color_index, 0, (color_index != 3), mode) @@ -119,11 +119,11 @@ def writeKlaptrapSkinColorToROM(color_index, table_index, file_index, format: st data = bytearray(bytes_array) if table_index == 25: data = gzip.compress(data, compresslevel=9) - ROM().seek(file_start) - ROM().writeBytes(data) + ROM_COPY.seek(file_start) + ROM_COPY.writeBytes(data) -def writeSpecialKlaptrapTextureToROM(color_index, table_index, file_index, format: str, pixels_to_ignore: list, mode: ColorblindMode): +def writeSpecialKlaptrapTextureToROM(color_index, table_index, file_index, format: str, pixels_to_ignore: list, mode: ColorblindMode, ROM_COPY: ROM): """Write color to ROM for klaptraps special texture(s).""" im_f = getImageFile(table_index, file_index, True, 32, 43, format) pix_original = im_f.load() @@ -164,8 +164,8 @@ def writeSpecialKlaptrapTextureToROM(color_index, table_index, file_index, forma data = bytearray(bytes_array) if table_index == 25: data = gzip.compress(data, compresslevel=9) - ROM().seek(file_start) - ROM().writeBytes(data) + ROM_COPY.seek(file_start) + ROM_COPY.writeBytes(data) def calculateKlaptrapPixel(mask: list, format: str): @@ -275,17 +275,17 @@ def maskPotionImage(im_f, primary_color, secondary_color=None): return im_f -def recolorWrinklyDoors(mode: ColorblindMode): +def recolorWrinklyDoors(mode: ColorblindMode, ROM_COPY: ROM): """Recolor the Wrinkly hint door doorframes for colorblind mode.""" file = [0xF0, 0xF2, 0xEF, 0x67, 0xF1] for kong in range(5): wrinkly_door_start = js.pointer_addresses[4]["entries"][file[kong]]["pointing_to"] wrinkly_door_finish = js.pointer_addresses[4]["entries"][file[kong] + 1]["pointing_to"] wrinkly_door_size = wrinkly_door_finish - wrinkly_door_start - ROM().seek(wrinkly_door_start) - indicator = int.from_bytes(ROM().readBytes(2), "big") - ROM().seek(wrinkly_door_start) - data = ROM().readBytes(wrinkly_door_size) + ROM_COPY.seek(wrinkly_door_start) + indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") + ROM_COPY.seek(wrinkly_door_start) + data = ROM_COPY.readBytes(wrinkly_door_size) if indicator == 0x1F8B: data = zlib.decompress(data, (15 + 32)) num_data = [] # data, but represented as nums rather than b strings @@ -400,8 +400,8 @@ def recolorWrinklyDoors(mode: ColorblindMode): data = bytearray(num_data) # convert num_data back to binary string if indicator == 0x1F8B: data = gzip.compress(data, compresslevel=9) - ROM().seek(wrinkly_door_start) - ROM().writeBytes(data) + ROM_COPY.seek(wrinkly_door_start) + ROM_COPY.writeBytes(data) def recolorKRoolShipSwitch(color: tuple, ROM_COPY: ROM): """Recolors the simian slam switch that is part of K. Rool's ship in galleon.""" @@ -539,10 +539,10 @@ def recolorSlamSwitches(galleon_switch_value, ROM_COPY: ROM, mode: ColorblindMod slam_switch_start = js.pointer_addresses[4]["entries"][file[switch]]["pointing_to"] slam_switch_finish = js.pointer_addresses[4]["entries"][file[switch] + 1]["pointing_to"] slam_switch_size = slam_switch_finish - slam_switch_start - ROM().seek(slam_switch_start) - indicator = int.from_bytes(ROM().readBytes(2), "big") - ROM().seek(slam_switch_start) - data = ROM().readBytes(slam_switch_size) + ROM_COPY.seek(slam_switch_start) + indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") + ROM_COPY.seek(slam_switch_start) + data = ROM_COPY.readBytes(slam_switch_size) if indicator == 0x1F8B: data = zlib.decompress(data, (15 + 32)) num_data = [] # data, but represented as nums rather than b strings @@ -573,8 +573,8 @@ def recolorSlamSwitches(galleon_switch_value, ROM_COPY: ROM, mode: ColorblindMod data = bytearray(num_data) # convert num_data back to binary string if indicator == 0x1F8B: data = gzip.compress(data, compresslevel=9) - ROM().seek(slam_switch_start) - ROM().writeBytes(data) + ROM_COPY.seek(slam_switch_start) + ROM_COPY.writeBytes(data) if not written_galleon_ship: galleon_switch_color = new_color1.copy() if galleon_switch_value is not None: @@ -586,17 +586,17 @@ def recolorSlamSwitches(galleon_switch_value, ROM_COPY: ROM, mode: ColorblindMod written_galleon_ship = True -def recolorBlueprintModelTwo(mode: ColorblindMode): +def recolorBlueprintModelTwo(mode: ColorblindMode, ROM_COPY: ROM): """Recolor the Blueprint Model2 items for colorblind mode.""" file = [0xDE, 0xE0, 0xE1, 0xDD, 0xDF] for kong in range(5): blueprint_model2_start = js.pointer_addresses[4]["entries"][file[kong]]["pointing_to"] blueprint_model2_finish = js.pointer_addresses[4]["entries"][file[kong] + 1]["pointing_to"] blueprint_model2_size = blueprint_model2_finish - blueprint_model2_start - ROM().seek(blueprint_model2_start) - indicator = int.from_bytes(ROM().readBytes(2), "big") - ROM().seek(blueprint_model2_start) - data = ROM().readBytes(blueprint_model2_size) + ROM_COPY.seek(blueprint_model2_start) + indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") + ROM_COPY.seek(blueprint_model2_start) + data = ROM_COPY.readBytes(blueprint_model2_size) if indicator == 0x1F8B: data = zlib.decompress(data, (15 + 32)) num_data = [] # data, but represented as nums rather than b strings @@ -634,8 +634,8 @@ def recolorBlueprintModelTwo(mode: ColorblindMode): data = bytearray(num_data) # convert num_data back to binary string if indicator == 0x1F8B: data = gzip.compress(data, compresslevel=9) - ROM().seek(blueprint_model2_start) - ROM().writeBytes(data) + ROM_COPY.seek(blueprint_model2_start) + ROM_COPY.writeBytes(data) def maskImageRotatingRoomTile(im_f, im_mask, paste_coords, image_color_index, tile_side, mode: ColorblindMode): """Apply RGB mask to image of a Rotating Room Memory Tile.""" @@ -758,16 +758,16 @@ def recolorRotatingRoomTiles(mode): masked_tile = maskImageRotatingRoomTile(tile_image, mask, face_offsets[int(tile / 2)], face_index, (int(tile / 2) % 2), mode) writeColorImageToROM(masked_tile, 7, face_tiles[tile], 32, 64, False, TextureFormat.RGBA5551) -def recolorBells(): +def recolorBells(ROM_COPY: ROM): """Recolor the Chunky Minecart bells for colorblind mode (prot/deut).""" file = 693 minecart_bell_start = js.pointer_addresses[4]["entries"][file]["pointing_to"] minecart_bell_finish = js.pointer_addresses[4]["entries"][file + 1]["pointing_to"] minecart_bell_size = minecart_bell_finish - minecart_bell_start - ROM().seek(minecart_bell_start) - indicator = int.from_bytes(ROM().readBytes(2), "big") - ROM().seek(minecart_bell_start) - data = ROM().readBytes(minecart_bell_size) + ROM_COPY.seek(minecart_bell_start) + indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") + ROM_COPY.seek(minecart_bell_start) + data = ROM_COPY.readBytes(minecart_bell_size) if indicator == 0x1F8B: data = zlib.decompress(data, (15 + 32)) num_data = [] # data, but represented as nums rather than b strings @@ -790,11 +790,11 @@ def recolorBells(): data = bytearray(num_data) # convert num_data back to binary string if indicator == 0x1F8B: data = gzip.compress(data, compresslevel=9) - ROM().seek(minecart_bell_start) - ROM().writeBytes(data) + ROM_COPY.seek(minecart_bell_start) + ROM_COPY.writeBytes(data) -def recolorKlaptraps(mode): +def recolorKlaptraps(mode, ROM_COPY: ROM): """Recolor the klaptrap models for colorblind mode.""" green_files = [0xF31, 0xF32, 0xF33, 0xF35, 0xF37, 0xF39] # 0xF2F collar? 0xF30 feet? red_files = [0xF44, 0xF45, 0xF46, 0xF47, 0xF48, 0xF49] # , 0xF42 collar? 0xF43 feet? @@ -802,9 +802,9 @@ def recolorKlaptraps(mode): # Regular textures for file in range(6): - writeKlaptrapSkinColorToROM(4, 25, green_files[file], TextureFormat.RGBA5551, mode) - writeKlaptrapSkinColorToROM(1, 25, red_files[file], TextureFormat.RGBA5551, mode) - writeKlaptrapSkinColorToROM(3, 25, purple_files[file], TextureFormat.RGBA5551, mode) + writeKlaptrapSkinColorToROM(4, 25, green_files[file], TextureFormat.RGBA5551, mode, ROM_COPY) + writeKlaptrapSkinColorToROM(1, 25, red_files[file], TextureFormat.RGBA5551, mode, ROM_COPY) + writeKlaptrapSkinColorToROM(3, 25, purple_files[file], TextureFormat.RGBA5551, mode, ROM_COPY) belly_pixels_to_ignore = [] for x in range(32): @@ -815,10 +815,10 @@ def recolorKlaptraps(mode): belly_pixels_to_ignore.append([x, y]) # Special texture that requires only partial recoloring, in this case file 0xF38 which is the belly, and only the few green pixels - writeSpecialKlaptrapTextureToROM(4, 25, 0xF38, TextureFormat.RGBA5551, belly_pixels_to_ignore, mode) + writeSpecialKlaptrapTextureToROM(4, 25, 0xF38, TextureFormat.RGBA5551, belly_pixels_to_ignore, mode, ROM_COPY) -def recolorPotions(colorblind_mode): +def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): """Overwrite potion colors.""" diddy_color = getKongItemColor(colorblind_mode, Kongs.diddy) chunky_color = getKongItemColor(colorblind_mode, Kongs.chunky) @@ -837,10 +837,10 @@ def recolorPotions(colorblind_mode): potion_actor_start = js.pointer_addresses[5]["entries"][file[type][potion_color]]["pointing_to"] potion_actor_finish = js.pointer_addresses[5]["entries"][file[type][potion_color] + 1]["pointing_to"] potion_actor_size = potion_actor_finish - potion_actor_start - ROM().seek(potion_actor_start) - indicator = int.from_bytes(ROM().readBytes(2), "big") - ROM().seek(potion_actor_start) - data = ROM().readBytes(potion_actor_size) + ROM_COPY.seek(potion_actor_start) + indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") + ROM_COPY.seek(potion_actor_start) + data = ROM_COPY.readBytes(potion_actor_size) if indicator == 0x1F8B: data = zlib.decompress(data, (15 + 32)) num_data = [] # data, but represented as nums rather than b strings @@ -903,8 +903,8 @@ def recolorPotions(colorblind_mode): if len(data) > potion_actor_size: print(f"Attempted size bigger {hex(len(data))} than slot {hex(potion_actor_size)}") continue - ROM().seek(potion_actor_start) - ROM().writeBytes(data) + ROM_COPY.seek(potion_actor_start) + ROM_COPY.writeBytes(data) # Model2: file = [91, 498, 89, 499, 501, 502] @@ -912,10 +912,10 @@ def recolorPotions(colorblind_mode): potion_model2_start = js.pointer_addresses[4]["entries"][file[potion_color]]["pointing_to"] potion_model2_finish = js.pointer_addresses[4]["entries"][file[potion_color] + 1]["pointing_to"] potion_model2_size = potion_model2_finish - potion_model2_start - ROM().seek(potion_model2_start) - indicator = int.from_bytes(ROM().readBytes(2), "big") - ROM().seek(potion_model2_start) - data = ROM().readBytes(potion_model2_size) + ROM_COPY.seek(potion_model2_start) + indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") + ROM_COPY.seek(potion_model2_start) + data = ROM_COPY.readBytes(potion_model2_size) if indicator == 0x1F8B: data = zlib.decompress(data, (15 + 32)) num_data = [] # data, but represented as nums rather than b strings @@ -975,8 +975,8 @@ def recolorPotions(colorblind_mode): data = bytearray(num_data) # convert num_data back to binary string if indicator == 0x1F8B: data = gzip.compress(data, compresslevel=9) - ROM().seek(potion_model2_start) - ROM().writeBytes(data) + ROM_COPY.seek(potion_model2_start) + ROM_COPY.writeBytes(data) return # DK Arcade sprites From 57572d695a87ee6ddaad9b6084db2d6753322691 Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Thu, 26 Dec 2024 21:28:43 -0600 Subject: [PATCH 05/13] Maximize usage of getRawFile and writeRawFile --- randomizer/Patching/CosmeticColors.py | 4 +- randomizer/Patching/Cosmetics/Colorblind.py | 99 ++++----------------- randomizer/Patching/Cosmetics/KongColor.py | 17 +--- randomizer/Patching/Cosmetics/Krusha.py | 23 ++--- 4 files changed, 27 insertions(+), 116 deletions(-) diff --git a/randomizer/Patching/CosmeticColors.py b/randomizer/Patching/CosmeticColors.py index ec98edd2c..0681f7758 100644 --- a/randomizer/Patching/CosmeticColors.py +++ b/randomizer/Patching/CosmeticColors.py @@ -519,11 +519,11 @@ def applyKongModelSwaps(settings: Settings) -> None: ROM_COPY.writeMultipleBytes(unc_size, 4) changeModelTextures(settings, index) if value in (KongModels.krusha, KongModels.krool_cutscene, KongModels.krool_fight): - fixModelSmallKongCollision(index) + fixModelSmallKongCollision(index, ROM_COPY) if value == KongModels.krusha: placeKrushaHead(settings, index) if index == Kongs.donkey: - fixBaboonBlasts() + fixBaboonBlasts(ROM_COPY) # Orange Switches switch_faces = [0xB25, 0xB1E, 0xC81, 0xC80, 0xB24] base_im = getImageFile(25, 0xC20, True, 32, 32, TextureFormat.RGBA5551) diff --git a/randomizer/Patching/Cosmetics/Colorblind.py b/randomizer/Patching/Cosmetics/Colorblind.py index c1a165b3e..2d21bd438 100644 --- a/randomizer/Patching/Cosmetics/Colorblind.py +++ b/randomizer/Patching/Cosmetics/Colorblind.py @@ -279,15 +279,8 @@ def recolorWrinklyDoors(mode: ColorblindMode, ROM_COPY: ROM): """Recolor the Wrinkly hint door doorframes for colorblind mode.""" file = [0xF0, 0xF2, 0xEF, 0x67, 0xF1] for kong in range(5): - wrinkly_door_start = js.pointer_addresses[4]["entries"][file[kong]]["pointing_to"] - wrinkly_door_finish = js.pointer_addresses[4]["entries"][file[kong] + 1]["pointing_to"] - wrinkly_door_size = wrinkly_door_finish - wrinkly_door_start - ROM_COPY.seek(wrinkly_door_start) - indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") - ROM_COPY.seek(wrinkly_door_start) - data = ROM_COPY.readBytes(wrinkly_door_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) + file_index = file[kong] + data = getRawFile(TableNames.ModelTwoGeometry, file_index, True) num_data = [] # data, but represented as nums rather than b strings for d in data: num_data.append(d) @@ -398,10 +391,7 @@ def recolorWrinklyDoors(mode: ColorblindMode, ROM_COPY: ROM): num_data[offset + i] = new_color2[i] data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - ROM_COPY.seek(wrinkly_door_start) - ROM_COPY.writeBytes(data) + writeRawFile(TableNames.ModelTwoGeometry, file_index, True, data, ROM_COPY) def recolorKRoolShipSwitch(color: tuple, ROM_COPY: ROM): """Recolors the simian slam switch that is part of K. Rool's ship in galleon.""" @@ -536,15 +526,8 @@ def recolorSlamSwitches(galleon_switch_value, ROM_COPY: ROM, mode: ColorblindMod file = [0x94, 0x93, 0x95, 0x96, 0xB8, 0x16C, 0x16B, 0x16D, 0x16E, 0x16A, 0x167, 0x166, 0x168, 0x169, 0x165] written_galleon_ship = False for switch in range(15): - slam_switch_start = js.pointer_addresses[4]["entries"][file[switch]]["pointing_to"] - slam_switch_finish = js.pointer_addresses[4]["entries"][file[switch] + 1]["pointing_to"] - slam_switch_size = slam_switch_finish - slam_switch_start - ROM_COPY.seek(slam_switch_start) - indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") - ROM_COPY.seek(slam_switch_start) - data = ROM_COPY.readBytes(slam_switch_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) + file_index = file[switch] + data = getRawFile(TableNames.ModelTwoGeometry, file_index, True) num_data = [] # data, but represented as nums rather than b strings for d in data: num_data.append(d) @@ -571,10 +554,7 @@ def recolorSlamSwitches(galleon_switch_value, ROM_COPY: ROM, mode: ColorblindMod num_data[offset + i] = new_color3[i] data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - ROM_COPY.seek(slam_switch_start) - ROM_COPY.writeBytes(data) + writeRawFile(TableNames.ModelTwoGeometry, file_index, True, data, ROM_COPY) if not written_galleon_ship: galleon_switch_color = new_color1.copy() if galleon_switch_value is not None: @@ -590,15 +570,8 @@ def recolorBlueprintModelTwo(mode: ColorblindMode, ROM_COPY: ROM): """Recolor the Blueprint Model2 items for colorblind mode.""" file = [0xDE, 0xE0, 0xE1, 0xDD, 0xDF] for kong in range(5): - blueprint_model2_start = js.pointer_addresses[4]["entries"][file[kong]]["pointing_to"] - blueprint_model2_finish = js.pointer_addresses[4]["entries"][file[kong] + 1]["pointing_to"] - blueprint_model2_size = blueprint_model2_finish - blueprint_model2_start - ROM_COPY.seek(blueprint_model2_start) - indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") - ROM_COPY.seek(blueprint_model2_start) - data = ROM_COPY.readBytes(blueprint_model2_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) + file_index = file[kong] + data = getRawFile(TableNames.ModelTwoGeometry, file_index, True) num_data = [] # data, but represented as nums rather than b strings for d in data: num_data.append(d) @@ -632,10 +605,7 @@ def recolorBlueprintModelTwo(mode: ColorblindMode, ROM_COPY: ROM): num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - ROM_COPY.seek(blueprint_model2_start) - ROM_COPY.writeBytes(data) + writeRawFile(TableNames.ModelTwoGeometry, file_index, True, data, ROM_COPY) def maskImageRotatingRoomTile(im_f, im_mask, paste_coords, image_color_index, tile_side, mode: ColorblindMode): """Apply RGB mask to image of a Rotating Room Memory Tile.""" @@ -760,16 +730,7 @@ def recolorRotatingRoomTiles(mode): def recolorBells(ROM_COPY: ROM): """Recolor the Chunky Minecart bells for colorblind mode (prot/deut).""" - file = 693 - minecart_bell_start = js.pointer_addresses[4]["entries"][file]["pointing_to"] - minecart_bell_finish = js.pointer_addresses[4]["entries"][file + 1]["pointing_to"] - minecart_bell_size = minecart_bell_finish - minecart_bell_start - ROM_COPY.seek(minecart_bell_start) - indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") - ROM_COPY.seek(minecart_bell_start) - data = ROM_COPY.readBytes(minecart_bell_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) + data = getRawFile(TableNames.ModelTwoGeometry, 693, True) num_data = [] # data, but represented as nums rather than b strings for d in data: num_data.append(d) @@ -788,10 +749,7 @@ def recolorBells(ROM_COPY: ROM): num_data[offset + i] = new_color2[i] data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - ROM_COPY.seek(minecart_bell_start) - ROM_COPY.writeBytes(data) + writeRawFile(TableNames.ModelTwoGeometry, 693, True, data, ROM_COPY) def recolorKlaptraps(mode, ROM_COPY: ROM): @@ -834,15 +792,8 @@ def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): file = [[0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2], [0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA]] for type in range(2): for potion_color in range(6): - potion_actor_start = js.pointer_addresses[5]["entries"][file[type][potion_color]]["pointing_to"] - potion_actor_finish = js.pointer_addresses[5]["entries"][file[type][potion_color] + 1]["pointing_to"] - potion_actor_size = potion_actor_finish - potion_actor_start - ROM_COPY.seek(potion_actor_start) - indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") - ROM_COPY.seek(potion_actor_start) - data = ROM_COPY.readBytes(potion_actor_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) + file_index = file[type][potion_color] + data = getRawFile(TableNames.ActorGeometry, file_index, True) num_data = [] # data, but represented as nums rather than b strings for d in data: num_data.append(d) @@ -898,26 +849,13 @@ def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - if len(data) > potion_actor_size: - print(f"Attempted size bigger {hex(len(data))} than slot {hex(potion_actor_size)}") - continue - ROM_COPY.seek(potion_actor_start) - ROM_COPY.writeBytes(data) + writeRawFile(TableNames.ActorGeometry, file_index, True, data, ROM_COPY) # Model2: file = [91, 498, 89, 499, 501, 502] for potion_color in range(6): - potion_model2_start = js.pointer_addresses[4]["entries"][file[potion_color]]["pointing_to"] - potion_model2_finish = js.pointer_addresses[4]["entries"][file[potion_color] + 1]["pointing_to"] - potion_model2_size = potion_model2_finish - potion_model2_start - ROM_COPY.seek(potion_model2_start) - indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") - ROM_COPY.seek(potion_model2_start) - data = ROM_COPY.readBytes(potion_model2_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) + file_index = file[potion_color] + data = getRawFile(TableNames.ModelTwoGeometry, file_index, True) num_data = [] # data, but represented as nums rather than b strings for d in data: num_data.append(d) @@ -973,10 +911,7 @@ def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - ROM_COPY.seek(potion_model2_start) - ROM_COPY.writeBytes(data) + writeRawFile(TableNames.ModelTwoGeometry, file_index, True, data, ROM_COPY) return # DK Arcade sprites diff --git a/randomizer/Patching/Cosmetics/KongColor.py b/randomizer/Patching/Cosmetics/KongColor.py index 105121bc4..b6cca0302 100644 --- a/randomizer/Patching/Cosmetics/KongColor.py +++ b/randomizer/Patching/Cosmetics/KongColor.py @@ -6,7 +6,7 @@ from randomizer.Enums.Kongs import Kongs from randomizer.Enums.Settings import CharacterColors, KongModels from randomizer.Settings import Settings -from randomizer.Patching.Lib import PaletteFillType, int_to_list +from randomizer.Patching.Lib import PaletteFillType, int_to_list, getRawFile, writeRawFile, TableNames from randomizer.Patching.LibImage import getKongItemColor from randomizer.Patching.generate_kong_color_images import convertColors from randomizer.Patching.Cosmetics.Krusha import kong_index_mapping @@ -238,15 +238,7 @@ def changeModelTextures(settings: Settings, kong_index: int): file = kong_index_mapping[kong_index][x] if file is None: continue - krusha_model_start = js.pointer_addresses[5]["entries"][file]["pointing_to"] - krusha_model_finish = js.pointer_addresses[5]["entries"][file + 1]["pointing_to"] - krusha_model_size = krusha_model_finish - krusha_model_start - ROM_COPY.seek(krusha_model_start) - indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") - ROM_COPY.seek(krusha_model_start) - data = ROM_COPY.readBytes(krusha_model_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) + data = getRawFile(TableNames.ActorGeometry, file, True) num_data = [] # data, but represented as nums rather than b strings for d in data: num_data.append(d) @@ -258,7 +250,4 @@ def changeModelTextures(settings: Settings, kong_index: int): for di, d in enumerate(int_to_list(krusha_texture_replacement[kong_index][1], 2)): # Belt num_data[tex_idx + di] = d data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - ROM_COPY.seek(krusha_model_start) - ROM_COPY.writeBytes(data) \ No newline at end of file + writeRawFile(TableNames.ActorGeometry, file, True, data, ROM_COPY) \ No newline at end of file diff --git a/randomizer/Patching/Cosmetics/Krusha.py b/randomizer/Patching/Cosmetics/Krusha.py index 86e6544eb..b2863b49d 100644 --- a/randomizer/Patching/Cosmetics/Krusha.py +++ b/randomizer/Patching/Cosmetics/Krusha.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from randomizer.Settings import Settings from randomizer.Enums.Settings import ColorblindMode -from randomizer.Patching.Lib import TableNames, getObjectAddress, float_to_hex, intf_to_float, int_to_list +from randomizer.Patching.Lib import TableNames, getObjectAddress, float_to_hex, intf_to_float, int_to_list, getRawFile, writeRawFile from randomizer.Patching.Patcher import LocalROM from randomizer.Patching.LibImage import ( writeColorImageToROM, @@ -73,22 +73,13 @@ def readListAsInt(arr: list, start: int, size: int) -> int: Kongs.chunky: (11, 12), } -def fixModelSmallKongCollision(kong_index: int): +def fixModelSmallKongCollision(kong_index: int, ROM_COPY: LocalROM): """Modify Krusha Model to be smaller to enable him to fit through smaller gaps.""" for x in range(2): file = kong_index_mapping[kong_index][x] if file is None: continue - krusha_model_start = js.pointer_addresses[5]["entries"][file]["pointing_to"] - krusha_model_finish = js.pointer_addresses[5]["entries"][file + 1]["pointing_to"] - krusha_model_size = krusha_model_finish - krusha_model_start - ROM_COPY = LocalROM() - ROM_COPY.seek(krusha_model_start) - indicator = int.from_bytes(ROM_COPY.readBytes(2), "big") - ROM_COPY.seek(krusha_model_start) - data = ROM_COPY.readBytes(krusha_model_size) - if indicator == 0x1F8B: - data = zlib.decompress(data, (15 + 32)) + data = getRawFile(TableNames.ActorGeometry, file, True) num_data = [] # data, but represented as nums rather than b strings for d in data: num_data.append(d) @@ -121,15 +112,11 @@ def fixModelSmallKongCollision(kong_index: int): for di, d in enumerate(int_to_list(val_i, 4)): num_data[i_start + (4 * coord_index) + di] = d data = bytearray(num_data) # convert num_data back to binary string - if indicator == 0x1F8B: - data = gzip.compress(data, compresslevel=9) - LocalROM().seek(krusha_model_start) - LocalROM().writeBytes(data) + writeRawFile(TableNames.ActorGeometry, file, True, data, ROM_COPY) -def fixBaboonBlasts(): +def fixBaboonBlasts(ROM_COPY: LocalROM): """Fix various baboon blasts to work for Krusha.""" # Fungi Baboon Blast - ROM_COPY = LocalROM() for id in (2, 5): item_start = getObjectAddress(0xBC, id, "actor") if item_start is not None: From d2100426dc6de21b8c21ce6e5fbd4e58fe51f73d Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Thu, 26 Dec 2024 21:29:33 -0600 Subject: [PATCH 06/13] Lint --- randomizer/Patching/CosmeticColors.py | 27 ++++++++++++------- randomizer/Patching/Cosmetics/Colorblind.py | 11 +++++++- .../Patching/Cosmetics/CustomTextures.py | 4 ++- randomizer/Patching/Cosmetics/EnemyColors.py | 23 ++++++++++------ randomizer/Patching/Cosmetics/Holiday.py | 10 ++++--- randomizer/Patching/Cosmetics/KongColor.py | 7 ++++- randomizer/Patching/Cosmetics/Krusha.py | 8 +++++- randomizer/Patching/Cosmetics/ModelSwaps.py | 5 +++- randomizer/Patching/Cosmetics/Puzzles.py | 4 ++- randomizer/Patching/Cosmetics/TextRando.py | 3 ++- randomizer/Patching/Lib.py | 1 + randomizer/Patching/LibImage.py | 14 ++++++++-- 12 files changed, 87 insertions(+), 30 deletions(-) diff --git a/randomizer/Patching/CosmeticColors.py b/randomizer/Patching/CosmeticColors.py index 0681f7758..fde003ed1 100644 --- a/randomizer/Patching/CosmeticColors.py +++ b/randomizer/Patching/CosmeticColors.py @@ -36,11 +36,11 @@ TableNames, ) from randomizer.Patching.LibImage import ( - getImageFile, + getImageFile, TextureFormat, - ExtraTextures, - getBonusSkinOffset, - writeColorImageToROM, + ExtraTextures, + getBonusSkinOffset, + writeColorImageToROM, numberToImage, maskImage, maskImageWithColor, @@ -55,6 +55,7 @@ ) from randomizer.Patching.Cosmetics.KongColor import writeKongColors, changeModelTextures + class HelmDoorSetting: """Class to store information regarding helm doors.""" @@ -86,8 +87,10 @@ def __init__( self.dimensions = dimensions self.format = format + RECOLOR_MEDAL_RIM = False + def changePatchFace(settings: Settings): """Change the top of the dirt patch image.""" if not settings.better_dirt_patch_cosmetic: @@ -110,14 +113,14 @@ def changePatchFace(settings: Settings): def apply_cosmetic_colors(settings: Settings): """Apply cosmetic skins to kongs.""" - + ROM_COPY = ROM() sav = settings.rom_data applyCosmeticModelSwaps(settings, ROM_COPY) changePatchFace(settings) writeKongColors(settings) - + settings.jetman_color = [0xFF, 0xFF, 0xFF] if settings.misc_cosmetics and settings.override_cosmetics: ROM_COPY.seek(sav + 0x196) @@ -141,7 +144,7 @@ def apply_cosmetic_colors(settings: Settings): value = random.randint(brightness_threshold, 0xFF) jetman_color[channel] = value settings.jetman_color = jetman_color.copy() - + if js.document.getElementById("override_cosmetics").checked or True: writeTransition(settings) writeCustomPortal(settings) @@ -151,7 +154,7 @@ def apply_cosmetic_colors(settings: Settings): settings.gb_custom_color = js.document.getElementById("gb_custom_color").value else: settings.gb_colors = CharacterColors.randomized - + # GB Shine if settings.override_cosmetics and settings.gb_colors != CharacterColors.vanilla: channels = [] @@ -210,8 +213,10 @@ def apply_cosmetic_colors(settings: Settings): ) writeColorImageToROM(gb_shine_img, 25, tex, width, height, False, TextureFormat.RGBA5551) + balloon_single_frames = [(4, 38), (5, 38), (5, 38), (5, 38), (5, 38), (5, 38), (4, 38), (4, 38)] + def getSpinPixels() -> dict: """Get pixels that shouldn't be affected by the mask.""" spin_lengths = { @@ -293,6 +298,7 @@ def maskImageGBSpin(im_f, color: tuple, image_index: int): px_0[point[0], point[1]] = px[point[0], point[1]] return masked_im + def maskImageWithOutline(im_f, base_index, min_y, colorblind_mode, type=""): """Apply RGB mask to image with an Outline in a different color.""" w, h = im_f.size @@ -343,6 +349,7 @@ def maskImageWithOutline(im_f, base_index, min_y, colorblind_mode, type=""): pix[x, y] = (mask2[0], mask2[1], mask2[2], base[3]) return im_f + BALLOON_START = [5835, 5827, 5843, 5851, 5819] @@ -564,7 +571,6 @@ def applyKongModelSwaps(settings: Settings) -> None: writeColorImageToROM(base_im, 25, switch_faces[index], 32, 32, False, TextureFormat.RGBA5551) - def darkenDPad(): """Change the DPad cross texture for the DPad HUD.""" img = getImageFile(14, 187, True, 32, 32, TextureFormat.RGBA5551) @@ -592,6 +598,7 @@ def darkenDPad(): ROM().seek(js.pointer_addresses[14]["entries"][187]["pointing_to"]) ROM().writeBytes(px_data) + def applyHelmDoorCosmetics(settings: Settings) -> None: """Apply Helm Door Cosmetic Changes.""" crown_door_required_item = settings.crown_door_item @@ -666,6 +673,7 @@ def applyHelmDoorCosmetics(settings: Settings) -> None: TextureFormat.RGBA5551, ) + def darkenPauseBubble(settings: Settings): """Change the brightness of the text bubble used for the pause menu for dark mode.""" if not settings.dark_mode_textboxes: @@ -765,6 +773,7 @@ def showWinCondition(settings: Settings): base_im.paste(num_im, (6, 6), num_im) writeColorImageToROM(base_im, 14, 195, 32, 32, False, TextureFormat.RGBA5551) + def randomizePlants(ROM_COPY: ROM, settings: Settings): """Randomize the plants in the setup file.""" if not settings.misc_cosmetics: diff --git a/randomizer/Patching/Cosmetics/Colorblind.py b/randomizer/Patching/Cosmetics/Colorblind.py index 2d21bd438..19b6c9ec9 100644 --- a/randomizer/Patching/Cosmetics/Colorblind.py +++ b/randomizer/Patching/Cosmetics/Colorblind.py @@ -1,4 +1,5 @@ """All code associated with colorblind mode.""" + import js import gzip import zlib @@ -18,6 +19,7 @@ from randomizer.Enums.Kongs import Kongs from PIL import ImageEnhance + def writeKasplatHairColorToROM(color, table_index, file_index, format: str, ROM_COPY: ROM): """Write color to ROM for kasplats.""" file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] @@ -393,6 +395,7 @@ def recolorWrinklyDoors(mode: ColorblindMode, ROM_COPY: ROM): data = bytearray(num_data) # convert num_data back to binary string writeRawFile(TableNames.ModelTwoGeometry, file_index, True, data, ROM_COPY) + def recolorKRoolShipSwitch(color: tuple, ROM_COPY: ROM): """Recolors the simian slam switch that is part of K. Rool's ship in galleon.""" addresses = ( @@ -521,6 +524,7 @@ def recolorKRoolShipSwitch(color: tuple, ROM_COPY: ROM): data[0x1B58 + x] = 0 writeRawFile(TableNames.ModelTwoGeometry, 305, True, data, ROM_COPY) + def recolorSlamSwitches(galleon_switch_value, ROM_COPY: ROM, mode: ColorblindMode): """Recolor the Simian Slam switches for colorblind mode.""" file = [0x94, 0x93, 0x95, 0x96, 0xB8, 0x16C, 0x16B, 0x16D, 0x16E, 0x16A, 0x167, 0x166, 0x168, 0x169, 0x165] @@ -607,6 +611,7 @@ def recolorBlueprintModelTwo(mode: ColorblindMode, ROM_COPY: ROM): data = bytearray(num_data) # convert num_data back to binary string writeRawFile(TableNames.ModelTwoGeometry, file_index, True, data, ROM_COPY) + def maskImageRotatingRoomTile(im_f, im_mask, paste_coords, image_color_index, tile_side, mode: ColorblindMode): """Apply RGB mask to image of a Rotating Room Memory Tile.""" w, h = im_f.size @@ -670,6 +675,7 @@ def maskImageRotatingRoomTile(im_f, im_mask, paste_coords, image_color_index, ti pix[x, y] = (base[0], base[1], base[2], base[3]) return im_f + def recolorRotatingRoomTiles(mode): """Determine how to recolor the tiles rom the memory game in Donkey's Rotating Room in Caves.""" question_mark_tiles = [900, 901, 892, 893, 896, 897, 890, 891, 898, 899, 894, 895] @@ -728,6 +734,7 @@ def recolorRotatingRoomTiles(mode): masked_tile = maskImageRotatingRoomTile(tile_image, mask, face_offsets[int(tile / 2)], face_index, (int(tile / 2) % 2), mode) writeColorImageToROM(masked_tile, 7, face_tiles[tile], 32, 64, False, TextureFormat.RGBA5551) + def recolorBells(ROM_COPY: ROM): """Recolor the Chunky Minecart bells for colorblind mode (prot/deut).""" data = getRawFile(TableNames.ModelTwoGeometry, 693, True) @@ -925,6 +932,7 @@ def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): potion_image = maskPotionImage(potion_image, color, secondary_color[index]) writeColorImageToROM(potion_image, 6, file, 20, 20, False, TextureFormat.RGBA5551) + def maskMushroomImage(im_f, reference_image, color, side_2=False): """Apply RGB mask to mushroom image.""" w, h = im_f.size @@ -957,6 +965,7 @@ def maskMushroomImage(im_f, reference_image, color, side_2=False): pix[x, y] = (base[0], base[1], base[2], base[3]) return im_f + def recolorMushrooms(mode: ColorblindMode): """Recolor the various colored mushrooms in the game for colorblind mode.""" reference_mushroom_image = getImageFile(7, 297, False, 32, 32, TextureFormat.RGBA5551) @@ -977,4 +986,4 @@ def recolorMushrooms(mode: ColorblindMode): writeColorImageToROM(mushroom_image_side_1, 25, files_table_25_side_1[file], 64, 32, False, TextureFormat.RGBA5551) mushroom_image_side_2 = getImageFile(25, files_table_25_side_2[file], True, 64, 32, TextureFormat.RGBA5551) mushroom_image_side_2 = maskMushroomImage(mushroom_image_side_2, reference_mushroom_image_side2, file_color, True) - writeColorImageToROM(mushroom_image_side_2, 25, files_table_25_side_2[file], 64, 32, False, TextureFormat.RGBA5551) \ No newline at end of file + writeColorImageToROM(mushroom_image_side_2, 25, files_table_25_side_2[file], 64, 32, False, TextureFormat.RGBA5551) diff --git a/randomizer/Patching/Cosmetics/CustomTextures.py b/randomizer/Patching/Cosmetics/CustomTextures.py index af600c99e..c34f8b0e7 100644 --- a/randomizer/Patching/Cosmetics/CustomTextures.py +++ b/randomizer/Patching/Cosmetics/CustomTextures.py @@ -1,4 +1,5 @@ """Code associated with custom textures that can be applied through the cosmetic pack.""" + import js import random import math @@ -8,6 +9,7 @@ from randomizer.Patching.LibImage import writeColorImageToROM, TextureFormat, getImageFile from PIL import Image + def writeTransition(settings: Settings) -> None: """Write transition cosmetic to ROM.""" if js.cosmetics is None: @@ -201,4 +203,4 @@ def writeCustomPaintings(settings: Settings) -> None: settings.painting_museum_knight = PAINTING_INFO[2].name settings.painting_museum_swords = PAINTING_INFO[3].name settings.painting_treehouse_dolphin = PAINTING_INFO[4].name - settings.painting_treehouse_candy = PAINTING_INFO[5].name \ No newline at end of file + settings.painting_treehouse_candy = PAINTING_INFO[5].name diff --git a/randomizer/Patching/Cosmetics/EnemyColors.py b/randomizer/Patching/Cosmetics/EnemyColors.py index 6b51a0f63..980333c67 100644 --- a/randomizer/Patching/Cosmetics/EnemyColors.py +++ b/randomizer/Patching/Cosmetics/EnemyColors.py @@ -1,4 +1,5 @@ """All code changes associated with enemy color rando.""" + import js import gzip import random @@ -22,6 +23,7 @@ from randomizer.Patching.Patcher import ROM from PIL import Image + def getEnemySwapColor(channel_min: int = 0, channel_max: int = 255, min_channel_variance: int = 0) -> int: """Get an RGB color compatible with enemy swaps.""" channels = [] @@ -47,6 +49,7 @@ def getEnemySwapColor(channel_min: int = 0, channel_max: int = 255, min_channel_ value += channels[x] return value + class EnemyColorSwap: """Class to store information regarding an enemy color swap.""" @@ -95,7 +98,8 @@ def getOutputColor(self, color: int): new_color <<= 8 new_color += replacement_channel return new_color - + + # Enemy texture data FIRE_TEXTURES = ( [0x1539, 0x1553, 32], # Fireball. RGBA32 32x32 @@ -251,11 +255,13 @@ def getOutputColor(self, color: int): 0x1118: (64, 32), 0x1119: (64, 32), } - + + def convertColorIntToTuple(color: int) -> tuple: """Convert color stored as 3-byte int to tuple.""" return ((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF) + def adjustFungiMushVertexColor(shift: int): """Adjust the special vertex coloring on Fungi Giant Mushroom.""" fungi_geo = bytearray(getRawFile(TableNames.MapGeometry, Maps.FungiForest, True)) @@ -293,6 +299,7 @@ def adjustFungiMushVertexColor(shift: int): ROM_COPY.seek(js.pointer_addresses[TableNames.MapGeometry]["entries"][Maps.FungiForest]["pointing_to"]) ROM_COPY.writeBytes(file_data) + def writeMiscCosmeticChanges(settings): """Write miscellaneous changes to the cosmetic colors.""" ROM_COPY = ROM() @@ -388,7 +395,7 @@ def writeMiscCosmeticChanges(settings): # Klump klump_jacket_shift = getRandomHueShift() klump_hatammo_shift = getRandomHueShift() - + for img_data in KLUMP_JACKET_TEXTURES: hueShiftImageContainer(25, img_data["image"], 1, img_data["px"], TextureFormat.RGBA5551, klump_jacket_shift, ROM_COPY) for img_data in KLUMP_HAT_AMMO_TEXTURES: @@ -416,7 +423,7 @@ def writeMiscCosmeticChanges(settings): writeColorImageToROM(kosha_im, 25, img, 1, 1372, False, TextureFormat.RGBA5551) if settings.colorblind_mode == ColorblindMode.off: # Kremling - + while True: kremling_shift = getRandomHueShift() # Block red coloring @@ -430,7 +437,7 @@ def writeMiscCosmeticChanges(settings): if dims is not None: hueShiftImageContainer(25, 0xFCE + dim_index, dims[0], dims[1], TextureFormat.RGBA5551, kremling_shift, ROM_COPY) # Rabbit - + rabbit_shift = getRandomHueShift() for dim_index, dims in enumerate(RABBIT_TEXTURE_DIMENSIONS): if dims is not None: @@ -478,7 +485,7 @@ def writeMiscCosmeticChanges(settings): hueShiftImageContainer(7, 0x1F0 + x, 48, 42, TextureFormat.RGBA5551, race_coin_shift, ROM_COPY) scoff_shift = getRandomHueShift() troff_shift = getRandomHueShift() - + for img in SCOFF_TEXTURE_DATA: hueShiftImageContainer(25, img, 1, SCOFF_TEXTURE_DATA[img], TextureFormat.RGBA5551, scoff_shift, ROM_COPY) @@ -565,7 +572,7 @@ def writeMiscCosmeticChanges(settings): hueShiftImageContainer(25, 0x134C, 32, 32, TextureFormat.RGBA5551, getRandomHueShift(), ROM_COPY) # Spider spider_shift = getRandomHueShift() - + for img_index in SPIDER_TEXTURE_DIMENSIONS: hueShiftImageContainer, ROM_COPY( 25, @@ -713,4 +720,4 @@ def writeMiscCosmeticChanges(settings): file_data[local_start + 0xC + x] = channel file_data = gzip.compress(file_data, compresslevel=9) ROM_COPY.seek(js.pointer_addresses[5]["entries"][enemy]["pointing_to"]) - ROM_COPY.writeBytes(file_data) \ No newline at end of file + ROM_COPY.writeBytes(file_data) diff --git a/randomizer/Patching/Cosmetics/Holiday.py b/randomizer/Patching/Cosmetics/Holiday.py index 80ce1ee62..625d4b79e 100644 --- a/randomizer/Patching/Cosmetics/Holiday.py +++ b/randomizer/Patching/Cosmetics/Holiday.py @@ -1,13 +1,14 @@ """All code associated with temporary holiday-based cosmetic effects.""" + import gzip import js from PIL import Image, ImageEnhance from randomizer.Patching.Patcher import ROM from randomizer.Patching.Lib import Holidays, getHoliday from randomizer.Patching.LibImage import ( - getImageFile, - getBonusSkinOffset, - ExtraTextures, + getImageFile, + getBonusSkinOffset, + ExtraTextures, TextureFormat, maskImageWithColor, writeColorImageToROM, @@ -16,6 +17,7 @@ ) from randomizer.Settings import CharacterColors, KongModels + def changeBarrelColor(barrel_color: tuple = None, metal_color: tuple = None, brighten_barrel: bool = False): """Change the colors of the various barrels.""" wood_img = getImageFile(25, getBonusSkinOffset(ExtraTextures.ShellWood), True, 32, 64, TextureFormat.RGBA5551) @@ -234,4 +236,4 @@ def applyHolidayMode(settings): new_im.paste(sticker_im_snipped, (0, 0), sticker_im_snipped) new_im.paste(vanilla_sticker_portion, (0, 1360), vanilla_sticker_portion) writeColorImageToROM(new_im, 25, 0x1266, 1, 1372, False, TextureFormat.RGBA5551) - applyCelebrationRims(0, [False, True, True, True, True]) \ No newline at end of file + applyCelebrationRims(0, [False, True, True, True, True]) diff --git a/randomizer/Patching/Cosmetics/KongColor.py b/randomizer/Patching/Cosmetics/KongColor.py index b6cca0302..aaff732d0 100644 --- a/randomizer/Patching/Cosmetics/KongColor.py +++ b/randomizer/Patching/Cosmetics/KongColor.py @@ -1,4 +1,5 @@ """All code related to changing the color of kongs.""" + import js import zlib import gzip @@ -15,6 +16,7 @@ DEFAULT_COLOR = "#000000" + class KongPalette: """Class to store information regarding a kong palette.""" @@ -38,6 +40,7 @@ def __init__(self, kong: str, kong_index: int, palettes: list[KongPalette]): self.palettes = palettes.copy() self.setting_kong = kong + krusha_texture_replacement = { # Textures Krusha can use when he replaces various kongs (Main color, belt color) Kongs.donkey: (3724, 0x177D), @@ -57,6 +60,7 @@ def __init__(self, kong: str, kong_index: int, palettes: list[KongPalette]): "Enguarde": ["Skin"], } + def writeKongColors(settings: Settings): color_palettes = [] color_obj = {} @@ -219,6 +223,7 @@ def writeKongColors(settings: Settings): new_color_palettes.append(pal) convertColors(new_color_palettes) + def changeModelTextures(settings: Settings, kong_index: int): """Change the textures associated with a model.""" settings_values = [ @@ -250,4 +255,4 @@ def changeModelTextures(settings: Settings, kong_index: int): for di, d in enumerate(int_to_list(krusha_texture_replacement[kong_index][1], 2)): # Belt num_data[tex_idx + di] = d data = bytearray(num_data) # convert num_data back to binary string - writeRawFile(TableNames.ActorGeometry, file, True, data, ROM_COPY) \ No newline at end of file + writeRawFile(TableNames.ActorGeometry, file, True, data, ROM_COPY) diff --git a/randomizer/Patching/Cosmetics/Krusha.py b/randomizer/Patching/Cosmetics/Krusha.py index b2863b49d..ef560f3f6 100644 --- a/randomizer/Patching/Cosmetics/Krusha.py +++ b/randomizer/Patching/Cosmetics/Krusha.py @@ -1,4 +1,5 @@ """All code associated with Krusha.""" + import js import zlib import gzip @@ -57,6 +58,7 @@ [lambda x: x, lambda x: x, lambda x: x, lambda x: x, lambda x: x], ] + def readListAsInt(arr: list, start: int, size: int) -> int: """Read list and convert to int.""" val = 0 @@ -64,6 +66,7 @@ def readListAsInt(arr: list, start: int, size: int) -> int: val = (val * 256) + arr[start + i] return val + kong_index_mapping = { # Regular model, instrument model Kongs.donkey: (3, None), @@ -73,6 +76,7 @@ def readListAsInt(arr: list, start: int, size: int) -> int: Kongs.chunky: (11, 12), } + def fixModelSmallKongCollision(kong_index: int, ROM_COPY: LocalROM): """Modify Krusha Model to be smaller to enable him to fit through smaller gaps.""" for x in range(2): @@ -114,6 +118,7 @@ def fixModelSmallKongCollision(kong_index: int, ROM_COPY: LocalROM): data = bytearray(num_data) # convert num_data back to binary string writeRawFile(TableNames.ActorGeometry, file, True, data, ROM_COPY) + def fixBaboonBlasts(ROM_COPY: LocalROM): """Fix various baboon blasts to work for Krusha.""" # Fungi Baboon Blast @@ -141,6 +146,7 @@ def fixBaboonBlasts(ROM_COPY: LocalROM): ROM_COPY.seek(item_start + 0x8) ROM_COPY.writeMultipleBytes(int(float_to_hex(1980), 16), 4) + def placeKrushaHead(settings: Settings, slot): """Replace a kong's face with the Krusha face.""" if settings.colorblind_mode != ColorblindMode.off: @@ -164,4 +170,4 @@ def placeKrushaHead(settings: Settings, slot): # Used in the DPad Selection Menu writeColorImageToROM(krushaFace32, 14, 190 + slot, 32, 32, False, TextureFormat.RGBA5551) # Used in Shops Previews - writeColorImageToROM(krushaFace32RBGA32, 14, 197 + slot, 32, 32, False, TextureFormat.RGBA32) \ No newline at end of file + writeColorImageToROM(krushaFace32RBGA32, 14, 197 + slot, 32, 32, False, TextureFormat.RGBA32) diff --git a/randomizer/Patching/Cosmetics/ModelSwaps.py b/randomizer/Patching/Cosmetics/ModelSwaps.py index 6e97fd2ac..6623483df 100644 --- a/randomizer/Patching/Cosmetics/ModelSwaps.py +++ b/randomizer/Patching/Cosmetics/ModelSwaps.py @@ -1,4 +1,5 @@ """All code associated with model swaps.""" + import random import js from randomizer.Enums.Models import Model, Sprite @@ -390,10 +391,12 @@ KLAPTRAPS = [Model.KlaptrapGreen, Model.KlaptrapPurple, Model.KlaptrapRed] + def getRandomKlaptrapModel() -> Model: """Get random klaptrap model.""" return random.choice(KLAPTRAPS) + def applyCosmeticModelSwaps(settings: Settings, ROM_COPY: ROM): """Apply model swaps to the settings dict.""" sav = settings.rom_data @@ -495,4 +498,4 @@ def applyCosmeticModelSwaps(settings: Settings, ROM_COPY: ROM): ROM_COPY.seek(sav + 0x1B5) ROM_COPY.writeMultipleBytes(settings.panic_fairy_model + 1, 1) # Still needed for end seq fairy swap ROM_COPY.seek(sav + 0x1E2) - ROM_COPY.write(swap_bitfield) \ No newline at end of file + ROM_COPY.write(swap_bitfield) diff --git a/randomizer/Patching/Cosmetics/Puzzles.py b/randomizer/Patching/Cosmetics/Puzzles.py index dad9addcd..77828c5fa 100644 --- a/randomizer/Patching/Cosmetics/Puzzles.py +++ b/randomizer/Patching/Cosmetics/Puzzles.py @@ -1,8 +1,10 @@ """All code associated with updating textures for puzzles.""" + from randomizer.Settings import Settings from randomizer.Patching.LibImage import writeColorImageToROM, TextureFormat, getImageFile, getNumberImage from PIL import Image + def updateMillLeverTexture(settings: Settings) -> None: """Update the 21132 texture.""" if settings.mill_levers[0] > 0: @@ -116,4 +118,4 @@ def updateCryptLeverTexture(settings: Settings) -> None: else: texture_1.paste(num, (tl_x, tl_y), num) writeColorImageToROM(texture_0, 25, 0x99A, 32, 64, False, TextureFormat.RGBA5551) - writeColorImageToROM(texture_1, 25, 0x999, 32, 64, False, TextureFormat.RGBA5551) \ No newline at end of file + writeColorImageToROM(texture_1, 25, 0x999, 32, 64, False, TextureFormat.RGBA5551) diff --git a/randomizer/Patching/Cosmetics/TextRando.py b/randomizer/Patching/Cosmetics/TextRando.py index 64343aa6d..a054b02d1 100644 --- a/randomizer/Patching/Cosmetics/TextRando.py +++ b/randomizer/Patching/Cosmetics/TextRando.py @@ -1,4 +1,5 @@ """All code associated with cosmetic tweaks to text.""" + import random from randomizer.Patching.Patcher import LocalROM from randomizer.Patching.Lib import writeText, grabText @@ -313,4 +314,4 @@ def writeBootMessages() -> None: placed_messages = random.sample(boot_phrases, 4) for message_index, message in enumerate(placed_messages): ROM_COPY.seek(0x1FFD000 + (0x40 * message_index)) - ROM_COPY.writeBytes(message.upper().encode("ascii")) \ No newline at end of file + ROM_COPY.writeBytes(message.upper().encode("ascii")) diff --git a/randomizer/Patching/Lib.py b/randomizer/Patching/Lib.py index 3d13b16e7..a72353a40 100644 --- a/randomizer/Patching/Lib.py +++ b/randomizer/Patching/Lib.py @@ -1129,6 +1129,7 @@ def getProgHintBarrierItem(item: ProgressiveHintItem) -> BarrierItems: } return barrier_bijection[item] + def getValueFromByteArray(ba: bytearray, offset: int, size: int) -> int: """Get value from byte array given an offset and size.""" value = 0 diff --git a/randomizer/Patching/LibImage.py b/randomizer/Patching/LibImage.py index d582aeafa..7c9b7a020 100644 --- a/randomizer/Patching/LibImage.py +++ b/randomizer/Patching/LibImage.py @@ -261,6 +261,7 @@ def imageToCI(ROM_COPY: ROM, im_f, ci_index: int, tex_index: int, pal_index: int ROM_COPY.seek(pal_start) ROM_COPY.write(pal_bin_file) + def writeColorImageToROM( im_f, table_index: int, @@ -333,6 +334,7 @@ def writeColorImageToROM( except Exception: ROM().writeBytes(data) + def getNumberImage(number: int): """Get Number Image from number.""" if number < 5: @@ -390,6 +392,7 @@ def numberToImage(number: int, dim: Tuple[int, int]): output.paste(base, (x_offset, y_offset), base) return output + def getRGBFromHash(hash: str): """Convert hash RGB code to rgb array.""" red = int(hash[1:3], 16) @@ -397,6 +400,7 @@ def getRGBFromHash(hash: str): blue = int(hash[5:7], 16) return [red, green, blue] + def maskImageWithColor(im_f: Image, mask: tuple): """Apply rgb mask to image using a rgb color tuple.""" w, h = im_f.size @@ -417,6 +421,7 @@ def maskImageWithColor(im_f: Image, mask: tuple): pix[x, y] = (base[0], base[1], base[2], base[3]) return im_f + def getColorBase(mode: ColorblindMode) -> list[str]: """Get the color base array.""" if mode == ColorblindMode.prot: @@ -427,6 +432,7 @@ def getColorBase(mode: ColorblindMode) -> list[str]: return ["#000000", "#C72020", "#13C4D8", "#FFFFFF", "#FFA4A4"] return ["#FFD700", "#FF0000", "#1699FF", "#B045FF", "#41FF25"] + def getKongItemColor(mode: ColorblindMode, kong: Kongs, output_as_list: bool = False) -> str: """Get the color assigned to a kong.""" hash_str = getColorBase(mode)[kong] @@ -434,7 +440,8 @@ def getKongItemColor(mode: ColorblindMode, kong: Kongs, output_as_list: bool = F return getRGBFromHash(hash_str) return hash_str -def maskImage(im_f, base_index, min_y, keep_dark=False, mode = ColorblindMode.off): + +def maskImage(im_f, base_index, min_y, keep_dark=False, mode=ColorblindMode.off): """Apply RGB mask to image.""" w, h = im_f.size converter = ImageEnhance.Color(im_f) @@ -456,6 +463,7 @@ def maskImage(im_f, base_index, min_y, keep_dark=False, mode = ColorblindMode.of pix[x, y] = (base[0], base[1], base[2], base[3]) return im_f + def hueShiftImageContainer(table: int, image: int, width: int, height: int, format: TextureFormat, shift: int, ROM_COPY: ROM = None): """Load an image, shift the hue and rewrite it back to ROM.""" loaded_im = getImageFile(table, image, table != 7, width, height, format) @@ -482,10 +490,12 @@ def hueShiftImageContainer(table: int, image: int, width: int, height: int, form ROM_COPY.seek(js.pointer_addresses[table]["entries"][image]["pointing_to"]) ROM_COPY.writeBytes(px_data) + def getLuma(color: tuple) -> float: """Get the luma value of a color.""" return (0.299 * color[0]) + (0.587 * color[1]) + (0.114 * color[2]) + def hueShiftColor(color: tuple, amount: int, head_ratio: int = None) -> tuple: """Apply a hue shift to a color.""" # RGB -> HSV Conversion @@ -537,4 +547,4 @@ def hueShiftColor(color: tuple, amount: int, head_ratio: int = None) -> tuple: red_ratio = c green_ratio = 0 blue_ratio = x - return (int((red_ratio + m) * 255), int((green_ratio + m) * 255), int((blue_ratio + m) * 255)) \ No newline at end of file + return (int((red_ratio + m) * 255), int((green_ratio + m) * 255), int((blue_ratio + m) * 255)) From f714bc04aeb47fef617cb47cbb856ea3c2d7bca0 Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Fri, 27 Dec 2024 07:43:13 -0600 Subject: [PATCH 07/13] Smoother camera option --- base-hack/src/misc/krusha.c | 39 +- randomizer/Patching/ASMPatcher.py | 18 +- randomizer/Patching/CosmeticColors.py | 128 +++--- randomizer/Patching/Cosmetics/Colorblind.py | 4 +- randomizer/SettingStrings.py | 1 + randomizer/Settings.py | 1 + static/patches/shrink-dk64.bps | Bin 5818995 -> 5818972 bytes static/patches/symbols.json | 478 ++++++++++---------- templates/cosmetics.html.jinja2 | 1 + 9 files changed, 335 insertions(+), 335 deletions(-) diff --git a/base-hack/src/misc/krusha.c b/base-hack/src/misc/krusha.c index 1c6d23600..d13d26ce4 100644 --- a/base-hack/src/misc/krusha.c +++ b/base-hack/src/misc/krusha.c @@ -319,26 +319,29 @@ typedef struct projectile_extra { /* 0x014 */ float unk14; } projectile_extra; +typedef struct KrushaProjectileColorStruct { + unsigned char pellet; + unsigned char red; + unsigned char green; + unsigned char blue; +} KrushaProjectileColorStruct; + +static KrushaProjectileColorStruct krusha_projectile_colors[] = { + {.pellet = 48, .red = 0xC0, .green = 0xFF, .blue = 0x00}, + {.pellet = 36, .red = 0xFF, .green = 0x40, .blue = 0x00}, + {.pellet = 42, .red = 0x18, .green = 0x18, .blue = 0x00}, + {.pellet = 43, .red = 0x80, .green = 0x00, .blue = 0x00}, + {.pellet = 38, .red = 0x00, .green = 0xFF, .blue = 0x00}, +}; + void setKrushaAmmoColor(void) { int currentPellet = CurrentActorPointer_0->actorType; - switch (currentPellet) { - case 48: - changeActorColor(0xC0, 0xFF, 0, 0xFF); - break; - case 36: - changeActorColor(0xFF, 0x40, 0x40, 0xFF); - break; - case 42: - changeActorColor(0x18, 0x18, 0xFF, 0xFF); - break; - case 43: - changeActorColor(0x80, 0, 0xFF, 0xFF); - break; - case 38: - changeActorColor(0, 0xFF, 0, 0xFF); - break; - default: - changeActorColor(0, 0xFF, 0, 0xFF); + for (int i = 0; i < 5; i++) { + KrushaProjectileColorStruct *data = &krusha_projectile_colors[i]; + if ((currentPellet == data->pellet) || (i == 4)) { + changeActorColor(data->red, data->green, data->blue, 0xFF); + return; + } } } diff --git a/randomizer/Patching/ASMPatcher.py b/randomizer/Patching/ASMPatcher.py index a4a31f7f2..12f518c2e 100644 --- a/randomizer/Patching/ASMPatcher.py +++ b/randomizer/Patching/ASMPatcher.py @@ -662,7 +662,23 @@ def patchAssemblyCosmetic(ROM_COPY: ROM, settings: Settings, has_dom: bool = Tru elif holiday == Holidays.Anniv25: # Change barrel base sprite writeValue(ROM_COPY, 0x80721458, Overlay.Static, getBonusSkinOffset(ExtraTextures.Anniv25Barrel), offset_dict) - + # Smoother Camera + if settings.smoother_camera: + camera_change_cooldown = 5 + writeValue(ROM_COPY, 0x806EA238, Overlay.Static, 0, offset_dict, 4) # Disable it requiring a new input + writeValue(ROM_COPY, 0x806EA2A4, Overlay.Static, 0, offset_dict, 4) # Disable it requiring a new input + camera_change_amount = 5 * (camera_change_cooldown - 2) + addr = getROMAddress(0x806EA25E, Overlay.Static, offset_dict) + ROM_COPY.seek(addr) + val = int.from_bytes(ROM_COPY.readBytes(2), "big") + if (val & 0x8000) == 0: # Is Mirror Mode + camera_change_amount = -camera_change_amount + writeValue(ROM_COPY, 0x806EA256, Overlay.Static, camera_change_cooldown, offset_dict) + writeValue(ROM_COPY, 0x806EA25E, Overlay.Static, -camera_change_amount, offset_dict, 2, True) + writeValue(ROM_COPY, 0x806EA2C2, Overlay.Static, camera_change_cooldown, offset_dict) + writeValue(ROM_COPY, 0x806EA2CA, Overlay.Static, camera_change_amount, offset_dict, 2, True) + + # Crosshair if settings.colorblind_mode != ColorblindMode.off: writeValue(ROM_COPY, 0x8069E974, Overlay.Static, 0x1000, offset_dict) # Force first option writeValue(ROM_COPY, 0x8069E9B0, Overlay.Static, 0, offset_dict, 4) # Prevent write diff --git a/randomizer/Patching/CosmeticColors.py b/randomizer/Patching/CosmeticColors.py index fde003ed1..4b9da6c6a 100644 --- a/randomizer/Patching/CosmeticColors.py +++ b/randomizer/Patching/CosmeticColors.py @@ -349,8 +349,13 @@ def maskImageWithOutline(im_f, base_index, min_y, colorblind_mode, type=""): pix[x, y] = (mask2[0], mask2[1], mask2[2], base[3]) return im_f - +SINGLE_START = [168, 152, 232, 208, 240] BALLOON_START = [5835, 5827, 5843, 5851, 5819] +LASER_START = [784, 748, 363, 760, 772] +SHOCKWAVE_START = [4897, 4903, 4712, 4950, 4925] +BLUEPRINT_START = [5624, 5608, 5519, 5632, 5616] +COIN_START = [224, 256, 248, 216, 264] +BUNCH_START = [274, 854, 818, 842, 830] def overwrite_object_colors(settings, ROM_COPY: ROM): @@ -367,13 +372,12 @@ def overwrite_object_colors(settings, ROM_COPY: ROM): if mode in (ColorblindMode.prot, ColorblindMode.deut): recolorBells(ROM_COPY) # Preload DK single cb image to paste onto balloons - file = 175 - dk_single = getImageFile(7, file, False, 44, 44, TextureFormat.RGBA5551) + dk_single = getImageFile(7, 175, False, 44, 44, TextureFormat.RGBA5551) dk_single = dk_single.resize((21, 21)) blueprint_lanky = [] # Preload blueprint images. Lanky's blueprint image is so much easier to mask, because it is blue, and the frame is brown for file in range(8): - blueprint_lanky.append(getImageFile(25, 5519 + (file), True, 48, 42, TextureFormat.RGBA5551)) + blueprint_lanky.append(getImageFile(25, 5519 + file, True, 48, 42, TextureFormat.RGBA5551)) writeWhiteKasplatHairColorToROM("#FFFFFF", "#000000", 25, 4125, TextureFormat.RGBA5551, ROM_COPY) recolorWrinklyDoors(mode, ROM_COPY) recolorSlamSwitches(galleon_switch_value, ROM_COPY, mode) @@ -389,74 +393,65 @@ def overwrite_object_colors(settings, ROM_COPY: ROM): # hair_im = maskImage(hair_im, kong_index, 0) # writeColorImageToROM(hair_im, 25, [4124, 4122, 4123, 4120, 4121][kong_index], 32, 44, False) writeKasplatHairColorToROM(getKongItemColor(mode, kong_index), 25, [4124, 4122, 4123, 4120, 4121][kong_index], TextureFormat.RGBA5551, ROM_COPY) - for file in range(5519, 5527): + for offset in range(8): # Blueprint sprite - blueprint_start = [5624, 5608, 5519, 5632, 5616] - blueprint_im = blueprint_lanky[(file - 5519)] + blueprint_im = blueprint_lanky[offset] blueprint_im = maskBlueprintImage(blueprint_im, kong_index, mode) - writeColorImageToROM(blueprint_im, 25, blueprint_start[kong_index] + (file - 5519), 48, 42, False, TextureFormat.RGBA5551) - for file in range(4925, 4931): + writeColorImageToROM(blueprint_im, 25, BLUEPRINT_START[kong_index] + offset, 48, 42, False, TextureFormat.RGBA5551) + for offset in range(6): # Shockwave - shockwave_start = [4897, 4903, 4712, 4950, 4925] - shockwave_im = getImageFile(25, shockwave_start[kong_index] + (file - 4925), True, 32, 32, TextureFormat.RGBA32) + shockwave_im = getImageFile(25, SHOCKWAVE_START[kong_index] + offset, True, 32, 32, TextureFormat.RGBA32) shockwave_im = maskImage(shockwave_im, kong_index, 0, False, mode) - writeColorImageToROM(shockwave_im, 25, shockwave_start[kong_index] + (file - 4925), 32, 32, False, TextureFormat.RGBA32) - for file in range(784, 796): + writeColorImageToROM(shockwave_im, 25, SHOCKWAVE_START[kong_index] + offset, 32, 32, False, TextureFormat.RGBA32) + for offset in range(12): # Helm Laser (will probably also affect the Pufftoss laser and the Game Over laser) - laser_start = [784, 748, 363, 760, 772] - laser_im = getImageFile(7, laser_start[kong_index] + (file - 784), False, 32, 32, TextureFormat.RGBA32) + laser_im = getImageFile(7, LASER_START[kong_index] + offset, False, 32, 32, TextureFormat.RGBA32) laser_im = maskLaserImage(laser_im, kong_index, mode) - writeColorImageToROM(laser_im, 7, laser_start[kong_index] + (file - 784), 32, 32, False, TextureFormat.RGBA32) - if kong_index == 0 or kong_index == 3 or (kong_index == 2 and mode != ColorblindMode.trit): # Lanky (prot, deut only) or DK or Tiny - for file in range(152, 160): + writeColorImageToROM(laser_im, 7, LASER_START[kong_index] + offset, 32, 32, False, TextureFormat.RGBA32) + if kong_index in (Kongs.donkey, Kongs.tiny) or (kong_index == Kongs.lanky and mode != ColorblindMode.trit): # Lanky (prot, deut only) or DK or Tiny + for offset in range(8): # Single - single_start = [168, 152, 232, 208, 240] - single_im = getImageFile(7, single_start[kong_index] + (file - 152), False, 44, 44, TextureFormat.RGBA5551) + single_im = getImageFile(7, SINGLE_START[kong_index] + offset, False, 44, 44, TextureFormat.RGBA5551) single_im = maskImageWithOutline(single_im, kong_index, 0, mode, "single") - writeColorImageToROM(single_im, 7, single_start[kong_index] + (file - 152), 44, 44, False, TextureFormat.RGBA5551) - for file in range(216, 224): + writeColorImageToROM(single_im, 7, SINGLE_START[kong_index] + offset, 44, 44, False, TextureFormat.RGBA5551) + for offset in range(8): # Coin - coin_start = [224, 256, 248, 216, 264] - coin_im = getImageFile(7, coin_start[kong_index] + (file - 216), False, 48, 42, TextureFormat.RGBA5551) + coin_im = getImageFile(7, COIN_START[kong_index] + offset, False, 48, 42, TextureFormat.RGBA5551) coin_im = maskImageWithOutline(coin_im, kong_index, 0, mode) - writeColorImageToROM(coin_im, 7, coin_start[kong_index] + (file - 216), 48, 42, False, TextureFormat.RGBA5551) - for file in range(274, 286): + writeColorImageToROM(coin_im, 7, COIN_START[kong_index] + offset, 48, 42, False, TextureFormat.RGBA5551) + for offset in range(12): # Bunch - bunch_start = [274, 854, 818, 842, 830] - bunch_im = getImageFile(7, bunch_start[kong_index] + (file - 274), False, 44, 44, TextureFormat.RGBA5551) + bunch_im = getImageFile(7, BUNCH_START[kong_index] + offset, False, 44, 44, TextureFormat.RGBA5551) bunch_im = maskImageWithOutline(bunch_im, kong_index, 0, mode, "bunch") - writeColorImageToROM(bunch_im, 7, bunch_start[kong_index] + (file - 274), 44, 44, False, TextureFormat.RGBA5551) - for file in range(5819, 5827): + writeColorImageToROM(bunch_im, 7, BUNCH_START[kong_index] + offset, 44, 44, False, TextureFormat.RGBA5551) + for offset in range(8): # Balloon - balloon_im = getImageFile(25, BALLOON_START[kong_index] + (file - 5819), True, 32, 64, TextureFormat.RGBA5551) + balloon_im = getImageFile(25, BALLOON_START[kong_index] + offset, True, 32, 64, TextureFormat.RGBA5551) balloon_im = maskImageWithOutline(balloon_im, kong_index, 33, mode) - balloon_im.paste(dk_single, balloon_single_frames[file - 5819], dk_single) - writeColorImageToROM(balloon_im, 25, BALLOON_START[kong_index] + (file - 5819), 32, 64, False, TextureFormat.RGBA5551) + balloon_im.paste(dk_single, balloon_single_frames[offset], dk_single) + writeColorImageToROM(balloon_im, 25, BALLOON_START[kong_index] + offset, 32, 64, False, TextureFormat.RGBA5551) else: - for file in range(152, 160): + for offset in range(8): # Single - single_start = [168, 152, 232, 208, 240] - single_im = getImageFile(7, single_start[kong_index] + (file - 152), False, 44, 44, TextureFormat.RGBA5551) + single_im = getImageFile(7, SINGLE_START[kong_index] + offset, False, 44, 44, TextureFormat.RGBA5551) single_im = maskImage(single_im, kong_index, 0, False, mode) - writeColorImageToROM(single_im, 7, single_start[kong_index] + (file - 152), 44, 44, False, TextureFormat.RGBA5551) - for file in range(216, 224): + writeColorImageToROM(single_im, 7, SINGLE_START[kong_index] + offset, 44, 44, False, TextureFormat.RGBA5551) + for offset in range(8): # Coin - coin_start = [224, 256, 248, 216, 264] - coin_im = getImageFile(7, coin_start[kong_index] + (file - 216), False, 48, 42, TextureFormat.RGBA5551) + coin_im = getImageFile(7, COIN_START[kong_index] + offset, False, 48, 42, TextureFormat.RGBA5551) coin_im = maskImage(coin_im, kong_index, 0, False, mode) - writeColorImageToROM(coin_im, 7, coin_start[kong_index] + (file - 216), 48, 42, False, TextureFormat.RGBA5551) - for file in range(274, 286): + writeColorImageToROM(coin_im, 7, COIN_START[kong_index] + offset, 48, 42, False, TextureFormat.RGBA5551) + for offset in range(12): # Bunch - bunch_start = [274, 854, 818, 842, 830] - bunch_im = getImageFile(7, bunch_start[kong_index] + (file - 274), False, 44, 44, TextureFormat.RGBA5551) + bunch_im = getImageFile(7, BUNCH_START[kong_index] + offset, False, 44, 44, TextureFormat.RGBA5551) bunch_im = maskImage(bunch_im, kong_index, 0, True, mode) - writeColorImageToROM(bunch_im, 7, bunch_start[kong_index] + (file - 274), 44, 44, False, TextureFormat.RGBA5551) - for file in range(5819, 5827): + writeColorImageToROM(bunch_im, 7, BUNCH_START[kong_index] + offset, 44, 44, False, TextureFormat.RGBA5551) + for offset in range(8): # Balloon - balloon_im = getImageFile(25, BALLOON_START[kong_index] + (file - 5819), True, 32, 64, TextureFormat.RGBA5551) + balloon_im = getImageFile(25, BALLOON_START[kong_index] + offset, True, 32, 64, TextureFormat.RGBA5551) balloon_im = maskImage(balloon_im, kong_index, 33, False, mode) - balloon_im.paste(dk_single, balloon_single_frames[file - 5819], dk_single) - writeColorImageToROM(balloon_im, 25, BALLOON_START[kong_index] + (file - 5819), 32, 64, False, TextureFormat.RGBA5551) + balloon_im.paste(dk_single, balloon_single_frames[offset], dk_single) + writeColorImageToROM(balloon_im, 25, BALLOON_START[kong_index] + offset, 32, 64, False, TextureFormat.RGBA5551) else: # Recolor slam switch if colorblind mode is off if galleon_switch_value is not None: @@ -487,6 +482,14 @@ def overwrite_object_colors(settings, ROM_COPY: ROM): KongModels.funky: (0x117, 0x117), } +LIME_COLORS = { + Kongs.donkey: (255, 224, 8), + Kongs.diddy: (255, 48, 32), + Kongs.lanky: (40, 168, 255), + Kongs.tiny: (216, 100, 248), + Kongs.chunky: (0, 255, 0), + Kongs.any: (100, 255, 60), +} def applyKongModelSwaps(settings: Settings) -> None: """Apply Krusha Kong setting.""" @@ -536,32 +539,7 @@ def applyKongModelSwaps(settings: Settings) -> None: base_im = getImageFile(25, 0xC20, True, 32, 32, TextureFormat.RGBA5551) orange_im = getImageFile(7, 0x136, False, 32, 32, TextureFormat.RGBA5551) if settings.colorblind_mode == ColorblindMode.off: - match index: - case Kongs.donkey: - color_r = 255 - color_g = 224 - color_b = 8 - case Kongs.diddy: - color_r = 255 - color_g = 48 - color_b = 32 - case Kongs.lanky: - color_r = 40 - color_g = 168 - color_b = 255 - case Kongs.tiny: - color_r = 216 - color_g = 100 - color_b = 248 - case Kongs.chunky: - color_r = 0 - color_g = 255 - color_b = 0 - case _: - color_r = 100 - color_g = 255 - color_b = 60 - orange_im = maskImageWithColor(orange_im, (color_r, color_g, color_b)) + orange_im = maskImageWithColor(orange_im, LIME_COLORS[index]) else: orange_im = maskImageWithColor(orange_im, (0, 255, 0)) # Brighter green makes this more distinguishable for colorblindness dim_length = int(32 * ORANGE_SCALING) diff --git a/randomizer/Patching/Cosmetics/Colorblind.py b/randomizer/Patching/Cosmetics/Colorblind.py index 19b6c9ec9..842bde50e 100644 --- a/randomizer/Patching/Cosmetics/Colorblind.py +++ b/randomizer/Patching/Cosmetics/Colorblind.py @@ -814,7 +814,7 @@ def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): if potion_color < 5: new_color = getKongItemColor(colorblind_mode, potion_color, True) else: - new_color = getRGBFromHash("#FFFFFF") + new_color = [0xFF, 0xFF, 0xFF] # Recolor the actor item for offset in color1_offsets: @@ -876,7 +876,7 @@ def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): if potion_color < 5: new_color = getKongItemColor(colorblind_mode, potion_color, True) else: - new_color = getRGBFromHash("#FFFFFF") + new_color = [0xFF, 0xFF, 0xFF] # Recolor the model2 item for offset in color1_offsets: diff --git a/randomizer/SettingStrings.py b/randomizer/SettingStrings.py index f6e9411d0..cb99a109d 100644 --- a/randomizer/SettingStrings.py +++ b/randomizer/SettingStrings.py @@ -221,6 +221,7 @@ def encrypt_settings_string_enum(dict_data: dict): "true_widescreen", "camera_is_not_inverted", "sound_type", + "smoother_camera", "songs_excluded", "excluded_songs_selected", "music_filtering", diff --git a/randomizer/Settings.py b/randomizer/Settings.py index 0e4e80c38..296b0d1e2 100644 --- a/randomizer/Settings.py +++ b/randomizer/Settings.py @@ -615,6 +615,7 @@ def generate_misc(self): self.camera_is_not_inverted = False self.sound_type = SoundType.stereo self.custom_music_proportion = 100 + self.smoother_camera = False self.fill_with_custom_music = False self.show_song_name = False diff --git a/static/patches/shrink-dk64.bps b/static/patches/shrink-dk64.bps index f37bdc977ec43b840693178e278b36a66cc93994..45a580f97ba617536904ef406a96ec4021e13a4c 100644 GIT binary patch delta 60579 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 diff --git a/static/patches/symbols.json b/static/patches/symbols.json index fefeadd28..b8f0ca056 100644 --- a/static/patches/symbols.json +++ b/static/patches/symbols.json @@ -744,245 +744,245 @@ "minecartjumpfix": 2153688132, "minecartjumpfix_0": 2153688172, "setkrushaammocolor": 2153688252, - "orangeguncode": 2153688484, - "istbarrelflag": 2153689800, - "isfairyflag": 2153689816, - "gethinttextindex": 2153689856, - "isgoodtextbox": 2153691052, - "getmovehint": 2153691164, - "resetdisplayedmusic": 2153691276, - "detectsongchange": 2153691288, - "initsongdisplay": 2153691628, - "displaysongnamehandler": 2153691940, - "curseremoved": 2153692532, - "haspermalossgrace": 2153692544, - "determinekongunlock": 2153692572, - "unlockkongpermaloss": 2153692716, - "givekongmoves": 2153692828, - "isdeathstate": 2153692896, - "kong_has_died": 2153692936, - "determinestartkong_permalossmode": 2153693488, - "transitionkong": 2153693656, - "fixgracecheese": 2153693964, - "changekongontransition_permaloss": 2153694136, - "forcebosskong": 2153694184, - "preventbosscheese": 2153694412, - "doeskongpossessmove": 2153694584, - "issharedmove": 2153695224, - "getcounteritem": 2153695436, - "getmovecountinshop": 2153696008, - "wipecounterimagecache": 2153696388, - "loadinternaltexture": 2153696436, - "loadfonttexture_counter": 2153696600, - "updatecounterdisplay": 2153696764, - "getactormodeltwodist": 2153696944, - "getclosestshop": 2153697080, - "getshopscale": 2153697732, - "newcountercode": 2153698072, - "handlefootprogress": 2153699472, - "changestat": 2153699924, - "setstat": 2153700008, - "getstat": 2153700024, - "getbonusblockstart": 2153700036, - "getbitoffset": 2153700112, - "getbitsize": 2153700188, - "readextradata": 2153700260, - "saveextradata": 2153700544, - "resetextradata": 2153700848, - "setkongigt": 2153700856, - "updatepercentagekongstat": 2153700908, - "genericstatupdate": 2153701120, - "updatetagstat": 2153701232, - "updatefairystat": 2153701348, - "updatekopstat": 2153701388, - "updateenemykillstat": 2153701396, - "createendseqcreditsfile": 2153701500, - "displaynumberonobject": 2153702288, - "shiftbrokenjapesportal": 2153702500, - "displaynumberontns": 2153702616, - "writewti": 2153703164, - "handle_wti": 2153703204, - "warptoisles": 2153703416, - "beatgame": 2153703524, - "finalizebeatgame": 2153703600, - "hasbeatendkrapwincon": 2153703700, - "checkseedvictory": 2153704000, - "checkvictory_flaghook": 2153704288, - "issnapenemyinrange": 2153704316, - "getpkmnsnapdata": 2153704800, - "pokemonsnapmode": 2153704956, - "arcadeexit": 2153705692, - "determinearcadelevel": 2153705724, - "handlearcadevictory": 2153705840, - "spawnoverlaytext": 2153706180, - "overlay_mod_bonus": 2153706492, - "overlay_mod_boss": 2153706788, - "overlay_changes": 2153707036, - "parsecutscenedata": 2153707248, - "loadjetpacsprites_handler": 2153707636, - "initjetpac": 2153707704, - "patchcrankycode": 2153707816, - "give_all_blueprints": 2153707964, - "overlay_mod_menu": 2153708172, - "overlay_mod_race": 2153708616, - "updatepausescreenwheel": 2153708796, - "newpausespritecode": 2153708876, - "totalssprite": 2153709764, - "checkssprite": 2153709772, - "handlespritecode": 2153709780, - "initcarousel_onpause": 2153709848, - "initcarousel_onboot": 2153710216, - "file_sprites": 2153710320, - "file_item_caps": 2153710388, - "file_items": 2153710440, - "initprogressivetimer": 2153710532, - "renderprogressivesprite": 2153710548, - "playprogressiveding": 2153710608, - "handleprogressiveindicator": 2153710628, - "resetprogressive": 2153710660, - "inithints": 2153710700, - "wipehintcache": 2153710876, - "drawhinttext": 2153710944, - "drawsplitstring": 2153711484, - "gethintrequirement": 2153712288, - "displaycbcount": 2153712332, - "gethintitemregion": 2153712528, - "showhint": 2153712552, - "displaybubble": 2153712664, - "gettiedshopmoveflag": 2153712824, - "getitemspecificity": 2153712888, - "inithintflags": 2153713252, - "getitemname": 2153713424, - "drawhintscreen": 2153713564, - "drawitemlocationscreen": 2153714520, - "item_names": 2153715868, - "item_name_plural": 2153715932, - "hints_initialized": 2153715948, - "display_billboard_fix": 2153715949, - "hint_region_names": 2153716988, - "unknown_hints": 2153717232, - "printleveligt": 2153718376, - "inititemcheckdenominators": 2153718760, - "checkitemdb": 2153718996, - "handlecshifting": 2153719556, - "pausescreen3and4header": 2153719688, - "drawtextpointers": 2153720268, - "pausescreen3and4itemname": 2153720420, - "pausescreen3and4counter": 2153720576, - "changepausescreen": 2153720828, - "changeselectedlevel": 2153720972, - "updatefilevariables": 2153721056, - "handleoutofcounters": 2153721092, - "initpausemenu": 2153721264, - "sethintregion": 2153722184, - "storehintregion": 2153723176, - "gethintregiontext": 2153723272, - "displayhintregion": 2153723404, - "getworldoffset": 2153724008, - "setblockerhead": 2153724056, - "displayblockeritemonhud": 2153724204, - "getcountofblockerrequireditem": 2153724296, - "displaycountonblockerteeth": 2153724360, - "cc_enable_drunky": 2153724476, - "cc_disable_drunky": 2153724540, - "cc_allower_generic": 2153724584, - "cc_enabler_icetrap": 2153724732, - "cc_allower_icetrap": 2153724764, - "cc_enabler_warptorap": 2153724816, - "handlegamemodewrapper": 2153724900, - "cc_disabler_warptorap": 2153724972, - "skipdktv": 2153725100, - "displaygetoutreticle": 2153725184, - "cc_enable_getout": 2153725508, - "fakegetout": 2153725692, - "cc_allower_rockfall": 2153726184, - "cc_enabler_rockfall": 2153726200, - "dummyguardcode": 2153726452, - "cc_allower_spawnkop": 2153726684, - "cc_enabler_spawnkop": 2153726716, - "cc_allower_balloon": 2153726880, - "cc_allower_backflip": 2153726932, - "cc_enabler_balloon": 2153726988, - "cc_enabler_slip": 2153727124, - "cc_allower_tag": 2153727196, - "cc_enabler_tag": 2153727316, - "cc_enabler_doabackflip": 2153727512, - "cc_enabler_ice": 2153727596, - "cc_disabler_ice": 2153727620, - "cc_allower_animals": 2153727668, - "cc_enabler_animals": 2153727728, - "cc_disabler_animals": 2153727976, - "cc_allower_mini": 2153728048, - "cc_setscale": 2153728092, - "cc_enabler_mini": 2153728140, - "cc_disabler_mini": 2153728220, - "cc_allower_boulder": 2153728276, - "cc_enabler_boulder": 2153728328, - "cc_effect_handler": 2153728400, - "replace_zones": 2153729304, - "blastwarphandler": 2153729700, - "swap_ending_cutscene_model": 2153729868, - "completeboss": 2153730084, - "fixkroolkong": 2153730768, - "handlekroolsaveprogress": 2153730868, - "swaprequirements": 2153731556, - "level_order_rando_funcs": 2153731632, - "writehudamount": 2153731720, - "movetransplant": 2153732024, - "progressivechange": 2153732092, - "getmoveprogressiveflagtype": 2153732492, - "writeprogressivetext": 2153732600, - "getnextmovepurchase": 2153732860, - "getpurchaseclassification": 2153733612, - "addhelmhurrypurchasetime": 2153734160, - "purchasemove": 2153734224, - "checkfirstmovepurchase": 2153735080, - "purchasefirstmovehandler": 2153735252, - "setlocation": 2153735360, - "getlocation": 2153736368, - "setlocationstatus": 2153736824, - "getlocationstatus": 2153736908, - "displaymovetext": 2153736996, - "getnextmovetext": 2153737308, - "displaybfimovetext": 2153739936, - "showpostmovetext": 2153740064, - "simianslamnames": 2153741304, - "specialmovesnames": 2153741312, - "gunnames": 2153741352, - "gunupgnames": 2153741360, - "ammobeltnames": 2153741364, - "instrumentnames": 2153741368, - "instrumentupgnames": 2153741376, - "alter_price": 2153741384, - "pricetransplant": 2153741420, - "destroybonus": 2153741768, - "completebonus": 2153741800, - "applyfaststart": 2153741988, - "opencrowndoor": 2153742164, - "opencoindoor": 2153742196, - "helminit": 2153742228, - "helmbarrelcode": 2153743096, - "checkdooritem": 2153743436, - "crowndoorcheck": 2153743824, - "coindoorcheck": 2153743836, - "initkongrando": 2153743976, - "initfile_checktraining": 2153744216, - "initfile_hasgun": 2153744248, - "initfile_hasinstrument": 2153744404, - "initfile_getbeltlevel": 2153744556, - "initfile_getinsupgradelevel": 2153744704, - "initfile_getslamlevel": 2153744864, - "initfile_getkongpotionbitfield": 2153745024, - "unlockmoves": 2153745440, - "apply_key": 2153747000, - "pre_turn_keys": 2153747332, - "writekeyflags": 2153747864, - "auto_turn_keys": 2153747880, - "qualityoflife_shorteners": 2153748024, - "fastwarp": 2153748116, - "fastwarp_playmusic": 2153748148, - "fastwarpshockwavefix": 2153748176, - "clearvulturecutscene": 2153748280, + "orangeguncode": 2153688380, + "istbarrelflag": 2153689664, + "isfairyflag": 2153689680, + "gethinttextindex": 2153689720, + "isgoodtextbox": 2153690916, + "getmovehint": 2153691028, + "resetdisplayedmusic": 2153691140, + "detectsongchange": 2153691152, + "initsongdisplay": 2153691492, + "displaysongnamehandler": 2153691804, + "curseremoved": 2153692396, + "haspermalossgrace": 2153692408, + "determinekongunlock": 2153692436, + "unlockkongpermaloss": 2153692580, + "givekongmoves": 2153692692, + "isdeathstate": 2153692760, + "kong_has_died": 2153692800, + "determinestartkong_permalossmode": 2153693352, + "transitionkong": 2153693520, + "fixgracecheese": 2153693828, + "changekongontransition_permaloss": 2153694000, + "forcebosskong": 2153694048, + "preventbosscheese": 2153694276, + "doeskongpossessmove": 2153694448, + "issharedmove": 2153695088, + "getcounteritem": 2153695300, + "getmovecountinshop": 2153695872, + "wipecounterimagecache": 2153696252, + "loadinternaltexture": 2153696300, + "loadfonttexture_counter": 2153696464, + "updatecounterdisplay": 2153696628, + "getactormodeltwodist": 2153696808, + "getclosestshop": 2153696944, + "getshopscale": 2153697596, + "newcountercode": 2153697936, + "handlefootprogress": 2153699336, + "changestat": 2153699788, + "setstat": 2153699872, + "getstat": 2153699888, + "getbonusblockstart": 2153699900, + "getbitoffset": 2153699976, + "getbitsize": 2153700052, + "readextradata": 2153700124, + "saveextradata": 2153700408, + "resetextradata": 2153700712, + "setkongigt": 2153700720, + "updatepercentagekongstat": 2153700772, + "genericstatupdate": 2153700984, + "updatetagstat": 2153701096, + "updatefairystat": 2153701212, + "updatekopstat": 2153701252, + "updateenemykillstat": 2153701260, + "createendseqcreditsfile": 2153701364, + "displaynumberonobject": 2153702152, + "shiftbrokenjapesportal": 2153702364, + "displaynumberontns": 2153702480, + "writewti": 2153703028, + "handle_wti": 2153703068, + "warptoisles": 2153703280, + "beatgame": 2153703388, + "finalizebeatgame": 2153703464, + "hasbeatendkrapwincon": 2153703564, + "checkseedvictory": 2153703864, + "checkvictory_flaghook": 2153704152, + "issnapenemyinrange": 2153704180, + "getpkmnsnapdata": 2153704664, + "pokemonsnapmode": 2153704820, + "arcadeexit": 2153705556, + "determinearcadelevel": 2153705588, + "handlearcadevictory": 2153705704, + "spawnoverlaytext": 2153706044, + "overlay_mod_bonus": 2153706356, + "overlay_mod_boss": 2153706652, + "overlay_changes": 2153706900, + "parsecutscenedata": 2153707112, + "loadjetpacsprites_handler": 2153707500, + "initjetpac": 2153707568, + "patchcrankycode": 2153707680, + "give_all_blueprints": 2153707828, + "overlay_mod_menu": 2153708036, + "overlay_mod_race": 2153708480, + "updatepausescreenwheel": 2153708660, + "newpausespritecode": 2153708740, + "totalssprite": 2153709628, + "checkssprite": 2153709636, + "handlespritecode": 2153709644, + "initcarousel_onpause": 2153709712, + "initcarousel_onboot": 2153710080, + "file_sprites": 2153710184, + "file_item_caps": 2153710252, + "file_items": 2153710304, + "initprogressivetimer": 2153710396, + "renderprogressivesprite": 2153710412, + "playprogressiveding": 2153710472, + "handleprogressiveindicator": 2153710492, + "resetprogressive": 2153710524, + "inithints": 2153710564, + "wipehintcache": 2153710740, + "drawhinttext": 2153710808, + "drawsplitstring": 2153711348, + "gethintrequirement": 2153712152, + "displaycbcount": 2153712196, + "gethintitemregion": 2153712392, + "showhint": 2153712416, + "displaybubble": 2153712528, + "gettiedshopmoveflag": 2153712688, + "getitemspecificity": 2153712752, + "inithintflags": 2153713116, + "getitemname": 2153713288, + "drawhintscreen": 2153713428, + "drawitemlocationscreen": 2153714384, + "item_names": 2153715732, + "item_name_plural": 2153715796, + "hints_initialized": 2153715812, + "display_billboard_fix": 2153715813, + "hint_region_names": 2153716852, + "unknown_hints": 2153717096, + "printleveligt": 2153718240, + "inititemcheckdenominators": 2153718624, + "checkitemdb": 2153718860, + "handlecshifting": 2153719420, + "pausescreen3and4header": 2153719552, + "drawtextpointers": 2153720132, + "pausescreen3and4itemname": 2153720284, + "pausescreen3and4counter": 2153720440, + "changepausescreen": 2153720692, + "changeselectedlevel": 2153720836, + "updatefilevariables": 2153720920, + "handleoutofcounters": 2153720956, + "initpausemenu": 2153721128, + "sethintregion": 2153722048, + "storehintregion": 2153723040, + "gethintregiontext": 2153723136, + "displayhintregion": 2153723268, + "getworldoffset": 2153723872, + "setblockerhead": 2153723920, + "displayblockeritemonhud": 2153724068, + "getcountofblockerrequireditem": 2153724160, + "displaycountonblockerteeth": 2153724224, + "cc_enable_drunky": 2153724340, + "cc_disable_drunky": 2153724404, + "cc_allower_generic": 2153724448, + "cc_enabler_icetrap": 2153724596, + "cc_allower_icetrap": 2153724628, + "cc_enabler_warptorap": 2153724680, + "handlegamemodewrapper": 2153724764, + "cc_disabler_warptorap": 2153724836, + "skipdktv": 2153724964, + "displaygetoutreticle": 2153725048, + "cc_enable_getout": 2153725372, + "fakegetout": 2153725556, + "cc_allower_rockfall": 2153726048, + "cc_enabler_rockfall": 2153726064, + "dummyguardcode": 2153726316, + "cc_allower_spawnkop": 2153726548, + "cc_enabler_spawnkop": 2153726580, + "cc_allower_balloon": 2153726744, + "cc_allower_backflip": 2153726796, + "cc_enabler_balloon": 2153726852, + "cc_enabler_slip": 2153726988, + "cc_allower_tag": 2153727060, + "cc_enabler_tag": 2153727180, + "cc_enabler_doabackflip": 2153727376, + "cc_enabler_ice": 2153727460, + "cc_disabler_ice": 2153727484, + "cc_allower_animals": 2153727532, + "cc_enabler_animals": 2153727592, + "cc_disabler_animals": 2153727840, + "cc_allower_mini": 2153727912, + "cc_setscale": 2153727956, + "cc_enabler_mini": 2153728004, + "cc_disabler_mini": 2153728084, + "cc_allower_boulder": 2153728140, + "cc_enabler_boulder": 2153728192, + "cc_effect_handler": 2153728264, + "replace_zones": 2153729168, + "blastwarphandler": 2153729564, + "swap_ending_cutscene_model": 2153729732, + "completeboss": 2153729948, + "fixkroolkong": 2153730632, + "handlekroolsaveprogress": 2153730732, + "swaprequirements": 2153731420, + "level_order_rando_funcs": 2153731496, + "writehudamount": 2153731584, + "movetransplant": 2153731888, + "progressivechange": 2153731956, + "getmoveprogressiveflagtype": 2153732356, + "writeprogressivetext": 2153732464, + "getnextmovepurchase": 2153732724, + "getpurchaseclassification": 2153733476, + "addhelmhurrypurchasetime": 2153734024, + "purchasemove": 2153734088, + "checkfirstmovepurchase": 2153734944, + "purchasefirstmovehandler": 2153735116, + "setlocation": 2153735224, + "getlocation": 2153736232, + "setlocationstatus": 2153736688, + "getlocationstatus": 2153736772, + "displaymovetext": 2153736860, + "getnextmovetext": 2153737172, + "displaybfimovetext": 2153739800, + "showpostmovetext": 2153739928, + "simianslamnames": 2153741168, + "specialmovesnames": 2153741176, + "gunnames": 2153741216, + "gunupgnames": 2153741224, + "ammobeltnames": 2153741228, + "instrumentnames": 2153741232, + "instrumentupgnames": 2153741240, + "alter_price": 2153741248, + "pricetransplant": 2153741284, + "destroybonus": 2153741632, + "completebonus": 2153741664, + "applyfaststart": 2153741852, + "opencrowndoor": 2153742028, + "opencoindoor": 2153742060, + "helminit": 2153742092, + "helmbarrelcode": 2153742960, + "checkdooritem": 2153743300, + "crowndoorcheck": 2153743688, + "coindoorcheck": 2153743700, + "initkongrando": 2153743840, + "initfile_checktraining": 2153744080, + "initfile_hasgun": 2153744112, + "initfile_hasinstrument": 2153744268, + "initfile_getbeltlevel": 2153744420, + "initfile_getinsupgradelevel": 2153744568, + "initfile_getslamlevel": 2153744728, + "initfile_getkongpotionbitfield": 2153744888, + "unlockmoves": 2153745304, + "apply_key": 2153746864, + "pre_turn_keys": 2153747196, + "writekeyflags": 2153747728, + "auto_turn_keys": 2153747744, + "qualityoflife_shorteners": 2153747888, + "fastwarp": 2153747980, + "fastwarp_playmusic": 2153748012, + "fastwarpshockwavefix": 2153748040, + "clearvulturecutscene": 2153748144, "codeend": 2153754112, "copyfunc": 2153756496, "regularframeloop": 2153759408, diff --git a/templates/cosmetics.html.jinja2 b/templates/cosmetics.html.jinja2 index bf3b57f47..fd1d76488 100644 --- a/templates/cosmetics.html.jinja2 +++ b/templates/cosmetics.html.jinja2 @@ -130,6 +130,7 @@ {{ toggle_input("dark_mode_textboxes", "Dark Mode UI", "Text bubbles will be darkened, with the font brightened. The DPad Graphic will be darkened.") }} {{ toggle_input("pause_hint_coloring", "Color Hints in Pause Menu", "Various important segments in hints on the pause menu will be colored to assist with parsing.", True) }} {{ toggle_input("disco_chunky", "Disco Chunky", "Gives Chunky his disco outfit. Will only work if his model is set to regular chunky.") }} + {{ toggle_input("smoother_camera", "Smoother Camera", "Controlling the camera with the C Buttons behaves closer to how Banjo-Tooie operates.") }}

From d36b7c90f8f485ba101c32a3b4e7df89810f1f39 Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Fri, 27 Dec 2024 09:43:20 -0600 Subject: [PATCH 08/13] Tidy of Colorblind --- randomizer/Patching/CosmeticColors.py | 2 + randomizer/Patching/Cosmetics/Colorblind.py | 529 +++++++------------- randomizer/Patching/LibImage.py | 9 + 3 files changed, 189 insertions(+), 351 deletions(-) diff --git a/randomizer/Patching/CosmeticColors.py b/randomizer/Patching/CosmeticColors.py index 4b9da6c6a..cf50c5e1f 100644 --- a/randomizer/Patching/CosmeticColors.py +++ b/randomizer/Patching/CosmeticColors.py @@ -349,6 +349,7 @@ def maskImageWithOutline(im_f, base_index, min_y, colorblind_mode, type=""): pix[x, y] = (mask2[0], mask2[1], mask2[2], base[3]) return im_f + SINGLE_START = [168, 152, 232, 208, 240] BALLOON_START = [5835, 5827, 5843, 5851, 5819] LASER_START = [784, 748, 363, 760, 772] @@ -491,6 +492,7 @@ def overwrite_object_colors(settings, ROM_COPY: ROM): Kongs.any: (100, 255, 60), } + def applyKongModelSwaps(settings: Settings) -> None: """Apply Krusha Kong setting.""" ROM_COPY = LocalROM() diff --git a/randomizer/Patching/Cosmetics/Colorblind.py b/randomizer/Patching/Cosmetics/Colorblind.py index 842bde50e..cfd3cc2c4 100644 --- a/randomizer/Patching/Cosmetics/Colorblind.py +++ b/randomizer/Patching/Cosmetics/Colorblind.py @@ -13,6 +13,7 @@ writeColorImageToROM, ExtraTextures, getBonusSkinOffset, + rgba32to5551, ) from randomizer.Patching.Lib import getRawFile, TableNames, writeRawFile from randomizer.Patching.Patcher import ROM @@ -20,7 +21,16 @@ from PIL import ImageEnhance -def writeKasplatHairColorToROM(color, table_index, file_index, format: str, ROM_COPY: ROM): +def changeVertexColor(num_data: list[int], offset: int, new_color: list[int]) -> list[int]: + """Changes the vertex color based on the luminance of the original.""" + total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) + channel_light = int(total_light / 3) + for i in range(3): + num_data[offset + i] = int(channel_light * (new_color[i] / 255)) + return num_data + + +def writeKasplatHairColorToROM(color: str, table_index: TableNames, file_index: int, format: str, ROM_COPY: ROM): """Write color to ROM for kasplats.""" file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] mask = getRGBFromHash(color) @@ -29,11 +39,7 @@ def writeKasplatHairColorToROM(color, table_index, file_index, format: str, ROM_ color_lst.append(255) # Alpha null_color = [0] * 4 else: - val_r = int((mask[0] >> 3) << 11) - val_g = int((mask[1] >> 3) << 6) - val_b = int((mask[2] >> 3) << 1) - rgba_val = val_r | val_g | val_b | 1 - color_lst = [(rgba_val >> 8) & 0xFF, rgba_val & 0xFF] + color_lst = rgba32to5551(mask) null_color = [0, 0] bytes_array = [] for y in range(42): @@ -46,13 +52,13 @@ def writeKasplatHairColorToROM(color, table_index, file_index, format: str, ROM_ for i in range(3): bytes_array.extend(color_lst) data = bytearray(bytes_array) - if table_index == 25: + if table_index == TableNames.TexturesGeometry: data = gzip.compress(data, compresslevel=9) ROM_COPY.seek(file_start) ROM_COPY.writeBytes(data) -def writeWhiteKasplatHairColorToROM(color1, color2, table_index, file_index, format: str, ROM_COPY: ROM): +def writeWhiteKasplatHairColorToROM(color1: str, color2: str, table_index: TableNames, file_index: int, format: str, ROM_COPY: ROM): """Write color to ROM for white kasplats, giving them a black-white block pattern.""" file_start = js.pointer_addresses[table_index]["entries"][file_index]["pointing_to"] mask = getRGBFromHash(color1) @@ -64,16 +70,8 @@ def writeWhiteKasplatHairColorToROM(color1, color2, table_index, file_index, for color_lst_1.append(255) null_color = [0] * 4 else: - val_r = int((mask[0] >> 3) << 11) - val_g = int((mask[1] >> 3) << 6) - val_b = int((mask[2] >> 3) << 1) - rgba_val = val_r | val_g | val_b | 1 - val_r2 = int((mask2[0] >> 3) << 11) - val_g2 = int((mask2[1] >> 3) << 6) - val_b2 = int((mask2[2] >> 3) << 1) - rgba_val2 = val_r2 | val_g2 | val_b2 | 1 - color_lst_0 = [(rgba_val >> 8) & 0xFF, rgba_val & 0xFF] - color_lst_1 = [(rgba_val2 >> 8) & 0xFF, rgba_val2 & 0xFF] + color_lst_0 = rgba32to5551(mask) + color_lst_1 = rgba32to5551(mask2) null_color = [0] * 2 bytes_array = [] for y in range(42): @@ -173,15 +171,8 @@ def writeSpecialKlaptrapTextureToROM(color_index, table_index, file_index, forma def calculateKlaptrapPixel(mask: list, format: str): """Calculate the new color for the given pixel.""" if format == TextureFormat.RGBA32: - color_lst = mask.copy() - color_lst.append(255) # Alpha - else: - val_r = int((mask[0] >> 3) << 11) - val_g = int((mask[1] >> 3) << 6) - val_b = int((mask[2] >> 3) << 1) - rgba_val = val_r | val_g | val_b | 1 - color_lst = [(rgba_val >> 8) & 0xFF, rgba_val & 0xFF] - return color_lst + return mask + [255] + return rgba32to5551(mask) def maskBlueprintImage(im_f, base_index, mode: ColorblindMode): @@ -277,6 +268,75 @@ def maskPotionImage(im_f, primary_color, secondary_color=None): return im_f +WRINKLY_DOOR_COLOR_1_OFFSETS = [ + 1548, + 1580, + 1612, + 1644, + 1676, + 1708, + 1756, + 1788, + 1804, + 1820, + 1836, + 1852, + 1868, + 1884, + 1900, + 1916, + 1932, + 1948, + 1964, + 1980, + 1996, + 2012, + 2028, + 2044, + 2076, + 2108, + 2124, + 2156, + 2188, + 2220, + 2252, + 2284, + 2316, + 2348, + 2380, + 2396, + 2412, + 2428, + 2444, + 2476, + 2508, + 2540, + 2572, + 2604, + 2636, + 2652, + 2668, + 2684, + 2700, + 2716, + 2732, + 2748, + 2764, + 2780, + 2796, + 2812, + 2828, + 2860, + 2892, + 2924, + 2956, + 2988, + 3020, + 3052, +] +WRINKLY_DOOR_COLOR_2_OFFSETS = [1564, 1596, 1628, 1660, 1692, 1724, 1740, 1772, 2332, 2364, 2460, 2492, 2524, 2556, 2588, 2620] + + def recolorWrinklyDoors(mode: ColorblindMode, ROM_COPY: ROM): """Recolor the Wrinkly hint door doorframes for colorblind mode.""" file = [0xF0, 0xF2, 0xEF, 0x67, 0xF1] @@ -287,96 +347,6 @@ def recolorWrinklyDoors(mode: ColorblindMode, ROM_COPY: ROM): for d in data: num_data.append(d) # Figure out which colors to use and where to put them (list extensions to mitigate the linter's "artistic freedom" putting 1 value per line) - color1_offsets = [ - 1548, - 1580, - 1612, - 1644, - 1676, - 1708, - 1756, - 1788, - 1804, - 1820, - 1836, - 1852, - 1868, - 1884, - 1900, - 1916, - ] - color1_offsets = color1_offsets + [ - 1932, - 1948, - 1964, - 1980, - 1996, - 2012, - 2028, - 2044, - 2076, - 2108, - 2124, - 2156, - 2188, - 2220, - 2252, - 2284, - ] - color1_offsets = color1_offsets + [ - 2316, - 2348, - 2380, - 2396, - 2412, - 2428, - 2444, - 2476, - 2508, - 2540, - 2572, - 2604, - 2636, - 2652, - 2668, - 2684, - ] - color1_offsets = color1_offsets + [ - 2700, - 2716, - 2732, - 2748, - 2764, - 2780, - 2796, - 2812, - 2828, - 2860, - 2892, - 2924, - 2956, - 2988, - 3020, - 3052, - ] - color2_offsets = [ - 1564, - 1596, - 1628, - 1660, - 1692, - 1724, - 1740, - 1772, - 2332, - 2364, - 2460, - 2492, - 2524, - 2556, - 2588, - 2620, - ] color_str = getKongItemColor(mode, kong) new_color1 = getRGBFromHash(color_str) new_color2 = getRGBFromHash(color_str) @@ -385,10 +355,10 @@ def recolorWrinklyDoors(mode: ColorblindMode, ROM_COPY: ROM): new_color2[channel] = max(80, new_color1[channel]) # Too black is bad, because anything times 0 is 0 # Recolor the doorframe - for offset in color1_offsets: + for offset in WRINKLY_DOOR_COLOR_1_OFFSETS: for i in range(3): num_data[offset + i] = new_color1[i] - for offset in color2_offsets: + for offset in WRINKLY_DOOR_COLOR_2_OFFSETS: for i in range(3): num_data[offset + i] = new_color2[i] @@ -411,115 +381,40 @@ def recolorKRoolShipSwitch(color: tuple, ROM_COPY: ROM): for x in range(3): data[addr + x] = color[x] new_tex = [ - 0xE7, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0xE2, - 0x00, - 0x00, - 0x1C, - 0x0C, - 0x19, - 0x20, - 0x38, - 0xE3, - 0x00, - 0x0A, - 0x01, - 0x00, - 0x10, - 0x00, - 0x00, - 0xE3, - 0x00, - 0x0F, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0xE7, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0xFC, - 0x12, - 0x7E, - 0x03, - 0xFF, - 0xFF, - 0xF9, - 0xF8, - 0xFD, - 0x90, - 0x00, - 0x00, - 0x00, - 0x00, - 0x0B, - 0xAF, - 0xF5, - 0x90, - 0x00, - 0x00, - 0x07, - 0x08, - 0x02, - 0x00, - 0xE6, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0xF3, - 0x00, - 0x00, - 0x00, - 0x07, - 0x7F, - 0xF1, - 0x00, - 0xE7, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0xF5, - 0x88, - 0x10, - 0x00, - 0x00, - 0x08, - 0x02, - 0x00, - 0xF2, - 0x00, - 0x00, - 0x00, - 0x00, - 0x0F, - 0xC0, - 0xFC, + 0xE7000000, + 0x00000000, + 0xE200001C, + 0x0C192038, + 0xE3000A01, + 0x00100000, + 0xE3000F00, + 0x00000000, + 0xE7000000, + 0x00000000, + 0xFC127E03, + 0xFFFFF9F8, + 0xFD900000, + 0x00000BAF, + 0xF5900000, + 0x07080200, + 0xE6000000, + 0x00000000, + 0xF3000000, + 0x077FF100, + 0xE7000000, + 0x00000000, + 0xF5881000, + 0x00080200, + 0xF2000000, + 0x000FC0FC, ] for x in range(8): data[0x1AD8 + x] = 0 for xi, x in enumerate(new_tex): - data[0x1AE8 + xi] = x + for y in range(4): + offset = (xi * 4) + y + shift = 24 - (8 * y) + data[0x1AE8 + offset] = (x >> shift) & 0xFF for x in range(40): data[0x1B58 + x] = 0 writeRawFile(TableNames.ModelTwoGeometry, 305, True, data, ROM_COPY) @@ -529,8 +424,7 @@ def recolorSlamSwitches(galleon_switch_value, ROM_COPY: ROM, mode: ColorblindMod """Recolor the Simian Slam switches for colorblind mode.""" file = [0x94, 0x93, 0x95, 0x96, 0xB8, 0x16C, 0x16B, 0x16D, 0x16E, 0x16A, 0x167, 0x166, 0x168, 0x169, 0x165] written_galleon_ship = False - for switch in range(15): - file_index = file[switch] + for switch_index, file_index in enumerate(file): data = getRawFile(TableNames.ModelTwoGeometry, file_index, True) num_data = [] # data, but represented as nums rather than b strings for d in data: @@ -541,19 +435,17 @@ def recolorSlamSwitches(galleon_switch_value, ROM_COPY: ROM, mode: ColorblindMod new_color2 = getKongItemColor(mode, Kongs.lanky, True) new_color3 = getKongItemColor(mode, Kongs.diddy, True) - # Green switches - if switch < 5: - for offset in color_offsets: + for offset in color_offsets: + # Green switches + if switch_index < 5: for i in range(3): num_data[offset + i] = new_color1[i] - # Blue switches - elif switch < 10: - for offset in color_offsets: + # Blue switches + elif switch_index < 10: for i in range(3): num_data[offset + i] = new_color2[i] - # Red switches - else: - for offset in color_offsets: + # Red switches + else: for i in range(3): num_data[offset + i] = new_color3[i] @@ -573,8 +465,7 @@ def recolorSlamSwitches(galleon_switch_value, ROM_COPY: ROM, mode: ColorblindMod def recolorBlueprintModelTwo(mode: ColorblindMode, ROM_COPY: ROM): """Recolor the Blueprint Model2 items for colorblind mode.""" file = [0xDE, 0xE0, 0xE1, 0xDD, 0xDF] - for kong in range(5): - file_index = file[kong] + for kong, file_index in enumerate(file): data = getRawFile(TableNames.ModelTwoGeometry, file_index, True) num_data = [] # data, but represented as nums rather than b strings for d in data: @@ -583,30 +474,15 @@ def recolorBlueprintModelTwo(mode: ColorblindMode, ROM_COPY: ROM): color1_offsets = [0x52C, 0x54C, 0x57C, 0x58C, 0x5AC, 0x5CC, 0x5FC, 0x61C] color2_offsets = [0x53C, 0x55C, 0x5EC, 0x60C] color3_offsets = [0x56C, 0x59C, 0x5BC, 0x5DC] + color_offsets = color1_offsets + color2_offsets + color3_offsets new_color = getKongItemColor(mode, kong, True) if kong == 0: for channel in range(3): new_color[channel] = max(39, new_color[channel]) # Too black is bad, because anything times 0 is 0 # Recolor the model2 item - for offset in color1_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color2_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color3_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + for offset in color_offsets: + num_data = changeVertexColor(num_data, offset, new_color) data = bytearray(num_data) # convert num_data back to binary string writeRawFile(TableNames.ModelTwoGeometry, file_index, True, data, ROM_COPY) @@ -719,20 +595,20 @@ def recolorRotatingRoomTiles(mode): mask = mask.resize((resize[0], resize[1])) masked_tile = maskImageRotatingRoomTile(tile_image, mask, question_mark_offsets[(tile % 2)], int(tile / 2), (tile % 2), mode) writeColorImageToROM(masked_tile, 7, question_mark_tiles[tile], 32, 64, False, TextureFormat.RGBA5551) - for tile in range(len(face_tiles)): - face_index = int(tile / 4) + for tile_index, tile in enumerate(face_tiles): + face_index = int(tile_index / 4) if face_index < 5: width = 32 height = 64 else: width = 44 height = 44 - mask = getImageFile(25, face_tile_masks[int(tile / 2)], True, width, height, TextureFormat.RGBA5551) + mask = getImageFile(25, face_tile_masks[int(tile_index / 2)], True, width, height, TextureFormat.RGBA5551) resize = face_resize[face_index] mask = mask.resize((resize[0], resize[1])) - tile_image = getImageFile(7, face_tiles[tile], False, 32, 64, TextureFormat.RGBA5551) - masked_tile = maskImageRotatingRoomTile(tile_image, mask, face_offsets[int(tile / 2)], face_index, (int(tile / 2) % 2), mode) - writeColorImageToROM(masked_tile, 7, face_tiles[tile], 32, 64, False, TextureFormat.RGBA5551) + tile_image = getImageFile(7, tile, False, 32, 64, TextureFormat.RGBA5551) + masked_tile = maskImageRotatingRoomTile(tile_image, mask, face_offsets[int(tile_index / 2)], face_index, (int(tile_index / 2) % 2), mode) + writeColorImageToROM(masked_tile, 7, tile, 32, 64, False, TextureFormat.RGBA5551) def recolorBells(ROM_COPY: ROM): @@ -783,17 +659,30 @@ def recolorKlaptraps(mode, ROM_COPY: ROM): writeSpecialKlaptrapTextureToROM(4, 25, 0xF38, TextureFormat.RGBA5551, belly_pixels_to_ignore, mode, ROM_COPY) -def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): - """Overwrite potion colors.""" - diddy_color = getKongItemColor(colorblind_mode, Kongs.diddy) - chunky_color = getKongItemColor(colorblind_mode, Kongs.chunky) +def getPotionColor(colorblind_mode: ColorblindMode, kong: Kongs) -> list[int]: + """Get the potion color associated with a kong.""" + diddy_color = getKongItemColor(colorblind_mode, Kongs.diddy, True) + chunky_color = getKongItemColor(colorblind_mode, Kongs.chunky, True) secondary_color = [diddy_color, None, chunky_color, diddy_color, None, None] if colorblind_mode == ColorblindMode.trit: - secondary_color[0] = chunky_color - secondary_color[2] = None - for color in range(len(secondary_color)): - if secondary_color[color] is not None: - secondary_color[color] = getRGBFromHash(secondary_color[color]) + secondary_color[Kongs.donkey] = chunky_color + secondary_color[Kongs.lanky] = None + if kong < 5: + new_color = getKongItemColor(colorblind_mode, kong, True) + else: + new_color = [0xFF, 0xFF, 0xFF] + if secondary_color[kong] is not None: + if kong == Kongs.tiny: + return secondary_color[kong].copy() + elif kong == Kongs.donkey: + return [int(x / 8) for x in secondary_color[kong]] + else: + return [int(x / 4) for x in secondary_color[kong]] + return new_color.copy() + + +def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): + """Overwrite potion colors.""" # Actor: file = [[0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2], [0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA]] @@ -810,6 +699,7 @@ def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): color3_offsets = [0x64, 0x74, 0x84, 0xE4] color4_offsets = [0x94] color5_offsets = [0xB4, 0xC4, 0xD4] + color_not_base_offsets = color2_offsets + color3_offsets + color4_offsets + color5_offsets # color6_offsets = [0xF4, 0x104, 0x114, 0x124, 0x134, 0x144, 0x154, 0x164] if potion_color < 5: new_color = getKongItemColor(colorblind_mode, potion_color, True) @@ -817,43 +707,11 @@ def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): new_color = [0xFF, 0xFF, 0xFF] # Recolor the actor item + color_applied = getPotionColor(colorblind_mode, potion_color) for offset in color1_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - if secondary_color[potion_color] is not None and potion_color == 3: # tiny - num_data[offset + i] = int(num_data[offset + i] * (secondary_color[potion_color][i] / 255)) - elif secondary_color[potion_color] is not None: # donkey gets an even darker shade - num_data[offset + i] = int(num_data[offset + i] * (int(secondary_color[potion_color][i] / 8) / 255)) - elif secondary_color[potion_color] is not None: # other kongs with a secondary color get a darker shade - num_data[offset + i] = int(num_data[offset + i] * (int(secondary_color[potion_color][i] / 4) / 255)) - else: - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color2_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color3_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color4_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color5_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + num_data = changeVertexColor(num_data, offset, color_applied) + for offset in color_not_base_offsets: + num_data = changeVertexColor(num_data, offset, new_color) data = bytearray(num_data) # convert num_data back to binary string writeRawFile(TableNames.ActorGeometry, file_index, True, data, ROM_COPY) @@ -872,6 +730,7 @@ def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): color3_offsets = [0x174, 0x184, 0x194, 0x1F4] color4_offsets = [0x1A4] color5_offsets = [0x1C4, 0x1D4, 0x1E4] + color_not_base_offsets = color2_offsets + color3_offsets + color4_offsets + color5_offsets # color6_offsets = [0x204, 0x214, 0x224, 0x234, 0x244, 0x254, 0x264, 0x274] if potion_color < 5: new_color = getKongItemColor(colorblind_mode, potion_color, True) @@ -879,58 +738,26 @@ def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): new_color = [0xFF, 0xFF, 0xFF] # Recolor the model2 item + color_applied = getPotionColor(colorblind_mode, potion_color) for offset in color1_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - if secondary_color[potion_color] is not None and potion_color == 3: # tiny - num_data[offset + i] = int(num_data[offset + i] * (secondary_color[potion_color][i] / 255)) - elif secondary_color[potion_color] is not None: # donkey gets an even darker shade - num_data[offset + i] = int(num_data[offset + i] * (int(secondary_color[potion_color][i] / 8) / 255)) - elif secondary_color[potion_color] is not None: # other kongs with a secondary color get a darker shade - num_data[offset + i] = int(num_data[offset + i] * (int(secondary_color[potion_color][i] / 4) / 255)) - else: - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color2_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color3_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color4_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) - for offset in color5_offsets: - total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) - for i in range(3): - num_data[offset + i] = int(total_light / 3) - for i in range(3): - num_data[offset + i] = int(num_data[offset + i] * (new_color[i] / 255)) + num_data = changeVertexColor(num_data, offset, color_applied) + for offset in color_not_base_offsets: + num_data = changeVertexColor(num_data, offset, new_color) data = bytearray(num_data) # convert num_data back to binary string writeRawFile(TableNames.ModelTwoGeometry, file_index, True, data, ROM_COPY) return # DK Arcade sprites - for file in range(8, 14): - index = file - 8 - if index < 5: - color = getKongItemColor(colorblind_mode, index) - else: - color = "#FFFFFF" - potion_image = getImageFile(6, file, False, 20, 20, TextureFormat.RGBA5551) - potion_image = maskPotionImage(potion_image, color, secondary_color[index]) - writeColorImageToROM(potion_image, 6, file, 20, 20, False, TextureFormat.RGBA5551) + # for file in range(8, 14): + # index = file - 8 + # if index < 5: + # color = getKongItemColor(colorblind_mode, index) + # else: + # color = "#FFFFFF" + # potion_image = getImageFile(6, file, False, 20, 20, TextureFormat.RGBA5551) + # potion_image = maskPotionImage(potion_image, color, secondary_color[index]) + # writeColorImageToROM(potion_image, 6, file, 20, 20, False, TextureFormat.RGBA5551) def maskMushroomImage(im_f, reference_image, color, side_2=False): diff --git a/randomizer/Patching/LibImage.py b/randomizer/Patching/LibImage.py index 7c9b7a020..26d178bfd 100644 --- a/randomizer/Patching/LibImage.py +++ b/randomizer/Patching/LibImage.py @@ -548,3 +548,12 @@ def hueShiftColor(color: tuple, amount: int, head_ratio: int = None) -> tuple: green_ratio = 0 blue_ratio = x return (int((red_ratio + m) * 255), int((green_ratio + m) * 255), int((blue_ratio + m) * 255)) + + +def rgba32to5551(rgba_32: list[int]) -> list[int]: + """Convert list as RGBA32 bytes with no alpha to list of RGBA5551 bytes.""" + val_r = int((rgba_32[0] >> 3) << 11) + val_g = int((rgba_32[1] >> 3) << 6) + val_b = int((rgba_32[2] >> 3) << 1) + rgba_val = val_r | val_g | val_b | 1 + return [(rgba_val >> 8) & 0xFF, rgba_val & 0xFF] From 9d0d3872be41459ab1fb814b0f6cdf3ce3459283 Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Fri, 27 Dec 2024 10:35:03 -0600 Subject: [PATCH 09/13] Docstrings --- randomizer/Patching/CosmeticColors.py | 1 - randomizer/Patching/Cosmetics/Colorblind.py | 3 +-- randomizer/Patching/Cosmetics/KongColor.py | 1 + randomizer/Patching/Cosmetics/__init__.py | 1 + 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/randomizer/Patching/CosmeticColors.py b/randomizer/Patching/CosmeticColors.py index cf50c5e1f..b1a22b98f 100644 --- a/randomizer/Patching/CosmeticColors.py +++ b/randomizer/Patching/CosmeticColors.py @@ -113,7 +113,6 @@ def changePatchFace(settings: Settings): def apply_cosmetic_colors(settings: Settings): """Apply cosmetic skins to kongs.""" - ROM_COPY = ROM() sav = settings.rom_data diff --git a/randomizer/Patching/Cosmetics/Colorblind.py b/randomizer/Patching/Cosmetics/Colorblind.py index cfd3cc2c4..944b47dd8 100644 --- a/randomizer/Patching/Cosmetics/Colorblind.py +++ b/randomizer/Patching/Cosmetics/Colorblind.py @@ -22,7 +22,7 @@ def changeVertexColor(num_data: list[int], offset: int, new_color: list[int]) -> list[int]: - """Changes the vertex color based on the luminance of the original.""" + """Change the vertex color based on the luminance of the original.""" total_light = int(num_data[offset] + num_data[offset + 1] + num_data[offset + 2]) channel_light = int(total_light / 3) for i in range(3): @@ -683,7 +683,6 @@ def getPotionColor(colorblind_mode: ColorblindMode, kong: Kongs) -> list[int]: def recolorPotions(colorblind_mode: ColorblindMode, ROM_COPY: ROM): """Overwrite potion colors.""" - # Actor: file = [[0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2], [0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA]] for type in range(2): diff --git a/randomizer/Patching/Cosmetics/KongColor.py b/randomizer/Patching/Cosmetics/KongColor.py index aaff732d0..2382ea6a2 100644 --- a/randomizer/Patching/Cosmetics/KongColor.py +++ b/randomizer/Patching/Cosmetics/KongColor.py @@ -62,6 +62,7 @@ def __init__(self, kong: str, kong_index: int, palettes: list[KongPalette]): def writeKongColors(settings: Settings): + """Write kong colors based on the settings.""" color_palettes = [] color_obj = {} colors_dict = {} diff --git a/randomizer/Patching/Cosmetics/__init__.py b/randomizer/Patching/Cosmetics/__init__.py index e69de29bb..12a12ea1b 100644 --- a/randomizer/Patching/Cosmetics/__init__.py +++ b/randomizer/Patching/Cosmetics/__init__.py @@ -0,0 +1 @@ +"""Functions to write cosmetic data to ROM.""" \ No newline at end of file From cd8274b0b673561327ff242f74cd62e2f17beda2 Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Fri, 27 Dec 2024 10:36:50 -0600 Subject: [PATCH 10/13] Update __init__.py --- randomizer/Patching/Cosmetics/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/randomizer/Patching/Cosmetics/__init__.py b/randomizer/Patching/Cosmetics/__init__.py index 12a12ea1b..4f0ec25fe 100644 --- a/randomizer/Patching/Cosmetics/__init__.py +++ b/randomizer/Patching/Cosmetics/__init__.py @@ -1 +1 @@ -"""Functions to write cosmetic data to ROM.""" \ No newline at end of file +"""Functions to write cosmetic data to ROM.""" From 49cc86e024e468e81707b5b290399ffa6cbbfe2e Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Fri, 27 Dec 2024 16:59:45 -0600 Subject: [PATCH 11/13] KeiperWantThemPortals --- randomizer/Lists/DoorLocations.py | 1 + static/presets/preset_files.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/randomizer/Lists/DoorLocations.py b/randomizer/Lists/DoorLocations.py index 3170b7194..bb3dda436 100644 --- a/randomizer/Lists/DoorLocations.py +++ b/randomizer/Lists/DoorLocations.py @@ -2852,6 +2852,7 @@ def isBarrierRemoved(spoiler, barrier_id: RemovedBarriersSelected): kong_lst=[Kongs.tiny, Kongs.chunky], group=11, moveless=False, + door_type=[DoorType.boss, DoorType.wrinkly], ), # might be accessible by all kongs post-punch? DoorData( name="Winch Room - on the Winch", diff --git a/static/presets/preset_files.json b/static/presets/preset_files.json index 1ccdb9737..09677f010 100644 --- a/static/presets/preset_files.json +++ b/static/presets/preset_files.json @@ -56,6 +56,6 @@ { "name": "KEVIN 4.0", "description": "Requires usage of a special tracker: https://dev.dk64randomizer.com/wiki/Trackers.html#kevintrackers", - "settings_string": "fjNPxAMxDIUx0QSpbHPUlZls5iglwR6H1AOCIhkJUtjXPhEf3cRolIlsQ2FKlZXHaoEJhvQADvwAHwQAAwwAAxQAAm84FBiMhjoStwFILRS22BblBJANMyaqIUYpBSimAyZZXq/BQFuAIMBN4CBwNwAYQCOIECQVyAoUDOYGCwd0A4YEXIIDTJ1S1eADKcQmqGEtCkd1WrJY0ZmK0gRyfotqUvRURYBExFAFYqI6EQyFMdCVLY1z4RlOlbXxnW+dZY5G7JeLvIoHIdjL2nYGeTjDkAZVOCoQAC4UAC4YACYcACYgAB60AB4kABYoAA4sAB4wABY0AA64AC5GnivQJLlS2eyyej4GEcyU+YDOXQcVRWUyotGOKUAKCsGiYoBOKi0SRcQyEnjWlkGI10dgAdALwjDqwjLBAREiQtFBUWJS4YGRomLxwdHicwICEiKCkzCAkKCwy+Bw0qKzEyNTTAAMIAxACdALIA" + "settings_string": "fjNPxAMxDIUx0QSpbHPUlZls5kglwR6H1AOCIhkJUtjXPhEf3cRolIlsQ2FKlZXHaoEJhvQADvwAHwQAAwwAAxQAAm84FBiMhjoStwFILRS22BblBJANMyaqIUYpBSimAyZZXq/BQFuAIMBN4CBwNwAYQCOIECQVyAoUDOYGCwd0A4YEXIIDTJ1S1eADKcQmqGEtCkd1WrJY0ZmK0gRyfotqUvRURYBExFAFYqI6EQyFMdCVLY1z4RlOlbXxnW+dZY5G7JeLvIoHIdjL2nYGeTjDkAZVOCoQAC4UAC4YACYcACYgAB60AB4kABYoAA4sAB4wABY0AA64AC5GnivQJLlS2eyyej4GEcyU+YDOXQcVRWUyotGOKUAKCsGiYoBOKi0SRcQyEnjWlkGI10dgAdALwjDqwjLBAREiQtFBUWJS4YGRomLxwdHicwICEiKCkzCAkKCwy+Bw0qKzEyNTTAAMIAxACdALIA" } ] From a9deedbc366ac0723a86f1b8c1e3824c6261f499 Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Fri, 27 Dec 2024 17:15:42 -0600 Subject: [PATCH 12/13] Reset preset --- static/presets/preset_files.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/presets/preset_files.json b/static/presets/preset_files.json index 09677f010..1ccdb9737 100644 --- a/static/presets/preset_files.json +++ b/static/presets/preset_files.json @@ -56,6 +56,6 @@ { "name": "KEVIN 4.0", "description": "Requires usage of a special tracker: https://dev.dk64randomizer.com/wiki/Trackers.html#kevintrackers", - "settings_string": "fjNPxAMxDIUx0QSpbHPUlZls5kglwR6H1AOCIhkJUtjXPhEf3cRolIlsQ2FKlZXHaoEJhvQADvwAHwQAAwwAAxQAAm84FBiMhjoStwFILRS22BblBJANMyaqIUYpBSimAyZZXq/BQFuAIMBN4CBwNwAYQCOIECQVyAoUDOYGCwd0A4YEXIIDTJ1S1eADKcQmqGEtCkd1WrJY0ZmK0gRyfotqUvRURYBExFAFYqI6EQyFMdCVLY1z4RlOlbXxnW+dZY5G7JeLvIoHIdjL2nYGeTjDkAZVOCoQAC4UAC4YACYcACYgAB60AB4kABYoAA4sAB4wABY0AA64AC5GnivQJLlS2eyyej4GEcyU+YDOXQcVRWUyotGOKUAKCsGiYoBOKi0SRcQyEnjWlkGI10dgAdALwjDqwjLBAREiQtFBUWJS4YGRomLxwdHicwICEiKCkzCAkKCwy+Bw0qKzEyNTTAAMIAxACdALIA" + "settings_string": "fjNPxAMxDIUx0QSpbHPUlZls5iglwR6H1AOCIhkJUtjXPhEf3cRolIlsQ2FKlZXHaoEJhvQADvwAHwQAAwwAAxQAAm84FBiMhjoStwFILRS22BblBJANMyaqIUYpBSimAyZZXq/BQFuAIMBN4CBwNwAYQCOIECQVyAoUDOYGCwd0A4YEXIIDTJ1S1eADKcQmqGEtCkd1WrJY0ZmK0gRyfotqUvRURYBExFAFYqI6EQyFMdCVLY1z4RlOlbXxnW+dZY5G7JeLvIoHIdjL2nYGeTjDkAZVOCoQAC4UAC4YACYcACYgAB60AB4kABYoAA4sAB4wABY0AA64AC5GnivQJLlS2eyyej4GEcyU+YDOXQcVRWUyotGOKUAKCsGiYoBOKi0SRcQyEnjWlkGI10dgAdALwjDqwjLBAREiQtFBUWJS4YGRomLxwdHicwICEiKCkzCAkKCwy+Bw0qKzEyNTTAAMIAxACdALIA" } ] From 8673921514ee560b082514b76bcbd372375e912a Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Fri, 27 Dec 2024 17:24:03 -0600 Subject: [PATCH 13/13] Update DoorLocations.py --- randomizer/Lists/DoorLocations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/randomizer/Lists/DoorLocations.py b/randomizer/Lists/DoorLocations.py index b20ffcc5f..37117ff8b 100644 --- a/randomizer/Lists/DoorLocations.py +++ b/randomizer/Lists/DoorLocations.py @@ -2955,6 +2955,7 @@ def isBarrierRemoved(spoiler, barrier_id: RemovedBarriersSelected): logicregion=Regions.MushroomLankyMushroomsRoom, location=[447, 0, 368, 239.50], group=20, + door_type=[DoorType.dk_portal, DoorType.wrinkly], ), DoorData( name="DK's Barn - Second floor",