Hello fellow Civ 3 modders,
I asked Gemini 3 if it could in some way help with civ 3 unit creation out of curiosity.
It complied info from these forums and spat out a python script that it says should compliment storyboard and palette creation.
Can a python expert and unit maker chime in here and see if this script is actually useful?
import sys
from PIL import Image, ImagePalette
def create_civ3_palette_image(input_image_path, output_image_path):
"""
Converts an image to a valid Civ 3 Indexed PCX/BMP.
- Slots 0-31: Reserved for Team Color (Blue gradient placeholder).
- Slots 32-253: Optimized palette from the input image.
- Slot 254: Green (Shadow).
- Slot 255: Magenta (Transparency).
"""
# 1. Load the user's image (Must be RGB mode)
img = Image.open(input_image_path).convert("RGB")
width, height = img.size
pixels = img.load()
# 2. Define Reserved Colors
TRANS_MAGENTA = (255, 0, 255) # Index 255
SHADOW_GREEN = (0, 255, 0) # Index 254
# Generate Team Color Gradient (Blue) for indices 0-31
# Civ3 uses this blue ramp as a placeholder for the civ-specific colors.
team_colors = []
for i in range(32):
# Create a gradient from Dark Blue to Light Blue
# This is a safe approximation of the standard Civ3 donor palette
val = int((i / 31) * 255)
team_colors.append((0, 0, val))
# 3. Quantize the "Body" of the unit
# We need 222 colors (256 total - 32 team - 1 shadow - 1 trans)
# We exclude Magenta and Green from this quantization to strictly reserve them.
# Trick: Crop out transparent areas if possible, or just quantize the whole thing
# then force-fix the reserved colors later.
quantized = img.quantize(colors=222, method=2) # Method 2 is Fast Octree
body_palette = quantized.getpalette()[:222*3] # Get the flat RGB list
# Convert flat list to tuples
body_colors = []
for i in range(0, len(body_palette), 3):
body_colors.append((body_palette, body_palette[i+1], body_palette[i+2]))
# Fill remaining slots if image had fewer than 222 colors
while len(body_colors) < 222:
body_colors.append((0, 0, 0))
# 4. Construct the Final Master Palette (List of 256 tuples)
# 0-31: Team Colors
# 32-253: Body Colors
# 254: Green
# 255: Magenta
final_palette_rgb = []
final_palette_rgb.extend(team_colors) # 0-31
final_palette_rgb.extend(body_colors) # 32-253
final_palette_rgb.append(SHADOW_GREEN) # 254
final_palette_rgb.append(TRANS_MAGENTA) # 255
# Flatten for PIL
flat_palette = [c for color in final_palette_rgb for c in color]
# 5. Create the New Image
# We cannot just "convert" because we need to force specific indices.
# We create a new Paletted image and paste the data.
# Helper: Find nearest color index in our new specific palette
def get_nearest_index(pixel):
r, g, b = pixel
# STRICT RESERVED CHECKS
if pixel == TRANS_MAGENTA: return 255
if pixel == SHADOW_GREEN: return 254
# For body colors, find closest match in indices 32-253
# (We skip 0-31 because we don't want the unit body to accidentally flash team colors)
best_idx = 32
min_dist = float('inf')
for idx in range(32, 254):
pr, pg, pb = final_palette_rgb[idx]
dist = (r-pr)**2 + (g-pg)**2 + (b-pb)**2
if dist < min_dist:
min_dist = dist
best_idx = idx
return best_idx
# Create blank indexed image
output_img = Image.new("P", (width, height))
output_img.putpalette(flat_palette)
# 6. Remap pixels (This can be slow for massive images, but fine for unit strips)
# For speed, we can use a cache
print("Remapping pixels... this may take a moment.")
new_data = []
cache = {}
for y in range(height):
for x in range(width):
p = pixels[x, y]
if p not in cache:
cache[p] = get_nearest_index(p)
new_data.append(cache[p])
output_img.putdata(new_data)
# 7. Save
output_img.save(output_image_path)
print(f"Success! Saved to {output_image_path}")
print("Remember: Indices 0-31 are BLUE Team Colors.")
print("If you want parts of your unit to change color, paint them BLUE in your editor now.")
# --- USAGE ---
# Run this from terminal or command prompt:
# python civ3_palette_maker.py
if __name__ == "__main__":
# Example: Change these filenames to what you need
create_civ3_palette_image("Storyboard.bmp", "Storyboard_Indexed.pcx")
I asked Gemini 3 if it could in some way help with civ 3 unit creation out of curiosity.
It complied info from these forums and spat out a python script that it says should compliment storyboard and palette creation.
Can a python expert and unit maker chime in here and see if this script is actually useful?
import sys
from PIL import Image, ImagePalette
def create_civ3_palette_image(input_image_path, output_image_path):
"""
Converts an image to a valid Civ 3 Indexed PCX/BMP.
- Slots 0-31: Reserved for Team Color (Blue gradient placeholder).
- Slots 32-253: Optimized palette from the input image.
- Slot 254: Green (Shadow).
- Slot 255: Magenta (Transparency).
"""
# 1. Load the user's image (Must be RGB mode)
img = Image.open(input_image_path).convert("RGB")
width, height = img.size
pixels = img.load()
# 2. Define Reserved Colors
TRANS_MAGENTA = (255, 0, 255) # Index 255
SHADOW_GREEN = (0, 255, 0) # Index 254
# Generate Team Color Gradient (Blue) for indices 0-31
# Civ3 uses this blue ramp as a placeholder for the civ-specific colors.
team_colors = []
for i in range(32):
# Create a gradient from Dark Blue to Light Blue
# This is a safe approximation of the standard Civ3 donor palette
val = int((i / 31) * 255)
team_colors.append((0, 0, val))
# 3. Quantize the "Body" of the unit
# We need 222 colors (256 total - 32 team - 1 shadow - 1 trans)
# We exclude Magenta and Green from this quantization to strictly reserve them.
# Trick: Crop out transparent areas if possible, or just quantize the whole thing
# then force-fix the reserved colors later.
quantized = img.quantize(colors=222, method=2) # Method 2 is Fast Octree
body_palette = quantized.getpalette()[:222*3] # Get the flat RGB list
# Convert flat list to tuples
body_colors = []
for i in range(0, len(body_palette), 3):
body_colors.append((body_palette, body_palette[i+1], body_palette[i+2]))
# Fill remaining slots if image had fewer than 222 colors
while len(body_colors) < 222:
body_colors.append((0, 0, 0))
# 4. Construct the Final Master Palette (List of 256 tuples)
# 0-31: Team Colors
# 32-253: Body Colors
# 254: Green
# 255: Magenta
final_palette_rgb = []
final_palette_rgb.extend(team_colors) # 0-31
final_palette_rgb.extend(body_colors) # 32-253
final_palette_rgb.append(SHADOW_GREEN) # 254
final_palette_rgb.append(TRANS_MAGENTA) # 255
# Flatten for PIL
flat_palette = [c for color in final_palette_rgb for c in color]
# 5. Create the New Image
# We cannot just "convert" because we need to force specific indices.
# We create a new Paletted image and paste the data.
# Helper: Find nearest color index in our new specific palette
def get_nearest_index(pixel):
r, g, b = pixel
# STRICT RESERVED CHECKS
if pixel == TRANS_MAGENTA: return 255
if pixel == SHADOW_GREEN: return 254
# For body colors, find closest match in indices 32-253
# (We skip 0-31 because we don't want the unit body to accidentally flash team colors)
best_idx = 32
min_dist = float('inf')
for idx in range(32, 254):
pr, pg, pb = final_palette_rgb[idx]
dist = (r-pr)**2 + (g-pg)**2 + (b-pb)**2
if dist < min_dist:
min_dist = dist
best_idx = idx
return best_idx
# Create blank indexed image
output_img = Image.new("P", (width, height))
output_img.putpalette(flat_palette)
# 6. Remap pixels (This can be slow for massive images, but fine for unit strips)
# For speed, we can use a cache
print("Remapping pixels... this may take a moment.")
new_data = []
cache = {}
for y in range(height):
for x in range(width):
p = pixels[x, y]
if p not in cache:
cache[p] = get_nearest_index(p)
new_data.append(cache[p])
output_img.putdata(new_data)
# 7. Save
output_img.save(output_image_path)
print(f"Success! Saved to {output_image_path}")
print("Remember: Indices 0-31 are BLUE Team Colors.")
print("If you want parts of your unit to change color, paint them BLUE in your editor now.")
# --- USAGE ---
# Run this from terminal or command prompt:
# python civ3_palette_maker.py
if __name__ == "__main__":
# Example: Change these filenames to what you need
create_civ3_palette_image("Storyboard.bmp", "Storyboard_Indexed.pcx")