Add palette swaps for fusions
This commit is contained in:
parent
06943ac5dc
commit
4d02432606
|
@ -8,6 +8,7 @@
|
|||
"name": "pokemon-rogue-battle",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"phaser": "^3.70.0",
|
||||
"phaser3-rex-plugins": "^1.1.84"
|
||||
},
|
||||
|
@ -492,6 +493,11 @@
|
|||
"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": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"vite-plugin-fs": "^1.0.0-beta.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"phaser": "^3.70.0",
|
||||
"phaser3-rex-plugins": "^1.1.84"
|
||||
}
|
||||
|
|
|
@ -681,6 +681,7 @@ export abstract class BattleAnim {
|
|||
let sprite: Phaser.GameObjects.Sprite;
|
||||
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 });
|
||||
[ 'spriteColors', 'fusionSpriteColors' ].map(k => sprite.pipelineData[k] = (isUser ? user : target).getSprite().pipelineData[k]);
|
||||
spriteSource.on('animationupdate', (_anim, frame) => sprite.setFrame(frame.textureFrame));
|
||||
scene.field.add(sprite);
|
||||
sprites.push(sprite);
|
||||
|
|
|
@ -23,6 +23,8 @@ uniform vec2 relPosition;
|
|||
uniform vec2 size;
|
||||
uniform float yOffset;
|
||||
uniform vec4 tone;
|
||||
uniform ivec4 spriteColors[32];
|
||||
uniform ivec4 fusionSpriteColors[32];
|
||||
|
||||
const vec3 lumaF = vec3(.299, .587, .114);
|
||||
|
||||
|
@ -37,13 +39,24 @@ void main()
|
|||
// Multiply texture tint
|
||||
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
|
||||
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
|
||||
color = texel;
|
||||
}
|
||||
|
@ -138,7 +151,7 @@ export default class SpritePipeline extends Phaser.Renderer.WebGL.Pipelines.Mult
|
|||
this.set2f('relPosition', 0, 0);
|
||||
this.set2f('size', 0, 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 {
|
||||
|
@ -149,6 +162,8 @@ export default class SpritePipeline extends Phaser.Renderer.WebGL.Pipelines.Mult
|
|||
const data = sprite.pipelineData;
|
||||
const tone = data['tone'] as number[];
|
||||
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
|
||||
? [ 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('size', sprite.frame.width, sprite.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 {
|
||||
|
|
182
src/pokemon.ts
182
src/pokemon.ts
|
@ -32,6 +32,7 @@ import { GameMode } from './game-mode';
|
|||
import { LevelMoves } from './data/pokemon-level-moves';
|
||||
import { DamageAchv, achvs } from './system/achv';
|
||||
import { DexAttr } from './system/game-data';
|
||||
import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from '@material/material-color-utilities';
|
||||
|
||||
export enum FieldPosition {
|
||||
CENTER,
|
||||
|
@ -247,7 +248,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
loadMoveAnimAssets(this.scene, moveIds);
|
||||
this.getSpeciesForm().loadAssets(this.scene, this.getGender() === Gender.FEMALE, this.formIndex, this.shiny);
|
||||
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())
|
||||
this.scene.loadAtlas(this.getBattleSpriteKey(), 'pokemon', this.getBattleSpriteAtlasPath());
|
||||
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => {
|
||||
|
@ -267,6 +268,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
}
|
||||
}
|
||||
this.playAnim();
|
||||
if (this.fusionSpecies)
|
||||
this.updateFusionPalette();
|
||||
resolve();
|
||||
});
|
||||
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 {
|
||||
this.battleInfo.destroy();
|
||||
super.destroy();
|
||||
|
@ -1494,6 +1672,7 @@ export class PlayerPokemon extends Pokemon {
|
|||
this.scene.removePartyMemberModifiers(fusedPartyMemberIndex);
|
||||
this.scene.getParty().splice(fusedPartyMemberIndex, 1)[0];
|
||||
pokemon.destroy();
|
||||
this.updateFusionPalette();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
@ -1511,6 +1690,7 @@ export class PlayerPokemon extends Pokemon {
|
|||
this.calculateStats();
|
||||
this.generateCompatibleTms();
|
||||
this.updateInfo(true).then(() => resolve());
|
||||
this.updateFusionPalette();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,6 +98,7 @@ export default class SummaryUiHandler extends UiHandler {
|
|||
this.summaryContainer.add(this.numberText);
|
||||
|
||||
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.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.pokemonSprite.play(this.pokemon.getSpriteKey(true));
|
||||
[ 'spriteColors', 'fusionSpriteColors' ].map(k => this.pokemonSprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k]);
|
||||
this.pokemon.cry();
|
||||
|
||||
let nameLabel = this.pokemon.name;
|
||||
|
|
23
src/utils.ts
23
src/utils.ts
|
@ -148,4 +148,27 @@ export class FixedInt extends IntegerHolder {
|
|||
|
||||
export function fixedInt(value: integer): 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));
|
||||
}
|
Loading…
Reference in New Issue