feat(game): 创建游戏核心框架(常量、配置、状态管理、地图渲染、主场景)
This commit is contained in:
96
game/GameManager.ts
Normal file
96
game/GameManager.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
INITIAL_HC,
|
||||||
|
INITIAL_KPI,
|
||||||
|
} from './constants'
|
||||||
|
|
||||||
|
type GameState = 'idle' | 'playing' | 'paused' | 'victory' | 'defeat'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏状态管理器(单例)
|
||||||
|
* 负责管理 HC(人才储备)、KPI、波次状态,并通过回调事件通知 Scene
|
||||||
|
*/
|
||||||
|
export class GameManager {
|
||||||
|
private static instance: GameManager
|
||||||
|
|
||||||
|
public hc: number = INITIAL_HC
|
||||||
|
public kpi: number = INITIAL_KPI
|
||||||
|
public currentWave: number = 0
|
||||||
|
public gameState: GameState = 'idle'
|
||||||
|
|
||||||
|
// 事件回调(Scene 通过这些 hook 更新 HUD)
|
||||||
|
public onHCChange: ((hc: number) => void)[] = []
|
||||||
|
public onKPIChange: ((kpi: number) => void)[] = []
|
||||||
|
public onGameOver: (() => void)[] = []
|
||||||
|
public onVictory: (() => void)[] = []
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static getInstance(): GameManager {
|
||||||
|
if (!GameManager.instance) {
|
||||||
|
GameManager.instance = new GameManager()
|
||||||
|
}
|
||||||
|
return GameManager.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置游戏状态(新局开始时调用) */
|
||||||
|
reset(): void {
|
||||||
|
this.hc = INITIAL_HC
|
||||||
|
this.kpi = INITIAL_KPI
|
||||||
|
this.currentWave = 0
|
||||||
|
this.gameState = 'idle'
|
||||||
|
this.onHCChange = []
|
||||||
|
this.onKPIChange = []
|
||||||
|
this.onGameOver = []
|
||||||
|
this.onVictory = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扣除 HC,余额不足时返回 false
|
||||||
|
* @param amount 扣除数量
|
||||||
|
*/
|
||||||
|
spendHC(amount: number): boolean {
|
||||||
|
if (this.hc < amount) return false
|
||||||
|
this.hc -= amount
|
||||||
|
this.onHCChange.forEach(cb => cb(this.hc))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加 HC
|
||||||
|
* @param amount 增加数量
|
||||||
|
*/
|
||||||
|
addHC(amount: number): void {
|
||||||
|
this.hc += amount
|
||||||
|
this.onHCChange.forEach(cb => cb(this.hc))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 减少 KPI,归零时触发 GameOver
|
||||||
|
* @param amount 减少数量
|
||||||
|
*/
|
||||||
|
reduceKPI(amount: number): void {
|
||||||
|
this.kpi = Math.max(0, this.kpi - amount)
|
||||||
|
this.onKPIChange.forEach(cb => cb(this.kpi))
|
||||||
|
if (this.kpi <= 0 && this.gameState === 'playing') {
|
||||||
|
this.gameState = 'defeat'
|
||||||
|
this.onGameOver.forEach(cb => cb())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加 KPI(不超过 100)
|
||||||
|
* @param amount 增加数量
|
||||||
|
*/
|
||||||
|
addKPI(amount: number): void {
|
||||||
|
this.kpi = Math.min(100, this.kpi + amount)
|
||||||
|
this.onKPIChange.forEach(cb => cb(this.kpi))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 触发胜利 */
|
||||||
|
triggerVictory(): void {
|
||||||
|
if (this.gameState === 'playing') {
|
||||||
|
this.gameState = 'victory'
|
||||||
|
this.onVictory.forEach(cb => cb())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
148
game/GameScene.ts
Normal file
148
game/GameScene.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { MAP_COLS, MAP_ROWS, HUD_HEIGHT, GAME_HEIGHT, GAME_WIDTH } from './constants'
|
||||||
|
import { GameManager } from './GameManager'
|
||||||
|
import {
|
||||||
|
PATH_TILES,
|
||||||
|
getCellSize,
|
||||||
|
drawAllTiles,
|
||||||
|
renderMapLabels,
|
||||||
|
renderHUD,
|
||||||
|
updateKPIBar,
|
||||||
|
BAR_X,
|
||||||
|
BAR_Y,
|
||||||
|
BAR_W,
|
||||||
|
BAR_H,
|
||||||
|
} from './mapRenderer'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建主游戏场景类(工厂函数,接收动态导入的 Phaser 实例)
|
||||||
|
* 这样可以保证 SSR 安全(只在客户端执行)
|
||||||
|
*/
|
||||||
|
export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
|
||||||
|
class GameScene extends PhaserLib.Scene {
|
||||||
|
private manager!: GameManager
|
||||||
|
private kpiBar!: Phaser.GameObjects.Graphics
|
||||||
|
private kpiText!: Phaser.GameObjects.Text
|
||||||
|
private hcText!: Phaser.GameObjects.Text
|
||||||
|
private hoveredTile: { col: number; row: number } | null = null
|
||||||
|
private tileGraphics!: Phaser.GameObjects.Graphics
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({ key: 'GameScene' })
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): void {
|
||||||
|
this.manager = GameManager.getInstance()
|
||||||
|
this.manager.reset()
|
||||||
|
this.manager.gameState = 'playing'
|
||||||
|
|
||||||
|
// 渲染地图
|
||||||
|
this.tileGraphics = this.add.graphics()
|
||||||
|
drawAllTiles(this.tileGraphics, null)
|
||||||
|
|
||||||
|
// 地图装饰标签
|
||||||
|
renderMapLabels(this)
|
||||||
|
|
||||||
|
// HUD(在地图之上)
|
||||||
|
const hud = renderHUD(this)
|
||||||
|
this.kpiBar = hud.kpiBar
|
||||||
|
this.kpiText = hud.kpiText
|
||||||
|
this.hcText = hud.hcText
|
||||||
|
|
||||||
|
// 鼠标交互
|
||||||
|
this.setupInteraction()
|
||||||
|
|
||||||
|
// GameManager 事件回调 → 更新 HUD
|
||||||
|
this.manager.onKPIChange.push((kpi: number) => {
|
||||||
|
updateKPIBar(this.kpiBar, kpi)
|
||||||
|
this.kpiText.setText(`${kpi}%`)
|
||||||
|
})
|
||||||
|
this.manager.onHCChange.push((hc: number) => {
|
||||||
|
this.hcText.setText(`HC: ${hc}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册鼠标悬停 + 点击事件 */
|
||||||
|
private setupInteraction(): void {
|
||||||
|
const { cellW, cellH } = getCellSize()
|
||||||
|
|
||||||
|
this.input.on(
|
||||||
|
'pointermove',
|
||||||
|
(pointer: Phaser.Input.Pointer) => {
|
||||||
|
const col = Math.floor(pointer.x / cellW)
|
||||||
|
const row = Math.floor((pointer.y - HUD_HEIGHT) / cellH)
|
||||||
|
|
||||||
|
if (col >= 0 && col < MAP_COLS && row >= 0 && row < MAP_ROWS) {
|
||||||
|
if (!PATH_TILES.has(`${col},${row}`)) {
|
||||||
|
if (
|
||||||
|
!this.hoveredTile ||
|
||||||
|
this.hoveredTile.col !== col ||
|
||||||
|
this.hoveredTile.row !== row
|
||||||
|
) {
|
||||||
|
this.hoveredTile = { col, row }
|
||||||
|
drawAllTiles(this.tileGraphics, this.hoveredTile)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.hoveredTile !== null) {
|
||||||
|
this.hoveredTile = null
|
||||||
|
drawAllTiles(this.tileGraphics, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.input.on(
|
||||||
|
'pointerdown',
|
||||||
|
(pointer: Phaser.Input.Pointer) => {
|
||||||
|
const col = Math.floor(pointer.x / cellW)
|
||||||
|
const row = Math.floor((pointer.y - HUD_HEIGHT) / cellH)
|
||||||
|
|
||||||
|
if (
|
||||||
|
col >= 0 && col < MAP_COLS &&
|
||||||
|
row >= 0 && row < MAP_ROWS &&
|
||||||
|
!PATH_TILES.has(`${col},${row}`)
|
||||||
|
) {
|
||||||
|
this.showBuildPrompt(col, row, cellW, cellH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点击可建格子时的占位提示(Phase 1) */
|
||||||
|
private showBuildPrompt(
|
||||||
|
col: number,
|
||||||
|
row: number,
|
||||||
|
cellW: number,
|
||||||
|
cellH: number
|
||||||
|
): void {
|
||||||
|
const x = col * cellW + cellW / 2
|
||||||
|
const y = HUD_HEIGHT + row * cellH + cellH / 2
|
||||||
|
const tip = this.add
|
||||||
|
.text(x, y - 20, '建塔 (即将开放)', {
|
||||||
|
fontFamily: 'VT323, monospace',
|
||||||
|
fontSize: '18px',
|
||||||
|
color: '#F43F5E',
|
||||||
|
backgroundColor: '#0a1628',
|
||||||
|
padding: { x: 8, y: 4 },
|
||||||
|
})
|
||||||
|
.setOrigin(0.5, 1)
|
||||||
|
.setDepth(20)
|
||||||
|
|
||||||
|
this.time.delayedCall(1500, () => {
|
||||||
|
tip.destroy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 避免 unused variable 警告
|
||||||
|
void MAP_ROWS
|
||||||
|
void GAME_HEIGHT
|
||||||
|
void GAME_WIDTH
|
||||||
|
void BAR_X
|
||||||
|
void BAR_Y
|
||||||
|
void BAR_W
|
||||||
|
void BAR_H
|
||||||
|
|
||||||
|
return GameScene
|
||||||
|
}
|
||||||
26
game/config.ts
Normal file
26
game/config.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { GAME_WIDTH, GAME_HEIGHT } from './constants'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Phaser 游戏配置
|
||||||
|
* 注意:此文件只在客户端(动态 import 后)执行,不会在 SSR 中运行
|
||||||
|
* @param containerId HTML 容器元素的 id
|
||||||
|
*/
|
||||||
|
export function createGameConfig(containerId: string): Phaser.Types.Core.GameConfig {
|
||||||
|
return {
|
||||||
|
type: (globalThis as typeof globalThis & { Phaser?: { AUTO: number } }).Phaser?.AUTO ?? 0,
|
||||||
|
width: GAME_WIDTH,
|
||||||
|
height: GAME_HEIGHT,
|
||||||
|
parent: containerId,
|
||||||
|
backgroundColor: '#0a1628',
|
||||||
|
scale: {
|
||||||
|
mode: 3, // Phaser.Scale.FIT = 3
|
||||||
|
autoCenter: 1, // Phaser.Scale.CENTER_BOTH = 1
|
||||||
|
},
|
||||||
|
scene: [], // 场景在 GamePage 中动态注入
|
||||||
|
physics: {
|
||||||
|
default: 'arcade',
|
||||||
|
arcade: { debug: false },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
40
game/constants.ts
Normal file
40
game/constants.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// 游戏地图常量
|
||||||
|
export const MAP_COLS = 16
|
||||||
|
export const MAP_ROWS = 12
|
||||||
|
export const TILE_SIZE = 80 // 每格80px,总计 1280x960,Phaser 会缩放适配
|
||||||
|
export const GAME_WIDTH = 1280
|
||||||
|
export const GAME_HEIGHT = 720
|
||||||
|
|
||||||
|
// HUD 高度(顶部状态栏)
|
||||||
|
export const HUD_HEIGHT = 60
|
||||||
|
|
||||||
|
// S型路径折线关键坐标点
|
||||||
|
// 路径:从左侧(0,2) → 右至(11,2) → 下至(11,9) → 右至(15,9)
|
||||||
|
export const PATH_WAYPOINTS = [
|
||||||
|
{ x: 0, y: 2 },
|
||||||
|
{ x: 11, y: 2 },
|
||||||
|
{ x: 11, y: 9 },
|
||||||
|
{ x: 15, y: 9 },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
// 游戏初始数值
|
||||||
|
export const INITIAL_HC = 200
|
||||||
|
export const INITIAL_KPI = 100
|
||||||
|
export const STAMINA_MAX = 100
|
||||||
|
export const STAMINA_REGEN = 5 // 每秒恢复量
|
||||||
|
export const COFFEE_COST = 10 // 瑞幸咖啡 HC 成本
|
||||||
|
|
||||||
|
// 颜色常量(Phaser 使用 0x 十六进制)
|
||||||
|
export const COLOR_PATH = 0x3d2b1f // 路径格子:深褐色
|
||||||
|
export const COLOR_BUILDABLE = 0x1e3a5f // 可建塔格子:深蓝色
|
||||||
|
export const COLOR_HOVER = 0x2d5a8e // 悬停高亮:亮蓝色
|
||||||
|
export const COLOR_BORDER = 0x0a1628 // 格子边框:深夜蓝
|
||||||
|
export const COLOR_HUD_BG = 0x0a1628 // HUD 背景色
|
||||||
|
|
||||||
|
// 地图装饰性标记文字
|
||||||
|
export const MAP_LABELS: { col: number; row: number; text: string }[] = [
|
||||||
|
{ col: 2, row: 5, text: '面试间' },
|
||||||
|
{ col: 8, row: 5, text: '财务室' },
|
||||||
|
{ col: 4, row: 10, text: 'P8会议室' },
|
||||||
|
{ col: 13, row: 5, text: '茶水间' },
|
||||||
|
]
|
||||||
179
game/mapRenderer.ts
Normal file
179
game/mapRenderer.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import {
|
||||||
|
MAP_COLS,
|
||||||
|
MAP_ROWS,
|
||||||
|
GAME_WIDTH,
|
||||||
|
HUD_HEIGHT,
|
||||||
|
PATH_WAYPOINTS,
|
||||||
|
COLOR_PATH,
|
||||||
|
COLOR_BUILDABLE,
|
||||||
|
COLOR_HOVER,
|
||||||
|
COLOR_BORDER,
|
||||||
|
MAP_LABELS,
|
||||||
|
INITIAL_KPI,
|
||||||
|
} from './constants'
|
||||||
|
|
||||||
|
/** 将折线关键坐标点展开为完整路径格子集合 */
|
||||||
|
export function buildPathTiles(
|
||||||
|
waypoints: readonly { x: number; y: number }[]
|
||||||
|
): Set<string> {
|
||||||
|
const tiles = new Set<string>()
|
||||||
|
for (let i = 0; i < waypoints.length - 1; i++) {
|
||||||
|
const from = waypoints[i]
|
||||||
|
const to = waypoints[i + 1]
|
||||||
|
if (from.x === to.x) {
|
||||||
|
const minY = Math.min(from.y, to.y)
|
||||||
|
const maxY = Math.max(from.y, to.y)
|
||||||
|
for (let y = minY; y <= maxY; y++) tiles.add(`${from.x},${y}`)
|
||||||
|
} else {
|
||||||
|
const minX = Math.min(from.x, to.x)
|
||||||
|
const maxX = Math.max(from.x, to.x)
|
||||||
|
for (let x = minX; x <= maxX; x++) tiles.add(`${x},${from.y}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tiles
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PATH_TILES = buildPathTiles(PATH_WAYPOINTS)
|
||||||
|
|
||||||
|
/** 计算格子尺寸 */
|
||||||
|
export function getCellSize() {
|
||||||
|
const cellW = Math.floor(GAME_WIDTH / MAP_COLS)
|
||||||
|
const cellH = Math.floor((720 - HUD_HEIGHT) / MAP_ROWS)
|
||||||
|
return { cellW, cellH }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制所有地图格子
|
||||||
|
*/
|
||||||
|
export function drawAllTiles(
|
||||||
|
g: Phaser.GameObjects.Graphics,
|
||||||
|
hovered: { col: number; row: number } | null
|
||||||
|
): void {
|
||||||
|
const { cellW, cellH } = getCellSize()
|
||||||
|
g.clear()
|
||||||
|
|
||||||
|
for (let row = 0; row < MAP_ROWS; row++) {
|
||||||
|
for (let col = 0; col < MAP_COLS; col++) {
|
||||||
|
const isPath = PATH_TILES.has(`${col},${row}`)
|
||||||
|
const isHovered =
|
||||||
|
!isPath && hovered !== null &&
|
||||||
|
hovered.col === col && hovered.row === row
|
||||||
|
|
||||||
|
const fillColor = isPath
|
||||||
|
? COLOR_PATH
|
||||||
|
: isHovered ? COLOR_HOVER : COLOR_BUILDABLE
|
||||||
|
|
||||||
|
const x = col * cellW
|
||||||
|
const y = HUD_HEIGHT + row * cellH
|
||||||
|
|
||||||
|
g.fillStyle(fillColor, 1)
|
||||||
|
g.fillRect(x + 1, y + 1, cellW - 2, cellH - 2)
|
||||||
|
g.lineStyle(1, COLOR_BORDER, 0.6)
|
||||||
|
g.strokeRect(x, y, cellW, cellH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染地图装饰性标签
|
||||||
|
*/
|
||||||
|
export function renderMapLabels(scene: Phaser.Scene): void {
|
||||||
|
const { cellW, cellH } = getCellSize()
|
||||||
|
for (const label of MAP_LABELS) {
|
||||||
|
const x = label.col * cellW + cellW / 2
|
||||||
|
const y = HUD_HEIGHT + label.row * cellH + cellH / 2
|
||||||
|
scene.add
|
||||||
|
.text(x, y, label.text, {
|
||||||
|
fontFamily: 'VT323, monospace',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#A78BFA',
|
||||||
|
})
|
||||||
|
.setOrigin(0.5, 0.5)
|
||||||
|
.setAlpha(0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** HUD 进度条参数 */
|
||||||
|
export const BAR_W = 300
|
||||||
|
export const BAR_H = 16
|
||||||
|
export const BAR_X = (GAME_WIDTH - BAR_W) / 2
|
||||||
|
export const BAR_Y = (HUD_HEIGHT - BAR_H) / 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染顶部 HUD 区域,返回可更新的 Graphics 和 Text 对象
|
||||||
|
*/
|
||||||
|
export function renderHUD(scene: Phaser.Scene): {
|
||||||
|
kpiBar: Phaser.GameObjects.Graphics
|
||||||
|
kpiText: Phaser.GameObjects.Text
|
||||||
|
hcText: Phaser.GameObjects.Text
|
||||||
|
} {
|
||||||
|
// HUD 背景
|
||||||
|
const hudBg = scene.add.graphics()
|
||||||
|
hudBg.fillStyle(0x0a1628, 0.92)
|
||||||
|
hudBg.fillRect(0, 0, GAME_WIDTH, HUD_HEIGHT)
|
||||||
|
hudBg.lineStyle(1, 0x1e3a5f, 1)
|
||||||
|
hudBg.strokeRect(0, 0, GAME_WIDTH, HUD_HEIGHT)
|
||||||
|
|
||||||
|
// 左侧标题
|
||||||
|
scene.add.text(16, HUD_HEIGHT / 2, '大厂保卫战', {
|
||||||
|
fontFamily: "'Press Start 2P', monospace",
|
||||||
|
fontSize: '10px',
|
||||||
|
color: '#A78BFA',
|
||||||
|
}).setOrigin(0, 0.5)
|
||||||
|
|
||||||
|
// KPI 标签
|
||||||
|
scene.add.text(BAR_X - 8, HUD_HEIGHT / 2, 'KPI', {
|
||||||
|
fontFamily: "'Press Start 2P', monospace",
|
||||||
|
fontSize: '8px',
|
||||||
|
color: '#E2E8F0',
|
||||||
|
}).setOrigin(1, 0.5)
|
||||||
|
|
||||||
|
// 进度条背景轨道
|
||||||
|
const trackBar = scene.add.graphics()
|
||||||
|
trackBar.fillStyle(0x1a1a2e, 1)
|
||||||
|
trackBar.fillRect(BAR_X, BAR_Y, BAR_W, BAR_H)
|
||||||
|
trackBar.lineStyle(1, 0x7c3aed, 0.6)
|
||||||
|
trackBar.strokeRect(BAR_X, BAR_Y, BAR_W, BAR_H)
|
||||||
|
|
||||||
|
// KPI 进度前景
|
||||||
|
const kpiBar = scene.add.graphics()
|
||||||
|
updateKPIBar(kpiBar, INITIAL_KPI)
|
||||||
|
|
||||||
|
// KPI 百分比文字
|
||||||
|
const kpiText = scene.add.text(
|
||||||
|
BAR_X + BAR_W / 2, HUD_HEIGHT / 2,
|
||||||
|
`${INITIAL_KPI}%`,
|
||||||
|
{
|
||||||
|
fontFamily: "'Press Start 2P', monospace",
|
||||||
|
fontSize: '8px',
|
||||||
|
color: '#E2E8F0',
|
||||||
|
}
|
||||||
|
).setOrigin(0.5, 0.5).setDepth(1)
|
||||||
|
|
||||||
|
// 右侧 HC 数值
|
||||||
|
const hcText = scene.add.text(
|
||||||
|
GAME_WIDTH - 16, HUD_HEIGHT / 2,
|
||||||
|
`HC: 200`,
|
||||||
|
{
|
||||||
|
fontFamily: "'Press Start 2P', monospace",
|
||||||
|
fontSize: '10px',
|
||||||
|
color: '#A78BFA',
|
||||||
|
}
|
||||||
|
).setOrigin(1, 0.5)
|
||||||
|
|
||||||
|
return { kpiBar, kpiText, hcText }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 KPI 进度条(颜色随数值变化:绿→黄→红)
|
||||||
|
*/
|
||||||
|
export function updateKPIBar(
|
||||||
|
kpiBar: Phaser.GameObjects.Graphics,
|
||||||
|
kpi: number
|
||||||
|
): void {
|
||||||
|
kpiBar.clear()
|
||||||
|
const color = kpi > 60 ? 0x22c55e : kpi > 30 ? 0xf59e0b : 0xef4444
|
||||||
|
kpiBar.fillStyle(color, 1)
|
||||||
|
kpiBar.fillRect(BAR_X + 1, BAR_Y + 1, (BAR_W - 2) * (kpi / 100), BAR_H - 2)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user