@@ -681,7 +681,7 @@ def validate_victory_screen(system_def=SYSTEM_DEF):
681681 spr = parse_num_pair (victory .get ("p1.spr" , "-1,0" ), int )
682682 required = {
683683 "enabled" : "1" ,
684- "p1.anim" : "-1 " ,
684+ "p1.anim" : "9400 " ,
685685 "p1.num" : "1" ,
686686 "p1.layerno" : "2" ,
687687 "p1.applypal" : "0" ,
@@ -1069,6 +1069,84 @@ def build_vs_screen(dst=KOMODO_VS_SFF):
10691069 print (f"[vs] { display_path (dst )} : packed { len (sprites )} VS screen sprites" )
10701070
10711071
1072+ FIGHT_TEXT_PNG = "fight-text.png"
1073+
1074+
1075+ def fight_text_sprites (filename = FIGHT_TEXT_PNG ):
1076+ """Slice one FIGHT image into the 4 corner-assembling quadrants used by the
1077+ round-start "FIGHT" callout (anims 520-523).
1078+
1079+ The resting pieces are groups 510,0-3 and the fly-in copies are 511,0-3
1080+ (identical pixels). Each quadrant's axis is the shared center point so all
1081+ four reassemble seamlessly when drawn at fight.offset. Quadrant->number
1082+ mapping matches the fly-in directions: 0=top-left, 1=top-right,
1083+ 2=bottom-left, 3=bottom-right.
1084+ """
1085+ path = ART / "ui/hud" / filename
1086+ if not path .exists ():
1087+ return []
1088+ img = Image .open (path ).convert ("RGBA" )
1089+ w , h = img .size
1090+ cx , cy = w // 2 , h // 2
1091+ quads = [
1092+ (0 , (0 , 0 , cx , cy ), (cx , cy )), # top-left
1093+ (1 , (cx , 0 , w , cy ), (0 , cy )), # top-right
1094+ (2 , (0 , cy , cx , h ), (cx , 0 )), # bottom-left
1095+ (3 , (cx , cy , w , h ), (0 , 0 )), # bottom-right
1096+ ]
1097+ sprites = []
1098+ for number , box , (ax , ay ) in quads :
1099+ tile = img .crop (box )
1100+ sprites .append (image_sprite (510 , number , tile , ax , ay )) # resting
1101+ sprites .append (image_sprite (511 , number , tile , ax , ay )) # fly-in
1102+ return sprites
1103+
1104+
1105+ KO_TEXT_PNG = "ko.png"
1106+ # fight.def [Round] draws the KO halves at these fixed bg offsets; we center the
1107+ # reassembled image on screen-x KO_CENTER_X.
1108+ KO_LEFT_OFFSET_X = 430
1109+ KO_RIGHT_OFFSET_X = 830
1110+ KO_CENTER_X = 640
1111+
1112+
1113+ def ko_text_sprites (filename = KO_TEXT_PNG ):
1114+ """Slice one K.O. image into the left/right halves used by the round-end KO
1115+ callout (anims 530/531).
1116+
1117+ The callout layers two sprite groups: 520 (solid letters) and 522 (the
1118+ additive glow flash, drawn scaled 1.25). Each is split at center into ,0
1119+ (left, shown at bg offset 430) and ,1 (right, shown at 830). The axes are
1120+ chosen so the two halves reassemble into the original image centered on
1121+ screen at KO_CENTER_X, so no fight.def/animation changes are needed.
1122+ """
1123+ path = ART / "ui/hud" / filename
1124+ if not path .exists ():
1125+ return []
1126+ img = Image .open (path ).convert ("RGBA" )
1127+ w , h = img .size
1128+ cx = w // 2
1129+ ay = h // 2
1130+ left = img .crop ((0 , 0 , cx , h ))
1131+ right = img .crop ((cx , 0 , w , h ))
1132+ # left half: screen-left edge = KO_CENTER_X - w/2, drawn at KO_LEFT_OFFSET_X
1133+ left_ax = KO_LEFT_OFFSET_X - KO_CENTER_X + cx
1134+ # right half: screen-left edge = KO_CENTER_X, drawn at KO_RIGHT_OFFSET_X
1135+ right_ax = KO_RIGHT_OFFSET_X - KO_CENTER_X
1136+ sprites = []
1137+ for group in (520 , 522 ): # solid letters + additive glow share the art
1138+ sprites .append (image_sprite (group , 0 , left , left_ax , ay ))
1139+ sprites .append (image_sprite (group , 1 , right , right_ax , ay ))
1140+ # Group 521 is the original black drop shadow (anims 532/533). The custom
1141+ # art carries its own shadow, so blank 521 with transparent tiles to drop
1142+ # the stale shadow behind the new letters.
1143+ blank_left = Image .new ("RGBA" , left .size , (0 , 0 , 0 , 0 ))
1144+ blank_right = Image .new ("RGBA" , right .size , (0 , 0 , 0 , 0 ))
1145+ sprites .append (image_sprite (521 , 0 , blank_left , left_ax , ay ))
1146+ sprites .append (image_sprite (521 , 1 , blank_right , right_ax , ay ))
1147+ return sprites
1148+
1149+
10721150def build_hud (dst = FIGHT_SFF ):
10731151 """Pack custom fight HUD art into fight.sff."""
10741152 sprites , palettes = read_sff_v2 (dst )
@@ -1082,12 +1160,27 @@ def build_hud(dst=FIGHT_SFF):
10821160 (13 , 3 , "health-fill-flash.png" , (435 , 24 ), 435 , 0 ),
10831161 (530 , 1 , "result-lizard-wins.png" , (707 , 102 ), 353 , 51 ),
10841162 (530 , 2 , "result-bug-wins.png" , (646 , 102 ), 323 , 51 ),
1163+ (40 , 0 , "powerbar-empty.png" , (206 , 18 ), 206 , 0 ),
1164+ (41 , 0 , "powerbar-frame.png" , (210 , 22 ), 210 , 0 ),
1165+ (43 , 0 , "powerbar-fill.png" , (202 , 14 ), 202 , 0 ),
10851166 ]
10861167 for group , number , filename , size , ax , ay in replacements :
10871168 img = load_hud_png (filename , size )
10881169 replace_sprite (sprites , image_sprite (group , number , img , ax , ay ))
1170+ fight_text = fight_text_sprites ()
1171+ for sprite in fight_text :
1172+ replace_sprite (sprites , sprite )
1173+ ko_text = ko_text_sprites ()
1174+ for sprite in ko_text :
1175+ replace_sprite (sprites , sprite )
10891176 write_sff_v2 (dst , sprites , palettes )
1090- print (f"[hud] { display_path (dst )} : packed { len (replacements )} HUD sprites" )
1177+ extras = []
1178+ if fight_text :
1179+ extras .append (f"FIGHT text ({ len (fight_text )} tiles)" )
1180+ if ko_text :
1181+ extras .append (f"KO text ({ len (ko_text )} tiles)" )
1182+ note = (" + " + " + " .join (extras )) if extras else ""
1183+ print (f"[hud] { display_path (dst )} : packed { len (replacements )} HUD sprites{ note } " )
10911184
10921185
10931186def build_winner_screen (dst = KOMODO_WINNER_SFF ):
@@ -1184,10 +1277,19 @@ def build_character(variant=DEFAULT_VARIANT, dst=None):
11841277 )
11851278
11861279
1187- def build_stage (scale = 1.2 ):
1188- """Build the Greptile city stage SFF from Screen.png."""
1280+ def build_stage (source = "Screen-2x.jpg" , overfill = 1.2 ):
1281+ """Build the Greptile city stage SFF from the city background image.
1282+
1283+ Ensures the result overfills the 1920x1080 stage space by at least
1284+ `overfill` (min width 2304) so camera panning never exposes black edges and
1285+ the .def positioning stays valid. A source already larger than the overfill
1286+ minimum (e.g. a 2x 2560x1440 image) is kept at full resolution; only smaller
1287+ sources are upscaled.
1288+ """
11891289 dst = ROOT / "extracted/stages/greptile_city.sff"
1190- img = Image .open (ART / "Screen.png" ).convert ("RGBA" )
1290+ img = Image .open (ART / source ).convert ("RGBA" )
1291+ min_w = round (1920 * overfill )
1292+ scale = max (1.0 , min_w / img .width )
11911293 if scale != 1 :
11921294 img = img .resize ((round (img .width * scale ), round (img .height * scale )), Image .NEAREST )
11931295 buf = io .BytesIO ()
0 commit comments