Add palette swaps for fusions

This commit is contained in:
Flashfyre 2023-11-23 23:52:13 -05:00
parent 06943ac5dc
commit 4d02432606
7 changed files with 246 additions and 8 deletions

6
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "pokemon-rogue-battle", "name": "pokemon-rogue-battle",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@material/material-color-utilities": "^0.2.7",
"phaser": "^3.70.0", "phaser": "^3.70.0",
"phaser3-rex-plugins": "^1.1.84" "phaser3-rex-plugins": "^1.1.84"
}, },
@ -492,6 +493,11 @@
"node": ">= 8.0.0" "node": ">= 8.0.0"
} }
}, },
"node_modules/@material/material-color-utilities": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@material/material-color-utilities/-/material-color-utilities-0.2.7.tgz",
"integrity": "sha512-0FCeqG6WvK4/Cc06F/xXMd/pv4FeisI0c1tUpBbfhA2n9Y8eZEv4Karjbmf2ZqQCPUWMrGp8A571tCjizxoTiQ=="
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",

View File

@ -16,6 +16,7 @@
"vite-plugin-fs": "^1.0.0-beta.6" "vite-plugin-fs": "^1.0.0-beta.6"
}, },
"dependencies": { "dependencies": {
"@material/material-color-utilities": "^0.2.7",
"phaser": "^3.70.0", "phaser": "^3.70.0",
"phaser3-rex-plugins": "^1.1.84" "phaser3-rex-plugins": "^1.1.84"
} }

View File

@ -681,6 +681,7 @@ export abstract class BattleAnim {
let sprite: Phaser.GameObjects.Sprite; let sprite: Phaser.GameObjects.Sprite;
sprite = scene.add.sprite(0, 0, spriteSource.texture, spriteSource.frame.name); sprite = scene.add.sprite(0, 0, spriteSource.texture, spriteSource.frame.name);
sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: true }); sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: true });
[ 'spriteColors', 'fusionSpriteColors' ].map(k => sprite.pipelineData[k] = (isUser ? user : target).getSprite().pipelineData[k]);
spriteSource.on('animationupdate', (_anim, frame) => sprite.setFrame(frame.textureFrame)); spriteSource.on('animationupdate', (_anim, frame) => sprite.setFrame(frame.textureFrame));
scene.field.add(sprite); scene.field.add(sprite);
sprites.push(sprite); sprites.push(sprite);

View File

