1414 NETTORI_EXTRA_PALS ,
1515 TILESET_ANIM_PALS ,
1616)
17- from mars_patcher .palette import Palette
17+ from mars_patcher .palette import ColorChange , Palette , SineWave
1818from mars_patcher .rom import Game , Rom
1919
2020
@@ -39,11 +39,13 @@ def __init__(
3939 pal_types : dict [PaletteType , tuple [int , int ]], # TODO: change this tuple(int, int)
4040 color_space : MarsschemaPalettesColorspace ,
4141 symmetric : bool ,
42+ extra_variation : bool ,
4243 ):
4344 self .seed = seed
4445 self .pal_types = pal_types
4546 self .color_space : MarsschemaPalettesColorspace = color_space
4647 self .symmetric = symmetric
48+ self .extra_variation = extra_variation
4749
4850 @classmethod
4951 def from_json (cls , data : MarsschemaPalettes ) -> "PaletteSettings" :
@@ -56,7 +58,8 @@ def from_json(cls, data: MarsschemaPalettes) -> "PaletteSettings":
5658 pal_types [pal_type ] = hue_range
5759 color_space = data .get ("ColorSpace" , "Oklab" )
5860 symmetric = data .get ("Symmetric" , True )
59- return cls (seed , pal_types , color_space , symmetric )
61+ # Extra variation is always enabled. This could be passed via JSON instead.
62+ return cls (seed , pal_types , color_space , symmetric , True )
6063
6164 @classmethod
6265 def get_hue_range (cls , data : MarsschemaPalettesRandomize ) -> tuple [int , int ]:
@@ -82,26 +85,37 @@ def __init__(self, rom: Rom, settings: PaletteSettings):
8285 self .rom = rom
8386 self .settings = settings
8487 if settings .color_space == "HSV" :
85- self .shift_func = self .shift_palette_hsv
88+ self .change_func = self .change_palette_hsv
8689 elif settings .color_space == "Oklab" :
87- self .shift_func = self .shift_palette_oklab
90+ self .change_func = self .change_palette_oklab
8891 else :
8992 raise ValueError (f"Invalid color space '{ settings .color_space } ' for color space!" )
9093
9194 @staticmethod
92- def shift_palette_hsv (pal : Palette , shift : int , excluded_rows : set [int ] = set ()) -> None :
93- pal .shift_hue_hsv (shift , excluded_rows )
95+ def change_palette_hsv (
96+ pal : Palette , change : ColorChange , excluded_rows : set [int ] = set ()
97+ ) -> None :
98+ pal .change_colors_hsv (change , excluded_rows )
9499
95100 @staticmethod
96- def shift_palette_oklab (pal : Palette , shift : int , excluded_rows : set [int ] = set ()) -> None :
97- pal .shift_hue_oklab (shift , excluded_rows )
98-
99- def get_hue_shift (self , hue_range : tuple [int , int ]) -> int :
100- """Returns a hue shift in a random direction between hue_min and hue_max."""
101- shift = random .randint (hue_range [0 ], hue_range [1 ])
102- if self .settings .symmetric and random .random () < 0.5 :
103- shift = 360 - shift
104- return shift
101+ def change_palette_oklab (
102+ pal : Palette , change : ColorChange , excluded_rows : set [int ] = set ()
103+ ) -> None :
104+ pal .change_colors_oklab (change , excluded_rows )
105+
106+ def generate_palette_change (self , hue_range : tuple [int , int ]) -> ColorChange :
107+ """Generates a random color change. hue_range determines how far each color's hue will be
108+ initially rotated. Individual colors can be additionally rotated using the values of a
109+ random sine wave."""
110+ hue_shift = random .randint (hue_range [0 ], hue_range [1 ])
111+ if self .settings .symmetric and random .choice ([True , False ]):
112+ hue_shift = 360 - hue_shift
113+ if self .settings .extra_variation :
114+ hue_var_range = min (1.0 , (hue_range [1 ] - hue_range [0 ]) / 180 )
115+ hue_var = SineWave .generate (hue_var_range )
116+ else :
117+ hue_var = None
118+ return ColorChange (hue_shift , hue_var )
105119
106120 def randomize (self ) -> None :
107121 random .seed (self .settings .seed )
@@ -120,24 +134,24 @@ def randomize(self) -> None:
120134 if self .rom .is_zm ():
121135 self .fix_zm_palettes ()
122136
123- def shift_palettes (self , pals : list [tuple [int , int ]], shift : int ) -> None :
137+ def change_palettes (self , pals : list [tuple [int , int ]], change : ColorChange ) -> None :
124138 for addr , rows in pals :
125139 if addr in self .randomized_pals :
126140 continue
127141 pal = Palette (rows , self .rom , addr )
128- self .shift_func (pal , shift )
142+ self .change_func (pal , change )
129143 pal .write (self .rom , addr )
130144 self .randomized_pals .add (addr )
131145
132146 def randomize_samus (self , hue_range : tuple [int , int ]) -> None :
133- shift = self .get_hue_shift (hue_range )
134- self .shift_palettes (gd .samus_palettes (self .rom ), shift )
135- self .shift_palettes (gd .helmet_cursor_palettes (self .rom ), shift )
136- self .shift_palettes (gd .sax_palettes (self .rom ), shift )
147+ change = self .generate_palette_change (hue_range )
148+ self .change_palettes (gd .samus_palettes (self .rom ), change )
149+ self .change_palettes (gd .helmet_cursor_palettes (self .rom ), change )
150+ self .change_palettes (gd .sax_palettes (self .rom ), change )
137151
138152 def randomize_beams (self , hue_range : tuple [int , int ]) -> None :
139- shift = self .get_hue_shift (hue_range )
140- self .shift_palettes (gd .beam_palettes (self .rom ), shift )
153+ change = self .generate_palette_change (hue_range )
154+ self .change_palettes (gd .beam_palettes (self .rom ), change )
141155
142156 def randomize_tilesets (self , hue_range : tuple [int , int ]) -> None :
143157 rom = self .rom
@@ -161,30 +175,30 @@ def randomize_tilesets(self, hue_range: tuple[int, int]) -> None:
161175 excluded_rows = {row }
162176 # Load palette and shift hue
163177 pal = Palette (13 , rom , pal_addr )
164- shift = self .get_hue_shift (hue_range )
165- self .shift_func (pal , shift , excluded_rows )
178+ change = self .generate_palette_change (hue_range )
179+ self .change_func (pal , change , excluded_rows )
166180 pal .write (rom , pal_addr )
167181 self .randomized_pals .add (pal_addr )
168182 # Check animated palette
169183 anim_pal_id = TILESET_ANIM_PALS .get (pal_addr )
170184 if anim_pal_id is not None :
171- self .randomize_anim_palette (anim_pal_id , shift )
185+ self .randomize_anim_palette (anim_pal_id , change )
172186 anim_pal_to_randomize .remove (anim_pal_id )
173187
174188 # Go through remaining animated palettes
175189 for anim_pal_id in anim_pal_to_randomize :
176- shift = self .get_hue_shift (hue_range )
177- self .randomize_anim_palette (anim_pal_id , shift )
190+ change = self .generate_palette_change (hue_range )
191+ self .randomize_anim_palette (anim_pal_id , change )
178192
179- def randomize_anim_palette (self , anim_pal_id : int , shift : int ) -> None :
193+ def randomize_anim_palette (self , anim_pal_id : int , change : ColorChange ) -> None :
180194 rom = self .rom
181195 addr = gd .anim_palette_entries (rom ) + anim_pal_id * 8
182196 pal_addr = rom .read_ptr (addr + 4 )
183197 if pal_addr in self .randomized_pals :
184198 return
185199 rows = rom .read_8 (addr + 2 )
186200 pal = Palette (rows , rom , pal_addr )
187- self .shift_func (pal , shift )
201+ self .change_func (pal , change )
188202 pal .write (rom , pal_addr )
189203 self .randomized_pals .add (pal_addr )
190204
@@ -198,18 +212,19 @@ def randomize_enemies(self, hue_range: tuple[int, int]) -> None:
198212 # Go through sprites in groups
199213 groups = ENEMY_GROUPS [rom .game ]
200214 for _ , sprite_ids in groups .items ():
201- shift = self .get_hue_shift (hue_range )
215+ change = self .generate_palette_change (hue_range )
202216 for sprite_id in sprite_ids :
203217 assert sprite_id in to_randomize , f"{ sprite_id :X} should be excluded"
204- self .randomize_enemy (sprite_id , shift )
218+ self .randomize_enemy (sprite_id , change )
205219 to_randomize .remove (sprite_id )
206220
207221 # Go through remaining sprites
208222 for sprite_id in to_randomize :
209- shift = self .get_hue_shift (hue_range )
210- self .randomize_enemy (sprite_id , shift )
223+ change = self .generate_palette_change (hue_range )
224+ self .randomize_enemy (sprite_id , change )
211225
212- def randomize_enemy (self , sprite_id : int , shift : int ) -> None :
226+ def randomize_enemy (self , sprite_id : int , change : ColorChange ) -> None :
227+ # Get palette address and row count
213228 rom = self .rom
214229 sprite_gfx_id = sprite_id - 0x10
215230 pal_ptr = gd .sprite_palette_ptrs (rom )
@@ -230,12 +245,13 @@ def randomize_enemy(self, sprite_id: int, shift: int) -> None:
230245 rows = (rom .read_32 (gfx_addr ) >> 8 ) // 0x800
231246 else :
232247 raise ValueError ("Unknown game!" )
248+ # Load palette, change colors, and write to ROM
233249 pal = Palette (rows , rom , pal_addr )
234- self .shift_func (pal , shift )
250+ self .change_func (pal , change )
235251 pal .write (rom , pal_addr )
236252 self .randomized_pals .add (pal_addr )
237253 if rom .is_mf () and sprite_id == 0x26 :
238- self .fix_nettori (shift )
254+ self .fix_nettori (change )
239255
240256 def get_sprite_addr (self , sprite_id : int ) -> int :
241257 addr = gd .sprite_palette_ptrs (self .rom ) + (sprite_id - 0x10 ) * 4
@@ -245,11 +261,11 @@ def get_tileset_addr(self, sprite_id: int) -> int:
245261 addr = gd .tileset_entries (self .rom ) + sprite_id * 0x14 + 4
246262 return self .rom .read_ptr (addr )
247263
248- def fix_nettori (self , shift : int ) -> None :
264+ def fix_nettori (self , change : ColorChange ) -> None :
249265 """Nettori has extra palettes stored separately, so they require the same color change."""
250266 for addr , rows in NETTORI_EXTRA_PALS :
251267 pal = Palette (rows , self .rom , addr )
252- self .shift_func (pal , shift )
268+ self .change_func (pal , change )
253269 pal .write (self .rom , addr )
254270
255271 def fix_zm_palettes (self ) -> None :
0 commit comments