From c5c04a8d83b593cd2e9a881c116e99a5c8b06737 Mon Sep 17 00:00:00 2001 From: Vivacious Box Date: Wed, 2 Jul 2025 02:09:18 +0200 Subject: [PATCH] Show structure levels (#1305) ## Description: Show structure levels on the structureIconsLayer Add new bitmap font (modified from this font: https://frostyfreeze.itch.io/pixel-bitmap-fonts-png-xml CC0 license) ![image](https://github.com/user-attachments/assets/8c077ec9-00bb-4f36-a9e7-6d81aec3a90f) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: Vivacious Box --- resources/fonts/round_6x6_modified.png | Bin 0 -> 2610 bytes resources/fonts/round_6x6_modified.xml | 276 ++++++++++++++++++ .../graphics/layers/StructureIconsLayer.ts | 147 ++++++---- src/global.d.ts | 4 + webpack.config.js | 2 +- 5 files changed, 376 insertions(+), 53 deletions(-) create mode 100644 resources/fonts/round_6x6_modified.png create mode 100644 resources/fonts/round_6x6_modified.xml diff --git a/resources/fonts/round_6x6_modified.png b/resources/fonts/round_6x6_modified.png new file mode 100644 index 0000000000000000000000000000000000000000..04ab5461a8230286925d84fedaf64df992eb0ce7 GIT binary patch literal 2610 zcmV-23eEM2P)Px;>`6pHRCt{2oy&C_I}C=Qds0eP-l_m^-GEw9QE4F!WSauA&C0##F5=M{(Zq`Y zhot;}=XfE{fFL@0q{eup@oqq#O`8%J_CnBOhfBrS{`K|sL`3xa_ivXxSAK2`#HHsQV+1T@H6M=pYqCIh;g58<|HA?S5G1P) z>>7jY`j*=zu?2a1dm|#EU%!6cTI8c+07HwGD$liiLVDR_7*~s4S}=!&bk%?5xi{t4 zT3+opED!)emKuYMmf!G>VGLu2q2*5)19i=RU<_0xGykwa00j7^Tz_aGta#UPw%@1I z$yL7f2v_7_Jwko^)rGD-CY3omwS*VBd_saja4pgSA)qnQ$lj;u^2!H_J zsM3OH7DBtu!L>ZrbOc|pkbbK?myT9H8rkLV_58Sete*dfepny?0001h<*2R$w8mR} z)p(+4I@$3^xPDwb6vjXR1Q1t(t??M6<9SxmU#8Q`7x7uHTjFtg$ZNn80e~c`o(Po{ zUy-uqHTV*VE5Tm*RrGI^-pbrMMq)h9C|qL-&f`Gyh0B)zR0u&H76^aAv0w93sjjcsS8fX&a#t`1VKiBeUEg2V&VkJ5TOXQF- z$S0-7$l9HLBEj;M`Qh{n7G1o~&7zjiakESq0|AhPXo0wRjFJQ5SxCBH)6cbJngy`N zqiAEFG9THqXwmvN%x1arU0QlAUtG)2$!A+|Zv9TE$Gd)5AOHfS!~^w?2hq^|4g6EYJiv z0s@z6!OP_X&4p@3wsjz)#0SXjWNF=#z^mUI5n9qqe`yJhdV#$4Uoy*!U-63W(trqE zP15wk0!@JS;RT#|hf;A4UCM%5*a~8LfZjfAmi&e=yOgS*<0$z7^%N9Wlg%b(_3^1)2csW+W4L z0ks#HXzu(s^Nl*h^H}9JqWVY6m)Be4np!FKmMiJr{2X15X#Tb5=Y4J-{jfk25dCl` z;&i!O+%GTyAxgLZ@^7UufLb|L%0B=Ad`-^h^R_$Yjfif$-(XUv7z1U+!8}_$4X_aj zM~G7O#;#;A^a9WR;*S1+1#fQCgO9#ItIJ1;oKQH01+s<_^a_UnYZD1ah*I^&{&bV> zNl-0_Rlc|WGM!d$;L-Yp2S3`I`>EtPmhr~+Iukx?JPnwi>Ic;81zx2K6)iuPPOCR} zbeEmk(ZV@;*FzbByJ2Z~r?fx-co-Q^g%3zz3k=gHF{~|Ez?-`(*%n~6E^`@rzG3M> zO!)!z3Woq+gCoblSv!+Hr91mBBw5Ti*Mi-J8_RDUJ4Wl@Y5}eHd0$}eOw&^S=rOdV z2Qj4u@`gfd?(DEg&&|kqs%k{)(eW6giN?QVg(I}qUsnB(h^NVrj}kfGX$lKe4r0H5 z|9&DOdVhZ>a#YyvzSBoKv`@EoOgUJek?~YhtjCn6D3l*iE87S3{{D^+=!t<7F#wD5 z;0t$vyy(kyfPSxh^m-wI+2|{nfo5no7oo*76*hJ0Gq3vuCY(rJGHe&#d?N({*tSR36R89o1*OPs5simRxo^z4FnjKkI14Ug>62 z=IeUB-raO+*SY_DepkOV;k=sdmQv2@bEQ#TPiS4oh_3fEq!W^FsCQAn_c>JLT=^;Q z-}#K0X46{Fm(ud-^#!c##LAW*vv2?esQSWjUoy8hvT(XUsLyQOlR3J%_-Bv~^#%@;PYfSS^pRKVVg_^?eXdxoDE(ewXP9`vR)vhXn#4K$9<(5P2B&VHzc$ zHHKgIM5*7%609XggO=s10pp@r_bszwM z3vL~V$Z>8?^n4vIo$~+I^J%Pd+37^(z4fl??=8pWf3uXkSuR)kOH12YZ{^vwt?#dN z9To@x0001BIUf3&S+gLO>!v<%_P%S@{pF%D_|e%XE}h-~o8+VAn0YUPS-=5YPW zFC*_4ED!(z9zH2=L|~<*bIET+*FU&)7MeX0$3PoGJ+^uwZ|M>V=j8N09pNZ?jKyGq z00I0000007*qoM6N<$f`aZL3jhEB literal 0 HcmV?d00001 diff --git a/resources/fonts/round_6x6_modified.xml b/resources/fonts/round_6x6_modified.xml new file mode 100644 index 000000000..509654b00 --- /dev/null +++ b/resources/fonts/round_6x6_modified.xml @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/graphics/layers/StructureIconsLayer.ts index ffdf680a5..8e0a973c5 100644 --- a/src/client/graphics/layers/StructureIconsLayer.ts +++ b/src/client/graphics/layers/StructureIconsLayer.ts @@ -1,4 +1,5 @@ import * as PIXI from "pixi.js"; +import bitmapFont from "../../../../resources/fonts/round_6x6_modified.xml"; import anchorIcon from "../../../../resources/images/AnchorIcon.png"; import cityIcon from "../../../../resources/images/CityIcon.png"; import factoryIcon from "../../../../resources/images/FactoryUnit.png"; @@ -17,7 +18,8 @@ class StructureRenderInfo { constructor( public unit: UnitView, public owner: PlayerID, - public pixiSprite: PIXI.Sprite, + public pixiContainer: PIXI.Container, + public level: number = 0, ) {} } const ZOOM_THRESHOLD = 2.8; // below this zoom level, structures are not rendered @@ -54,6 +56,11 @@ export class StructureIconsLayer implements Layer { } async setupRenderer() { + try { + await PIXI.Assets.load(bitmapFont); + } catch (error) { + console.error("Failed to load bitmap font:", error); + } this.renderer = new PIXI.WebGLRenderer(); this.pixicanvas = document.createElement("canvas"); this.pixicanvas.width = window.innerWidth; @@ -119,41 +126,57 @@ export class StructureIconsLayer implements Layer { if (unitView === undefined) return; if (unitView.isActive()) { - if (this.seenUnits.has(unitView)) { - // check if owner has changed - const render = this.renders.find( - (r) => r.unit.id() === unitView.id(), - ); - if (render) { - this.ownerChangeCheck(render, unitView); - } - } else if (this.structures.has(unitView.type())) { - // new unit, create render info - this.seenUnits.add(unitView); - const render = new StructureRenderInfo( - unitView, - unitView.owner().id(), - this.createPixiSprite(unitView), - ); - this.renders.push(render); - this.computeNewLocation(render); - this.shouldRedraw = true; - } - } - - if (!unitView.isActive() && this.seenUnits.has(unitView)) { - const render = this.renders.find( - (r) => r.unit.id() === unitView.id(), - ); - if (render) { - this.deleteStructure(render); - } - this.shouldRedraw = true; - return; + this.handleActiveUnit(unitView); + } else if (this.seenUnits.has(unitView)) { + this.handleInactiveUnit(unitView); } }); } + private findRenderByUnit( + unitView: UnitView, + ): StructureRenderInfo | undefined { + return this.renders.find((render) => render.unit.id() === unitView.id()); + } + + private handleActiveUnit(unitView: UnitView) { + if (this.seenUnits.has(unitView)) { + const render = this.findRenderByUnit(unitView); + if (render) { + this.checkForOwnershipChange(render, unitView); + this.checkForLevelChange(render, unitView); + } + } else if (this.structures.has(unitView.type())) { + this.addNewStructure(unitView); + } + } + + private handleInactiveUnit(unitView: UnitView) { + const render = this.findRenderByUnit(unitView); + if (render) { + this.deleteStructure(render); + this.shouldRedraw = true; + } + } + + private checkForOwnershipChange(render: StructureRenderInfo, unit: UnitView) { + if (render && render.owner !== unit.owner().id()) { + render.owner = unit.owner().id(); + render.pixiContainer?.destroy(); + render.pixiContainer = this.createPixiSprite(unit); + this.shouldRedraw = true; + } + } + + private checkForLevelChange(render: StructureRenderInfo, unit: UnitView) { + if (render && render.level !== unit.level()) { + render.level = unit.level(); + render.pixiContainer?.destroy(); + render.pixiContainer = this.createPixiSprite(unit); + this.shouldRedraw = true; + } + } + redraw() { this.resizeCanvas(); } @@ -176,15 +199,6 @@ export class StructureIconsLayer implements Layer { mainContext.drawImage(this.renderer.canvas, 0, 0); } - private ownerChangeCheck(render: StructureRenderInfo, unit: UnitView) { - if (render.owner !== unit.owner().id()) { - render.owner = unit.owner().id(); - render.pixiSprite?.destroy(); - render.pixiSprite = this.createPixiSprite(unit); - this.shouldRedraw = true; - } - } - private createTexture(unit: UnitView): PIXI.Texture { const cacheKey = `${unit.owner().id()}-${unit.type()}`; if (this.textureCache.has(cacheKey)) { @@ -229,7 +243,8 @@ export class StructureIconsLayer implements Layer { return texture; } - private createPixiSprite(unit: UnitView): PIXI.Sprite { + private createPixiSprite(unit: UnitView): PIXI.Container { + const parentContainer = new PIXI.Container(); const sprite = new PIXI.Sprite(this.createTexture(unit)); sprite.anchor.set(0.5, 0.5); const tile = unit.tile(); @@ -238,11 +253,26 @@ export class StructureIconsLayer implements Layer { const screenPos = this.transformHandler.worldToScreenCoordinates( new Cell(worldX, worldY), ); - sprite.x = screenPos.x; - sprite.y = screenPos.y - this.transformHandler.scale * OFFSET_ZOOM_Y; - sprite.scale.set(Math.min(1, this.transformHandler.scale)); - this.stage.addChild(sprite); - return sprite; + parentContainer.addChild(sprite); + if (unit.level() > 1) { + const text = new PIXI.BitmapText({ + text: unit.level().toString(), + style: { + fontFamily: "round_6x6_modified", + fontSize: 12, + }, + }); + text.anchor.set(0.5, 0.5); + text.position.y = -ICON_SIZE / 2 - 2; + parentContainer.addChild(text); + } + parentContainer.position.set( + Math.round(screenPos.x), + Math.round(screenPos.y - this.transformHandler.scale * OFFSET_ZOOM_Y), + ); + parentContainer.scale.set(Math.min(1, this.transformHandler.scale)); + this.stage.addChild(parentContainer); + return parentContainer; } private getImageColored( @@ -281,19 +311,32 @@ export class StructureIconsLayer implements Layer { screenPos.y - margin < this.pixicanvas.height; if (onScreen) { - render.pixiSprite.x = screenPos.x; - render.pixiSprite.y = screenPos.y; - render.pixiSprite.scale.set(Math.min(1, this.transformHandler.scale)); + render.pixiContainer.x = screenPos.x; + render.pixiContainer.y = screenPos.y; + render.pixiContainer.scale.set(Math.min(1, this.transformHandler.scale)); } if (render.isOnScreen !== onScreen) { // prevent unnecessary updates render.isOnScreen = onScreen; - render.pixiSprite.visible = onScreen; + render.pixiContainer.visible = onScreen; } } + private addNewStructure(unitView: UnitView) { + this.seenUnits.add(unitView); + const render = new StructureRenderInfo( + unitView, + unitView.owner().id(), + this.createPixiSprite(unitView), + unitView.level(), + ); + this.renders.push(render); + this.computeNewLocation(render); + this.shouldRedraw = true; + } + private deleteStructure(render: StructureRenderInfo) { - render.pixiSprite?.destroy(); + render.pixiContainer?.destroy(); this.renders = this.renders.filter((r) => r.unit !== render.unit); this.seenUnits.delete(render.unit); } diff --git a/src/global.d.ts b/src/global.d.ts index e100dde8e..5c3aadc48 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -36,3 +36,7 @@ declare module "*.html" { const content: string; export default content; } +declare module "*.xml" { + const value: string; + export default value; +} diff --git a/webpack.config.js b/webpack.config.js index 78721528f..487894a6c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -87,7 +87,7 @@ export default async (env, argv) => { }, }, { - test: /\.(woff|woff2|eot|ttf|otf)$/, + test: /\.(woff|woff2|eot|ttf|otf|xml)$/, type: "asset/resource", // Changed from file-loader generator: { filename: "fonts/[name].[contenthash][ext]", // Added content hash and fixed path