@ -23,6 +23,8 @@ uniform vec2 relPosition;
uniform vec2 size; uniform vec2 size;
uniform float yOffset; uniform float yOffset;
uniform vec4 tone; uniform vec4 tone;
uniform ivec4 spriteColors[32];
uniform ivec4 fusionSpriteColors[32];
const vec3 lumaF = vec3(.299, .587, .114); const vec3 lumaF = vec3(.299, .587, .114);
@ -37,13 +39,24 @@ void main()
// Multiply texture tint // Multiply texture tint
vec4 color = texture * texel; vec4 color = texture * texel;
if (outTintEffect == 1.0) for (int i = 0; i < 32; i++) {
{ if (spriteColors[i][3] == 0)
break;
if (texture.a > 0.0 && int(texture.r * 255.0) == spriteColors[i].r && int(texture.g * 255.0) == spriteColors[i].g && int(texture.b * 255.0) == spriteColors[i].b) {
vec3 fusionColor = vec3(float(fusionSpriteColors[i].r) / 255.0, float(fusionSpriteColors[i].g) / 255.0, float(fusionSpriteColors[i].b) / 255.0);
vec3 bg = vec3(float(spriteColors[i].r) / 255.0, float(spriteColors[i].g) / 255.0, float(spriteColors[i].b) / 255.0);
float gray = (bg.r + bg.g + bg.b) / 3.0;
bg = vec3(gray, gray, gray);
vec3 fg = fusionColor;
color.rgb = mix(1.0 - 2.0 * (1.0 - bg) * (1.0 - fg), 2.0 * bg * fg, step(bg, vec3(0.5)));
break;
}
}
if (outTintEffect == 1.0) {
// Solid color + texture alpha // Solid color + texture alpha
color.rgb = mix(texture.rgb, outTint.bgr * outTint.a, texture.a); color.rgb = mix(texture.rgb, outTint.bgr * outTint.a, texture.a);
} } else if (outTintEffect == 2.0) {
else if (outTintEffect == 2.0)
{
// Solid color, no texture // Solid color, no texture
color = texel; color = texel;
} }
@ -138,7 +151,7 @@ export default class SpritePipeline extends Phaser.Renderer.WebGL.Pipelines.Mult
this.set2f('relPosition', 0, 0); this.set2f('relPosition', 0, 0);
this.set2f('size', 0, 0); this.set2f('size', 0, 0);
this.set1f('yOffset', 0); this.set1f('yOffset', 0);
this.set4f('tone', this._tone[0], this._tone[1], this._tone[2], this._tone[3]); this.set4fv('tone', this._tone);
} }
onBind(gameObject: Phaser.GameObjects.GameObject): void { onBind(gameObject: Phaser.GameObjects.GameObject): void {
@ -149,6 +162,8 @@ export default class SpritePipeline extends Phaser.Renderer.WebGL.Pipelines.Mult
const data = sprite.pipelineData; const data = sprite.pipelineData;
const tone = data['tone'] as number[]; const tone = data['tone'] as number[];
const hasShadow = data['hasShadow'] as boolean; const hasShadow = data['hasShadow'] as boolean;
let spriteColors = data['spriteColors'] || [] as number[][];
const fusionSpriteColors = data['fusionSpriteColors'] || [] as number[][];
const position = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer const position = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer
? [ sprite.parentContainer.x, sprite.parentContainer.y ] ? [ sprite.parentContainer.x, sprite.parentContainer.y ]
@ -159,7 +174,17 @@ export default class SpritePipeline extends Phaser.Renderer.WebGL.Pipelines.Mult
this.set2f('relPosition', position[0], position[1]); this.set2f('relPosition', position[0], position[1]);
this.set2f('size', sprite.frame.width, sprite.height); this.set2f('size', sprite.frame.width, sprite.height);
this.set1f('yOffset', sprite.height - sprite.frame.height); this.set1f('yOffset', sprite.height - sprite.frame.height);
this.set4f('tone', tone[0], tone[1], tone[2], tone[3]); this.set4fv('tone', tone);
const emptyColors = [ 0, 0, 0, 0 ];
const flatSpriteColors: integer[] = [];
const flatFusionSpriteColors: integer[] = [];
for (let c = 0; c < 32; c++) {
flatSpriteColors.splice(flatSpriteColors.length, 0, c < spriteColors.length ? spriteColors[c] : emptyColors);
flatFusionSpriteColors.splice(flatFusionSpriteColors.length, 0, c < fusionSpriteColors.length ? fusionSpriteColors[c] : emptyColors);
}
this.set4iv(`spriteColors`, flatSpriteColors.flat());
this.set4iv(`fusionSpriteColors`, flatFusionSpriteColors.flat());
} }
onBatch(gameObject: Phaser.GameObjects.GameObject): void { onBatch(gameObject: Phaser.GameObjects.GameObject): void {

View File

@ -32,6 +32,7 @@ import { GameMode } from './game-mode';
import { LevelMoves } from './data/pokemon-level-moves'; import { LevelMoves } from './data/pokemon-level-moves';
import { DamageAchv, achvs } from './system/achv'; import { DamageAchv, achvs } from './system/achv';
import { DexAttr } from './system/game-data'; import { DexAttr } from './system/game-data';
import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from '@material/material-color-utilities';
export enum FieldPosition { export enum FieldPosition {
CENTER, CENTER,
@ -247,7 +248,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
loadMoveAnimAssets(this.scene, moveIds); loadMoveAnimAssets(this.scene, moveIds);
this.getSpeciesForm().loadAssets(this.scene, this.getGender() === Gender.FEMALE, this.formIndex, this.shiny); this.getSpeciesForm().loadAssets(this.scene, this.getGender() === Gender.FEMALE, this.formIndex, this.shiny);
if (this.fusionSpecies) if (this.fusionSpecies)
this.getFusionSpeciesForm().loadAssets(this.scene, this.getGender() === Gender.FEMALE, this.fusionFormIndex, this.shiny); this.getFusionSpeciesForm().loadAssets(this.scene, this.fusionGender === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny);
if (this.isPlayer()) if (this.isPlayer())
this.scene.loadAtlas(this.getBattleSpriteKey(), 'pokemon', this.getBattleSpriteAtlasPath()); this.scene.loadAtlas(this.getBattleSpriteKey(), 'pokemon', this.getBattleSpriteAtlasPath());
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => { this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => {
@ -267,6 +268,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
this.playAnim(); this.playAnim();
if (this.fusionSpecies)
this.updateFusionPalette();
resolve(); resolve();
}); });
if (!this.scene.load.isLoading()) if (!this.scene.load.isLoading())
@ -1350,6 +1353,181 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
updateFusionPalette(): void {
if (!this.fusionSpecies) {
[ this.getSprite(), this.getTintSprite() ].map(s => {
s.pipelineData['spriteColors'] = [];
s.pipelineData['fusionSpriteColors'] = [];
});
return;
}
const sourceTexture = this.scene.textures.get(this.getSpeciesForm().getSpriteKey(this.gender === Gender.FEMALE, this.formIndex, this.shiny));
const fusionTexture = this.scene.textures.get(this.getFusionSpeciesForm().getSpriteKey(this.fusionGender === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny));
const [ sourceFrame, fusionFrame ] = [ sourceTexture, fusionTexture ].map(texture => texture.frames[texture.firstFrame]);
const [ sourceImage, fusionImage ] = [ sourceTexture, fusionTexture ].map(i => i.getSourceImage() as HTMLImageElement);
const canvas = document.createElement('canvas');
const fusionCanvas = document.createElement('canvas');
const spriteColors: integer[][] = [];
const pixelData: Uint8ClampedArray[] = [];
[ canvas, fusionCanvas ].forEach((canv: HTMLCanvasElement, c: integer) => {
const context = canv.getContext('2d');
const frame = !c ? sourceFrame : fusionFrame;
canv.width = frame.width;
canv.height = frame.height;
context.drawImage(!c ? sourceImage : fusionImage, frame.cutX, frame.cutY, frame.width, frame.height, 0, 0, frame.width, frame.height);
const imageData = context.getImageData(frame.cutX, frame.cutY, frame.width, frame.height);
pixelData.push(imageData.data);
});
for (let i = 0; i < pixelData[0].length; i += 4) {
if (pixelData[0][i + 3]) {
const pixel = pixelData[0].slice(i, i + 4);
const [ r, g, b, a ] = pixel;
if (!spriteColors.find(c => c[0] === r && c[1] === g && c[2] === b))
spriteColors.push([ r, g, b, a ]);
}
}
const fusionSpriteColors = JSON.parse(JSON.stringify(spriteColors));
const pixelColors = [];
for (let i = 0; i < pixelData[0].length; i += 4) {
const total = pixelData[0].slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0);
if (!total)
continue;
pixelColors.push(argbFromRgba({ r: pixelData[0][i], g: pixelData[0][i + 1], b: pixelData[0][i + 2], a: pixelData[0][i + 3] }));
}
const fusionPixelColors = [];
for (let i = 0; i < pixelData[1].length; i += 4) {
const total = pixelData[1].slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0);
if (!total)
continue;
fusionPixelColors.push(argbFromRgba({ r: pixelData[1][i], g: pixelData[1][i + 1], b: pixelData[1][i + 2], a: pixelData[1][i + 3] }));
}
let paletteColors: Map<number, number>;
let fusionPaletteColors: Map<number, number>;
const originalRandom = Math.random;
Math.random = () => Phaser.Math.RND.realInRange(0, 1);
this.scene.executeWithSeedOffset(() => {
paletteColors = QuantizerCelebi.quantize(pixelColors, 4);
fusionPaletteColors = QuantizerCelebi.quantize(fusionPixelColors, 4);
}, 0, 'This result should not vary');
Math.random = originalRandom;
const [ palette, fusionPalette ] = [ paletteColors, fusionPaletteColors ]
.map(paletteColors => {
let keys = Array.from(paletteColors.keys()).sort((a: integer, b: integer) => paletteColors.get(a) < paletteColors.get(b) ? 1 : -1);
let rgbaColors: Map<number, integer[]>;
let hsvColors: Map<number, number[]>;
const mappedColors = new Map<integer, integer[]>();
do {
mappedColors.clear();
rgbaColors = keys.reduce((map: Map<number, integer[]>, k: number) => { map.set(k, Object.values(rgbaFromArgb(k))); return map; }, new Map<number, integer[]>());
hsvColors = Array.from(rgbaColors.keys()).reduce((map: Map<number, number[]>, k: number) => {
const rgb = rgbaColors.get(k).slice(0, 3);
map.set(k, Utils.rgbToHsv(rgb[0], rgb[1], rgb[2]));
return map;
}, new Map<number, number[]>());
for (let c = keys.length - 1; c >= 0; c--) {
const hsv = hsvColors.get(keys[c]);
for (let c2 = 0; c2 < c; c2++) {
const hsv2 = hsvColors.get(keys[c2]);
const diff = Math.abs(hsv[0] - hsv2[0]);
if (diff < 30 || diff >= 330) {
if (mappedColors.has(keys[c]))
mappedColors.get(keys[c]).push(keys[c2]);
else
mappedColors.set(keys[c], [ keys[c2] ]);
break;
}
}
}
mappedColors.forEach((values: integer[], key: integer) => {
const keyColor = rgbaColors.get(key);
const valueColors = values.map(v => rgbaColors.get(v));
let color = keyColor.slice(0);
let count = paletteColors.get(key);
for (let value of values) {
const valueCount = paletteColors.get(value);
if (!valueCount)
continue;
count += valueCount;
}
for (let c = 0; c < 3; c++) {
color[c] *= (paletteColors.get(key) / count);
values.forEach((value: integer, i: integer) => {
if (paletteColors.has(value)) {
const valueCount = paletteColors.get(value);
color[c] += valueColors[i][c] * (valueCount / count);
}
});
color[c] = Math.round(color[c]);
}
paletteColors.delete(key);
for (let value of values) {
paletteColors.delete(value);
if (mappedColors.has(value))
mappedColors.delete(value);
}
paletteColors.set(argbFromRgba({ r: color[0], g: color[1], b: color[2], a: color[3] }), count);
});
keys = Array.from(paletteColors.keys()).sort((a: integer, b: integer) => paletteColors.get(a) < paletteColors.get(b) ? 1 : -1);
} while (mappedColors.size);
return keys.map(c => Object.values(rgbaFromArgb(c)))
}
);
const paletteDeltas: number[][] = [];
spriteColors.forEach((sc: integer[], i: integer) => {
paletteDeltas.push([]);
for (let p = 0; p < palette.length; p++)
paletteDeltas[i].push(Utils.deltaRgb(sc, palette[p]));
});
const easeFunc = Phaser.Tweens.Builders.GetEaseFunction('Cubic.easeIn');
for (let sc = 0; sc < spriteColors.length; sc++) {
const delta = Math.min(...paletteDeltas[sc]);
const paletteIndex = Math.min(paletteDeltas[sc].findIndex(pd => pd === delta), fusionPalette.length - 1);
if (delta < 255) {
const ratio = easeFunc(delta / 255);
let color = [ 0, 0, 0, fusionSpriteColors[sc][3] ];
for (let c = 0; c < 3; c++)
color[c] = Math.round((fusionSpriteColors[sc][c] * ratio) + (fusionPalette[paletteIndex][c] * (1 - ratio)));
fusionSpriteColors[sc] = color;
}
}
[ this.getSprite(), this.getTintSprite() ].map(s => {
s.pipelineData['spriteColors'] = spriteColors;
s.pipelineData['fusionSpriteColors'] = fusionSpriteColors;
});
canvas.remove();
fusionCanvas.remove();
}
destroy(): void { destroy(): void {
this.battleInfo.destroy(); this.battleInfo.destroy();
super.destroy(); super.destroy();
@ -1494,6 +1672,7 @@ export class PlayerPokemon extends Pokemon {
this.scene.removePartyMemberModifiers(fusedPartyMemberIndex); this.scene.removePartyMemberModifiers(fusedPartyMemberIndex);
this.scene.getParty().splice(fusedPartyMemberIndex, 1)[0]; this.scene.getParty().splice(fusedPartyMemberIndex, 1)[0];
pokemon.destroy(); pokemon.destroy();
this.updateFusionPalette();
resolve(); resolve();
}); });
}); });
@ -1511,6 +1690,7 @@ export class PlayerPokemon extends Pokemon {
this.calculateStats(); this.calculateStats();
this.generateCompatibleTms(); this.generateCompatibleTms();
this.updateInfo(true).then(() => resolve()); this.updateInfo(true).then(() => resolve());
this.updateFusionPalette();
}); });
} }
} }

