From be7c890bd800472e7dd0521087481d9c11def157 Mon Sep 17 00:00:00 2001 From: Tom Ballaam Date: Sat, 21 Dec 2024 22:18:45 -0600 Subject: [PATCH] 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