diff --git a/package.json b/package.json index fecfab348..c2c0f016b 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,8 @@ "node": "20.x" }, "scripts": { - "build": "webpack --mode=production", + "generate:avatars": "node scripts/generateAvatar.js", + "build": "npm run generate:avatars && webpack --mode=production", "build:dev": "webpack --mode=development", "start": "cross-env NODE_ENV=production webpack-dev-server", "start:dev": "cross-env NODE_ENV=development webpack-dev-server", diff --git a/scripts/generateAvatar.js b/scripts/generateAvatar.js new file mode 100644 index 000000000..81553646f --- /dev/null +++ b/scripts/generateAvatar.js @@ -0,0 +1,35 @@ +// AUTO-GENERATED FILE — DO NOT EDIT +// Use `node scripts/generateAvatar.js` to regenerate + +const fs = require('fs'); +const path = require('path'); + +const AVATAR_DIR = path.join(__dirname, '../assets/units/avatars'); +const OUTPUT_FILE = path.join(__dirname, '../src/style/avatars.less'); + +// Helper: turn "Dark Angel.jpg" into "avatar-dark-angel" +function toClassName(filename) { + return ( + 'avatar-' + + filename + .replace(/\.[^/.]+$/, '') // remove extension + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') // replace non-alphanumerics with - + .replace(/^-+|-+$/g, '') + ); +} + +function generate() { + const files = fs.readdirSync(AVATAR_DIR).filter((file) => /\.(jpg|jpeg|png)$/i.test(file)); + + const lines = files.map((file) => { + const className = toClassName(file); + const relativePath = `/assets/units/avatars/${file}`; + return `.${className}[set="default"] {\n background-image: url('${relativePath}');\n}`; + }); + + fs.writeFileSync(OUTPUT_FILE, lines.join('\n\n'), 'utf8'); + console.log(`✅ avatars.less generated with ${files.length} avatars.`); +} + +generate(); diff --git a/src/assets.ts b/src/assets.ts index e40e4ff35..092514b6b 100644 --- a/src/assets.ts +++ b/src/assets.ts @@ -10,14 +10,33 @@ import { phaserAutoloadAssetPaths, assetPaths } from '../assets/index'; */ export function use(phaser: Phaser.Game): void { const assets = Object.entries(phaserAutoloadAssetPaths ?? {}); - const loadedKeys = new Set(); for (const [path, url] of assets) { - if (!/\.(png|jpg|jpeg|svg)$/i.test(path)) continue; // Only load images + if (!/\.(png|jpg|jpeg|svg)$/i.test(path)) continue; const key = getBasename(path); + // Special rule for avatars: if in /units/avatars/, use "_cardboard" + if (path.includes('/units/avatars/')) { + const baseKey = key; + const cardKey = baseKey + '_cardboard'; + + // Ensure both base and card keys are loaded + if (!loadedKeys.has(baseKey)) { + phaser.load.image(baseKey, url); // Load Wisp (or other creature) + loadedKeys.add(baseKey); + } + + if (!loadedKeys.has(cardKey)) { + phaser.load.image(cardKey, url); // Load Wisp_cardboard + loadedKeys.add(cardKey); + } + + continue; // Skip to next asset, since avatar assets are handled above + } + + // Regular asset handling (non-avatar) if (loadedKeys.has(key)) { console.warn(`[assets.ts] Duplicate key skipped: "${key}" from path: ${path}`); continue; diff --git a/src/game.ts b/src/game.ts index c69c1b70d..57fc06f41 100644 --- a/src/game.ts +++ b/src/game.ts @@ -742,6 +742,15 @@ export default class Game { $j('.lobby-match-list').append(_matchBtn); }); } + /** + * Function to refresh the avatar UI + */ + + refreshAvatarGrid() { + if (this.UI && typeof this.UI.refreshAvatarGrid === 'function') { + this.UI.refreshAvatarGrid(); + } + } /** * Resize the combat frame */ @@ -794,7 +803,8 @@ export default class Game { const last = this.activeCreature; this.activeCreature = next; // Set new activeCreature - + this.refreshAvatarGrid(); // trigger the grid refresh whenever a new creature becomes active + this.UI?.refreshAvatarGrid?.(); if (!last.dead) { last.updateHealth(); // Update health display due to active creature change } diff --git a/src/style/avatars.less b/src/style/avatars.less index 855698a48..c4d231df6 100644 --- a/src/style/avatars.less +++ b/src/style/avatars.less @@ -1,201 +1,219 @@ -.vignette { - &.typeA1 { - background-image: url('~assets/units/avatars/Swine Thug.jpg'); - } +.avatar-abolished[set="default"] { + background-image: url('/assets/units/avatars/Abolished.jpg'); +} - &.typeA2 { - background-image: url('~assets/units/avatars/Bounty Hunter.jpg'); - } +.avatar-aegis[set="default"] { + background-image: url('/assets/units/avatars/Aegis.jpg'); +} - &.typeA3 { - background-image: url('~assets/units/avatars/Cyber Wolf.jpg'); - } +.avatar-asher[set="default"] { + background-image: url('/assets/units/avatars/Asher.jpg'); +} - &.typeA4 { - background-image: url('~assets/units/avatars/Greedy Knight.jpg'); - } +.avatar-batmadillo[set="default"] { + background-image: url('/assets/units/avatars/Batmadillo.jpg'); +} - &.typeA5 { - background-image: url('~assets/units/avatars/Gilded Maiden.jpg'); - } +.avatar-blue-shrimp[set="default"] { + background-image: url('/assets/units/avatars/Blue Shrimp.jpg'); +} - &.typeA6 { - background-image: url('~assets/units/avatars/Living Armor.jpg'); - } +.avatar-bounty-hunter[set="default"] { + background-image: url('/assets/units/avatars/Bounty Hunter.jpg'); +} - &.typeA7 { - background-image: url('~assets/units/avatars/Golden Wyrm.jpg'); - } +.avatar-chimera[set="default"] { + background-image: url('/assets/units/avatars/Chimera.jpg'); +} - &.typeE1 { - background-image: url('~assets/units/avatars/Mr. Stitches.jpg'); - } +.avatar-crystalis[set="default"] { + background-image: url('/assets/units/avatars/Crystalis.jpg'); +} - &.typeE2 { - background-image: url('~assets/units/avatars/Nutcase.jpg'); - } +.avatar-cyber-wolf[set="default"] { + background-image: url('/assets/units/avatars/Cyber Wolf.jpg'); +} - &.typeE3 { - background-image: url('~assets/units/avatars/Stomper.jpg'); - } +.avatar-cycloper[set="default"] { + background-image: url('/assets/units/avatars/Cycloper.jpg'); +} - &.typeE4 { - background-image: url('~assets/units/avatars/Vertigo.jpg'); - } +.avatar-dark-priest-blue[set="default"] { + background-image: url('/assets/units/avatars/Dark Priest blue.jpg'); +} - &.typeE5 { - background-image: url('~assets/units/avatars/Scorpius.jpg'); - } +.avatar-dark-priest-green[set="default"] { + background-image: url('/assets/units/avatars/Dark Priest green.jpg'); +} - &.typeE6 { - background-image: url('~assets/units/avatars/Spikes.jpg'); - } +.avatar-dark-priest-orange[set="default"] { + background-image: url('/assets/units/avatars/Dark Priest orange.jpg'); +} - &.typeE7 { - background-image: url('~assets/units/avatars/Batmadillo.jpg'); - } +.avatar-dark-priest-red[set="default"] { + background-image: url('/assets/units/avatars/Dark Priest red.jpg'); +} - &.typeG1 { - background-image: url('~assets/units/avatars/Toxic Shroom.jpg'); - } +.avatar-dark-priest[set="default"] { + background-image: url('/assets/units/avatars/Dark Priest.jpg'); +} - &.typeG2 { - background-image: url('~assets/units/avatars/Swampler.jpg'); - } +.avatar-deep-beauty[set="default"] { + background-image: url('/assets/units/avatars/Deep Beauty.jpg'); +} + +.avatar-flayed[set="default"] { + background-image: url('/assets/units/avatars/Flayed.jpg'); +} + +.avatar-gilded-maiden[set="default"] { + background-image: url('/assets/units/avatars/Gilded Maiden.jpg'); +} + +.avatar-golden-wyrm[set="default"] { + background-image: url('/assets/units/avatars/Golden Wyrm.jpg'); +} - &.typeG3 { - background-image: url('~assets/units/avatars/Uncle Fungus.jpg'); - } +.avatar-greedy-knight[set="default"] { + background-image: url('/assets/units/avatars/Greedy Knight.jpg'); +} - &.typeG4 { - background-image: url('~assets/units/avatars/Miss Creeper.jpg'); - } +.avatar-gumble[set="default"] { + background-image: url('/assets/units/avatars/Gumble.jpg'); +} - &.typeG5 { - background-image: url('~assets/units/avatars/Papa Eggplant.jpg'); - } +.avatar-headless[set="default"] { + background-image: url('/assets/units/avatars/Headless.jpg'); +} - &.typeG6 { - background-image: url('~assets/units/avatars/Razorback.jpg'); - } +.avatar-horn-head[set="default"] { + background-image: url('/assets/units/avatars/Horn Head.jpg'); +} - &.typeG7 { - background-image: url('~assets/units/avatars/Moss Hound.jpg'); - } +.avatar-impaler[set="default"] { + background-image: url('/assets/units/avatars/Impaler.jpg'); +} + +.avatar-infernal[set="default"] { + background-image: url('/assets/units/avatars/Infernal.jpg'); +} - &.typeL1 { - background-image: url('~assets/units/avatars/Asher.jpg'); - } +.avatar-knightmare[set="default"] { + background-image: url('/assets/units/avatars/Knightmare.jpg'); +} - &.typeL2 { - background-image: url('~assets/units/avatars/Infernal.jpg'); - } +.avatar-kraken[set="default"] { + background-image: url('/assets/units/avatars/Kraken.jpg'); +} - &.typeL3 { - background-image: url('~assets/units/avatars/Metalist.jpg'); - } +.avatar-lavamander[set="default"] { + background-image: url('/assets/units/avatars/Lavamander.jpg'); +} - &.typeL4 { - background-image: url('~assets/units/avatars/Crystalis.jpg'); - } +.avatar-living-armor[set="default"] { + background-image: url('/assets/units/avatars/Living Armor.jpg'); +} - &.typeL5 { - background-image: url('~assets/units/avatars/Vulcan.jpg'); - } +.avatar-mangler[set="default"] { + background-image: url('/assets/units/avatars/Mangler.jpg'); +} - &.typeL6 { - background-image: url('~assets/units/avatars/Lavamander.jpg'); - } +.avatar-metalist[set="default"] { + background-image: url('/assets/units/avatars/Metalist.jpg'); +} - &.typeL7 { - background-image: url('~assets/units/avatars/Volpyr.jpg'); - } +.avatar-miss-creeper[set="default"] { + background-image: url('/assets/units/avatars/Miss Creeper.jpg'); +} - &.typeP1 { - background-image: url('~assets/units/avatars/Gumble.jpg'); - } +.avatar-moss-hound[set="default"] { + background-image: url('/assets/units/avatars/Moss Hound.jpg'); +} - &.typeP2 { - background-image: url('~assets/units/avatars/Royal Guard.jpg'); - } +.avatar-mr-stitches[set="default"] { + background-image: url('/assets/units/avatars/Mr. Stitches.jpg'); +} - &.typeP3 { - background-image: url('~assets/units/avatars/Scavenger.jpg'); - } +.avatar-nutcase[set="default"] { + background-image: url('/assets/units/avatars/Nutcase.jpg'); +} - &.typeP4 { - background-image: url('~assets/units/avatars/Troglodyte.jpg'); - } +.avatar-papa-eggplant[set="default"] { + background-image: url('/assets/units/avatars/Papa Eggplant.jpg'); +} - &.typeP5 { - background-image: url('~assets/units/avatars/Aegis.jpg'); - } +.avatar-razorback[set="default"] { + background-image: url('/assets/units/avatars/Razorback.jpg'); +} - &.typeP6 { - background-image: url('~assets/units/avatars/Chimera.jpg'); - } +.avatar-royal-guard[set="default"] { + background-image: url('/assets/units/avatars/Royal Guard.jpg'); +} - &.typeP7 { - background-image: url('~assets/units/avatars/Abolished.jpg'); - } +.avatar-sarcophag[set="default"] { + background-image: url('/assets/units/avatars/Sarcophag.jpg'); +} - &.typeS1 { - background-image: url('~assets/units/avatars/Snow Bunny.jpg'); - } +.avatar-satyr[set="default"] { + background-image: url('/assets/units/avatars/Satyr.jpg'); +} - &.typeS2 { - background-image: url('~assets/units/avatars/Blue Shrimp.jpg'); - } +.avatar-scavenger[set="default"] { + background-image: url('/assets/units/avatars/Scavenger.jpg'); +} - &.typeS3 { - background-image: url('~assets/units/avatars/Deep Beauty.jpg'); - } +.avatar-scorpius[set="default"] { + background-image: url('/assets/units/avatars/Scorpius.jpg'); +} - &.typeS4 { - background-image: url('~assets/units/avatars/Knightmare.jpg'); - } +.avatar-shadow-leech[set="default"] { + background-image: url('/assets/units/avatars/Shadow Leech.jpg'); +} - &.typeS5 { - background-image: url('~assets/units/avatars/Impaler.jpg'); - } +.avatar-snow-bunny[set="default"] { + background-image: url('/assets/units/avatars/Snow Bunny.jpg'); +} - &.typeS6 { - background-image: url('~assets/units/avatars/Kraken.jpg'); - } +.avatar-spikes[set="default"] { + background-image: url('/assets/units/avatars/Spikes.jpg'); +} - &.typeS7 { - background-image: url('~assets/units/avatars/Vehemoth.jpg'); - } +.avatar-stomper[set="default"] { + background-image: url('/assets/units/avatars/Stomper.jpg'); +} - &.typeW1 { - background-image: url('~assets/units/avatars/Mangler.jpg'); - } +.avatar-swampler[set="default"] { + background-image: url('/assets/units/avatars/Swampler.jpg'); +} - &.typeW2 { - background-image: url('~assets/units/avatars/Shadow Leech.jpg'); - } +.avatar-swine-thug[set="default"] { + background-image: url('/assets/units/avatars/Swine Thug.jpg'); +} - &.typeW3 { - background-image: url('~assets/units/avatars/Cycloper.jpg'); - } +.avatar-toxic-shroom[set="default"] { + background-image: url('/assets/units/avatars/Toxic Shroom.jpg'); +} - &.typeW4 { - background-image: url('~assets/units/avatars/Headless.jpg'); - } +.avatar-troglodyte[set="default"] { + background-image: url('/assets/units/avatars/Troglodyte.jpg'); +} - &.typeW5 { - background-image: url('~assets/units/avatars/Horn Head.jpg'); - } +.avatar-uncle-fungus[set="default"] { + background-image: url('/assets/units/avatars/Uncle Fungus.jpg'); +} - &.typeW6 { - background-image: url('~assets/units/avatars/Satyr.jpg'); - } +.avatar-vehemoth[set="default"] { + background-image: url('/assets/units/avatars/Vehemoth.jpg'); +} - &.typeW7 { - background-image: url('~assets/units/avatars/Sarcophag.jpg'); - } +.avatar-vertigo[set="default"] { + background-image: url('/assets/units/avatars/Vertigo.jpg'); +} - &.typeWS { - background-image: url('~assets/units/avatars/Shadow Leech.jpg'); - } +.avatar-volpyr[set="default"] { + background-image: url('/assets/units/avatars/Volpyr.jpg'); } + +.avatar-vulcan[set="default"] { + background-image: url('/assets/units/avatars/Vulcan.jpg'); +} \ No newline at end of file diff --git a/src/ui/interface.js b/src/ui/interface.js index 0ce089835..3514c5b93 100644 --- a/src/ui/interface.js +++ b/src/ui/interface.js @@ -713,6 +713,14 @@ export class UI { } } } + /** + * Refreshes the dashboard avatar grid. + * Called when the active creature changes, moves between realms/levels, + * or when new creatures are summoned. + */ + refreshAvatarGrid() { + console.log('🔄 Avatar grid refreshed!'); + } hideAbilityCosts() { const game = this.game, diff --git a/webpack.config.js b/webpack.config.js index c0f33e3b8..d42f38138 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -25,7 +25,11 @@ const Dotenv = require('dotenv-webpack'); const toObjString = (paths) => '{' + paths.map((path) => `"${path}":require("${path}")`).join(',') + '}'; const globOptions = { ignore: ['**/*.js', '**/*.ts', '**/*.md'], posix: true }; - const phaserAutoloadAssets = toObjString(glob.sync('assets/autoload/phaser/**/*.*', globOptions)); + const phaserAutoloadAssets = toObjString( + glob + .sync('assets/autoload/phaser/**/*.*', globOptions) + .concat(glob.sync('assets/units/avatars/**/*.{jpg,jpeg,png}', globOptions)), + ); const allAssets = toObjString(glob.sync('assets/**/*.*', globOptions)); fs.writeFileSync( @@ -61,11 +65,16 @@ module.exports = (env, argv) => { path: path.resolve(__dirname, 'deploy'), filename: '[name].[contenthash].bundle.js', clean: true, // NOTE: Clean the output folder before each build. - assetModuleFilename: () => { - if (production) { - return 'assets/[contenthash].[ext]'; + assetModuleFilename: (pathData) => { + const fullPath = pathData.filename.replace(/\\/g, '/'); + + // If it's an avatar, keep the original filename + if (fullPath.includes('/units/avatars/')) { + return 'assets/units/avatars/[name][ext]'; // Avoid file hashing for avatars } - return '[path][name].[ext]'; + + // For everything else, use contenthash + return production ? 'assets/[contenthash][ext]' : '[path][name][ext]'; }, }, devtool: production ? 'source-map' : 'inline-source-map', @@ -125,9 +134,21 @@ module.exports = (env, argv) => { test: /\.css$/, use: ['style-loader', 'css-loader'], }, + { + test: /\.(png|jpe?g)$/, + include: path.resolve(__dirname, 'assets/units/avatars'), + type: 'asset/resource', + generator: { + filename: 'assets/units/avatars/[name][ext]', // ✅ keeps Wisp.jpg etc. + }, + }, { test: /\.(png|jpg|gif|svg|ogg|ico|cur|woff|woff2)$/, + exclude: path.resolve(__dirname, 'assets/units/avatars'), type: 'asset/resource', + generator: { + filename: 'assets/[contenthash][ext]', // ✅ keeps hashing for everything else + }, }, ], },