Skip to content

Commit 1971bbb

Browse files
Overhaul UI
1 parent 6bceef6 commit 1971bbb

46 files changed

Lines changed: 12362 additions & 5083 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/client/src/App.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useEffect } from "react";
22
import { Nav } from "./ui/Nav";
3-
import { Backdrop } from "./ui/Backdrop";
43
import { HomePage } from "./pages/HomePage";
54
import { LoginPage } from "./pages/LoginPage";
65
import { RegisterPage } from "./pages/RegisterPage";
@@ -35,14 +34,12 @@ export function App(): JSX.Element {
3534

3635
if (loading) {
3736
return (
38-
<>
39-
<Backdrop />
40-
<div className="screen">
41-
<div className="center-card">
42-
<h1>Loading…</h1>
43-
</div>
37+
<div className="screen">
38+
<div className="center-card">
39+
<span className="center-card-stamp standby">Stand By</span>
40+
<h1>Loading</h1>
4441
</div>
45-
</>
42+
</div>
4643
);
4744
}
4845

@@ -75,15 +72,14 @@ export function App(): JSX.Element {
7572
case "settings": page = <SettingsPage navigate={navigate} />; break;
7673
case "customize": page = <CustomizePage navigate={navigate} />; break;
7774
case "arsenal": page = <ArsenalPage navigate={navigate} />; break;
78-
case "about": page = <AboutPage navigate={navigate} />; break;
75+
case "about": page = <AboutPage />; break;
7976
case "profile": page = <ProfilePage username={route.username} navigate={navigate} />; break;
8077
}
8178

8279
return (
8380
<>
84-
<Backdrop />
85-
<Nav navigate={navigate} />
86-
<div className="page">{page}</div>
81+
<Nav navigate={navigate} route={route} />
82+
<div className="page" data-route={route.name}>{page}</div>
8783
<MusicPlayer />
8884
</>
8985
);

packages/client/src/game/scenes/BattleScene.ts

Lines changed: 46 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
WEAPONS,
1818
} from "@artillery/shared";
1919
import { Sound } from "../audio/Sound";
20-
import { TankView, getTankPreviewTextures } from "../entities/Tank";
20+
import { TankView } from "../entities/Tank";
2121
import { TerrainView } from "../entities/TerrainView";
2222
import { ProjectileView } from "../entities/ProjectileView";
2323
import { FireView } from "../entities/FireView";
@@ -60,15 +60,6 @@ export class BattleScene extends Phaser.Scene {
6060
private reticle!: Phaser.GameObjects.Graphics;
6161
private dragOverlay!: Phaser.GameObjects.Graphics;
6262
private lastArcGfx!: Phaser.GameObjects.Graphics;
63-
private aimLabel?: Phaser.GameObjects.Text;
64-
private cursorPreview?: {
65-
container: Phaser.GameObjects.Container;
66-
hull: Phaser.GameObjects.Image;
67-
barrel: Phaser.GameObjects.Image;
68-
barrelOffsetX: number;
69-
barrelOffsetY: number;
70-
loadoutKey: string;
71-
};
7263
private windEmitter?: Phaser.GameObjects.Particles.ParticleEmitter;
7364
private cursorOnCanvas = false;
7465
private cursorScreenX = 0;
@@ -359,7 +350,7 @@ export class BattleScene extends Phaser.Scene {
359350
0,
360351
Math.min(1, (power - TANK.MIN_POWER) / (TANK.MAX_POWER - TANK.MIN_POWER)),
361352
);
362-
const arrowLen = 40 + powerT * 90;
353+
const arrowLen = 32 + Math.pow(Math.max(0, powerT), 1.4) * 320;
363354
const tipX = baseX + dirX * arrowLen;
364355
const tipY = baseY + dirY * arrowLen;
365356
this.lastArcGfx.lineStyle(2, color, 0.6);
@@ -938,10 +929,6 @@ export class BattleScene extends Phaser.Scene {
938929
this.aimLine.clear();
939930
this.reticle.clear();
940931
this.dragOverlay.clear();
941-
if (!this.dragging) {
942-
if (this.aimLabel) this.aimLabel.setVisible(false);
943-
if (this.cursorPreview) this.cursorPreview.container.setVisible(false);
944-
}
945932

946933
if (!this.isMyTurn()) return;
947934
const self = this.room.state.players.get(this.room.sessionId);
@@ -975,9 +962,16 @@ export class BattleScene extends Phaser.Scene {
975962
const perpX = -dirY;
976963
const perpY = dirX;
977964

965+
// Range scales with v² in the physics, so use a slight power curve
966+
// so 100% reads as "across-the-map" rather than just "a bit longer".
967+
const ARROW_BASE = 32;
968+
const ARROW_RANGE = 320;
969+
const ARROW_MAX = ARROW_BASE + ARROW_RANGE;
970+
const arrowScale = (t: number) => Math.pow(Math.max(0, t), 1.4);
971+
const arrowLen = ARROW_BASE + arrowScale(powerT) * ARROW_RANGE;
972+
978973
if (!dense) {
979974
// Idle: thin tracer of the current aim. No extra chrome.
980-
const arrowLen = 40 + powerT * 90;
981975
const tipX = baseX + dirX * arrowLen;
982976
const tipY = baseY + dirY * arrowLen;
983977
this.aimLine.lineStyle(2, 0xd49228, 0.6);
@@ -997,113 +991,55 @@ export class BattleScene extends Phaser.Scene {
997991
// Active drag: same scale as idle arrow, just thicker line and
998992
// power-coloured tier tint. Keeps geometry consistent so it never
999993
// jumps in size when drag begins.
1000-
const arrowLen = 40 + powerT * 90;
1001994
const tipX = baseX + dirX * arrowLen;
1002995
const tipY = baseY + dirY * arrowLen;
1003996
const tier =
1004997
powerT > 0.9 ? 0xff5a3c :
1005998
powerT > 0.65 ? 0xff8a2a :
1006999
powerT > 0.35 ? 0xffbe52 : 0xf0e090;
1007-
this.aimLine.lineStyle(3, tier, 0.95);
1000+
// Steps up at the same thresholds as the tier colour so each
1001+
// crossing is felt: thicker shaft + bigger arrowhead.
1002+
const shaftWidth =
1003+
powerT > 0.9 ? 6 :
1004+
powerT > 0.65 ? 5 :
1005+
powerT > 0.35 ? 4 : 3;
1006+
const headLen = 6 + shaftWidth;
1007+
const headHalf = 3 + shaftWidth * 0.6;
1008+
1009+
// Faint full-range track behind the live arrow plus perpendicular
1010+
// tick marks at 25/50/75/100% so the player can read power without
1011+
// chasing the moving tip.
1012+
const trackEndX = baseX + dirX * ARROW_MAX;
1013+
const trackEndY = baseY + dirY * ARROW_MAX;
1014+
this.aimLine.lineStyle(1, 0xffffff, 0.18);
1015+
this.aimLine.beginPath();
1016+
this.aimLine.moveTo(baseX, baseY);
1017+
this.aimLine.lineTo(trackEndX, trackEndY);
1018+
this.aimLine.strokePath();
1019+
for (const frac of [0.25, 0.5, 0.75, 1]) {
1020+
const tickLen = ARROW_BASE + arrowScale(frac) * ARROW_RANGE;
1021+
const tx = baseX + dirX * tickLen;
1022+
const ty = baseY + dirY * tickLen;
1023+
const half = frac === 1 ? 7 : 5;
1024+
const reached = powerT >= frac - 0.001;
1025+
this.aimLine.lineStyle(reached ? 2 : 1, reached ? tier : 0xffffff, reached ? 0.85 : 0.32);
1026+
this.aimLine.beginPath();
1027+
this.aimLine.moveTo(tx + perpX * half, ty + perpY * half);
1028+
this.aimLine.lineTo(tx - perpX * half, ty - perpY * half);
1029+
this.aimLine.strokePath();
1030+
}
1031+
1032+
this.aimLine.lineStyle(shaftWidth, tier, 0.95);
10081033
this.aimLine.beginPath();
10091034
this.aimLine.moveTo(baseX, baseY);
10101035
this.aimLine.lineTo(tipX, tipY);
10111036
this.aimLine.strokePath();
10121037
this.aimLine.fillStyle(tier, 0.95);
10131038
this.aimLine.fillTriangle(
1014-
tipX + dirX * 8, tipY + dirY * 8,
1015-
tipX - dirX * 3 + perpX * 5, tipY - dirY * 3 + perpY * 5,
1016-
tipX - dirX * 3 - perpX * 5, tipY - dirY * 3 - perpY * 5,
1039+
tipX + dirX * headLen, tipY + dirY * headLen,
1040+
tipX - dirX * 3 + perpX * headHalf, tipY - dirY * 3 + perpY * headHalf,
1041+
tipX - dirX * 3 - perpX * headHalf, tipY - dirY * 3 - perpY * headHalf,
10171042
);
1018-
1019-
// Cursor tank preview — a mini copy of the local player's tank
1020-
// anchored at the pointer, with the barrel rotated to match the
1021-
// current aim. Reuses the in-game hull/barrel textures so colours,
1022-
// pattern, decal, and turret shape all match instantly.
1023-
const ptr = this.input.activePointer;
1024-
this.renderCursorPreview(self, ptr.x, ptr.y, angleDeg, facing, powerT, tier);
1025-
}
1026-
1027-
private renderCursorPreview(
1028-
self: Player,
1029-
cx: number,
1030-
cy: number,
1031-
angleDeg: number,
1032-
facing: -1 | 1,
1033-
powerT: number,
1034-
tier: number,
1035-
): void {
1036-
const SCALE = 0.7;
1037-
const PREVIEW_OFFSET_Y = -12;
1038-
1039-
const loadoutKey =
1040-
`${self.bodyStyle}|${self.turretStyle}|${self.barrelStyle}|${self.color}|${self.accentColor}|${self.pattern}|${self.patternColor}|${self.decal}`;
1041-
1042-
let p = this.cursorPreview;
1043-
if (!p || p.loadoutKey !== loadoutKey) {
1044-
// Tear down any stale preview (loadout changed mid-match).
1045-
if (p) p.container.destroy(true);
1046-
const meta = getTankPreviewTextures(this, self);
1047-
const container = this.add.container(0, 0)
1048-
.setDepth(16)
1049-
.setScrollFactor(0);
1050-
const hull = this.add.image(0, 0, meta.hull.textureKey)
1051-
.setOrigin(
1052-
meta.hull.hullCenterX / meta.hull.widthLogical,
1053-
meta.hull.hullCenterY / meta.hull.heightLogical,
1054-
)
1055-
.setDisplaySize(meta.hull.widthLogical, meta.hull.heightLogical);
1056-
const barrelOffsetX = meta.hull.barrelPivotX - meta.hull.hullCenterX;
1057-
const barrelOffsetY = meta.hull.barrelPivotY - meta.hull.hullCenterY;
1058-
const barrel = this.add.image(barrelOffsetX, barrelOffsetY, meta.barrel.textureKey)
1059-
.setOrigin(
1060-
meta.barrel.pivotX / meta.barrel.widthLogical,
1061-
meta.barrel.pivotY / meta.barrel.heightLogical,
1062-
)
1063-
.setDisplaySize(meta.barrel.widthLogical, meta.barrel.heightLogical);
1064-
container.add([hull, barrel]);
1065-
p = { container, hull, barrel, barrelOffsetX, barrelOffsetY, loadoutKey };
1066-
this.cursorPreview = p;
1067-
}
1068-
1069-
p.container.setVisible(true);
1070-
p.container.setPosition(cx, cy + PREVIEW_OFFSET_Y);
1071-
p.container.setScale(SCALE * facing, SCALE);
1072-
const angleRad = (angleDeg * Math.PI) / 180;
1073-
p.barrel.setRotation(-angleRad);
1074-
1075-
// Power ring drawn around the preview — faint full circle plus a
1076-
// bright arc filling clockwise as power climbs.
1077-
const ringR = 32;
1078-
this.dragOverlay.lineStyle(2, 0xffffff, 0.18);
1079-
this.dragOverlay.strokeCircle(cx, cy + PREVIEW_OFFSET_Y, ringR);
1080-
this.dragOverlay.lineStyle(3, tier, 0.9);
1081-
this.dragOverlay.beginPath();
1082-
this.dragOverlay.arc(
1083-
cx, cy + PREVIEW_OFFSET_Y, ringR,
1084-
-Math.PI / 2,
1085-
-Math.PI / 2 + powerT * Math.PI * 2,
1086-
);
1087-
this.dragOverlay.strokePath();
1088-
1089-
// Power % label below the ring — single readout, no overlap with
1090-
// arrow since the arrow now lives at the tank.
1091-
const txt = this.aimLabel ?? this.add
1092-
.text(0, 0, "", {
1093-
fontFamily: "JetBrains Mono, monospace",
1094-
fontSize: "12px",
1095-
color: "#ffffff",
1096-
backgroundColor: "rgba(11,16,32,0.9)",
1097-
padding: { left: 6, right: 6, top: 2, bottom: 2 },
1098-
})
1099-
.setDepth(17)
1100-
.setScrollFactor(0)
1101-
.setOrigin(0.5, 0);
1102-
this.aimLabel = txt;
1103-
txt.setText(`${Math.abs(angleDeg).toFixed(0)}° · ${Math.round(powerT * 100)}%`);
1104-
txt.setColor(`#${tier.toString(16).padStart(6, "0")}`);
1105-
txt.setPosition(cx, cy + PREVIEW_OFFSET_Y + ringR + 6);
1106-
txt.setVisible(true);
11071043
}
11081044

11091045
private updateCamera(dt: number): void {

0 commit comments

Comments
 (0)