@maseya/z3pr
Version:
Randomize palettes in The Legend of Zelda: A Link to the Past.
100 lines (84 loc) • 3.31 kB
JavaScript
import color_f from './color_f';
import { group_values_ordered } from './util';
import { le_dw_value, le_dw_bytes } from './util';
import each from 'lodash/each';
import mapValues from 'lodash/mapValues';
import transform from 'lodash/transform';
import clamp from 'lodash/clamp';
import parseInt from 'lodash/parseInt';
export default function (rom, offsets) {
const methods = {};
let items = transform(offsets,
(items, offset) => {
const [r, g, b] = offset >= 0 ? raw(offset) : oam(-offset);
items[offset] = color_f(r / 0xFF, g / 0xFF, b / 0xFF);
},
{});
const by_color = group_values_ordered(offsets, (a, b) => color_f.equals(items[a], items[b]))
function raw(offset) {
const color = le_dw_value(rom, offset);
const r = (color >>> 0) & 0x1F;
const g = (color >>> 5) & 0x1F;
const b = (color >>> 10) & 0x1F;
return [r << 3, g << 3, b << 3];
}
function oam(offset) {
const r = rom[offset + 0] & 0x1F;
const g = rom[offset + 1] & 0x1F;
const b = rom[offset + 4] & 0x1F;
return [r << 3, g << 3, b << 3];
}
methods.blend_uniformly = blend_uniformly;
function blend_uniformly(blend_fn, blend_iter) {
const blend = blend_iter.next();
if (blend.done)
throw new Error("Reached the end of blend iterator");
items = mapValues(items, color => blend_fn(color, blend.value));
};
methods.blend_per_color = blend_per_color;
function blend_per_color(blend_fn, blend_iter) {
for (const [offset, offsets] of by_color) {
const base = items[offset];
const blend = blend_iter.next();
if (blend.done)
throw new Error("Reached the end of blend iterator");
const color = blend_fn(base, blend.value);
each(offsets, offset => items[offset] = color);
}
}
methods.write_to_rom = write_to_rom;
function write_to_rom(rom) {
each(items, (color, offset) => {
offset = parseInt(offset);
offset >= 0 ? raw(color, offset) : oam(color, -offset)
});
function raw(color, offset) {
const [r, g, b] = snes_5bit_channels(color);
const value = snes_color_value(r, g, b);
le_dw_bytes(value, rom, offset);
}
function oam(color, offset) {
const [r, g, b] = snes_5bit_channels(color);
rom[offset + 0] = 0x20 | r;
rom[offset + 1] = 0x40 | g;
rom[offset + 3] = 0x40 | g;
rom[offset + 4] = 0x80 | b;
}
function snes_5bit_channels(color) {
function convert_channel(x) {
// Convert channel from [0,1] (float point) to [0,255] (integer).
x = (x * 255 + .5) >>> 0;
// Convert to 5-bit snes channel, with +4 for 8 boundary rounding.
return clamp(x + 4, 255) >>> 3;
}
const r = convert_channel(color.r);
const g = convert_channel(color.g);
const b = convert_channel(color.b);
return [r, g, b];
}
function snes_color_value(r, g, b) {
return r | (g << 5) | (b << 10);
}
}
return methods;
};