View File

@ -98,6 +98,7 @@ export default class SummaryUiHandler extends UiHandler {
this.summaryContainer.add(this.numberText); this.summaryContainer.add(this.numberText);
this.pokemonSprite = this.scene.add.sprite(56, -106, `pkmn__sub`); this.pokemonSprite = this.scene.add.sprite(56, -106, `pkmn__sub`);
this.pokemonSprite.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false });
this.summaryContainer.add(this.pokemonSprite); this.summaryContainer.add(this.pokemonSprite);
this.nameText = addTextObject(this.scene, 6, -54, '', TextStyle.SUMMARY); this.nameText = addTextObject(this.scene, 6, -54, '', TextStyle.SUMMARY);
@ -197,6 +198,7 @@ export default class SummaryUiHandler extends UiHandler {
this.numberText.setShadowColor(getTextColor(!this.pokemon.isShiny() ? TextStyle.SUMMARY : TextStyle.SUMMARY_GOLD, true)); this.numberText.setShadowColor(getTextColor(!this.pokemon.isShiny() ? TextStyle.SUMMARY : TextStyle.SUMMARY_GOLD, true));
this.pokemonSprite.play(this.pokemon.getSpriteKey(true)); this.pokemonSprite.play(this.pokemon.getSpriteKey(true));
[ 'spriteColors', 'fusionSpriteColors' ].map(k => this.pokemonSprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k]);
this.pokemon.cry(); this.pokemon.cry();
let nameLabel = this.pokemon.name; let nameLabel = this.pokemon.name;

View File

@ -148,4 +148,27 @@ export class FixedInt extends IntegerHolder {
export function fixedInt(value: integer): integer { export function fixedInt(value: integer): integer {
return new FixedInt(value) as unknown as integer; return new FixedInt(value) as unknown as integer;
}
export function rgbToHsv(r: integer, g: integer, b: integer) {
let v = Math.max(r, g, b);
let c = v - Math.min(r, g, b);
let h = c && ((v === r) ? (g - b) / c : ((v === g) ? 2 + (b - r) / c : 4 + (r - g) / c));
return [ 60 * (h < 0 ? h + 6 : h), v && c / v, v];
}
/**
* Compare color difference in RGB
* @param {Array} rgb1 First RGB color in array
* @param {Array} rgb2 Second RGB color in array
*/
export function deltaRgb(rgb1: integer[], rgb2: integer[]): integer {
const [ r1, g1, b1 ] = rgb1;
const [ r2, g2, b2 ] = rgb2;
const drp2 = Math.pow(r1 - r2, 2);
const dgp2 = Math.pow(g1 - g2, 2);
const dbp2 = Math.pow(b1 - b2, 2);
const t = (r1 + r2) / 2;
return Math.ceil(Math.sqrt(2 * drp2 + 4 * dgp2 + 3 * dbp2 + t * (drp2 - dbp2) / 256));
} }