feat(game): 实现精灵图渲染优化游戏性能
- 新增 AnimalSpriteImage 组件用于精灵图渲染 - 集成精灵图渲染到桌面版动物组件 - 集成精灵图渲染到移动版动物组件 - 重构历史记录组件使用精灵图渲染 - 更新动物覆盖层组件使用精灵图渲染 - 移除旧的单个图片资源引用 - 添加精灵图生成脚本和相关配置 - 优化启动资源加载排除精灵图源文件 - 添加精灵图位置计算工具函数
This commit is contained in:
@@ -7,6 +7,9 @@
|
|||||||
"commit": "cz",
|
"commit": "cz",
|
||||||
"dev": "pnpm generate-routes && vite",
|
"dev": "pnpm generate-routes && vite",
|
||||||
"build": "pnpm generate-routes && tsc -b && vite build",
|
"build": "pnpm generate-routes && tsc -b && vite build",
|
||||||
|
"generate-animal-sprite": "node scripts/generate-animal-sprite.mjs",
|
||||||
|
"generate-reward-sprite": "node scripts/generate-reward-sprite.mjs",
|
||||||
|
"generate-sprites": "pnpm generate-animal-sprite && pnpm generate-reward-sprite",
|
||||||
"generate-routes": "tsr generate",
|
"generate-routes": "tsr generate",
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
"lint:fix": "biome check --write .",
|
"lint:fix": "biome check --write .",
|
||||||
@@ -74,6 +77,7 @@
|
|||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"lint-staged": "16.4.0",
|
"lint-staged": "16.4.0",
|
||||||
"postcss": "^8.5.12",
|
"postcss": "^8.5.12",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"vite": "^8.0.4"
|
"vite": "^8.0.4"
|
||||||
|
|||||||
287
pnpm-lock.yaml
generated
287
pnpm-lock.yaml
generated
@@ -150,6 +150,9 @@ importers:
|
|||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.5.12
|
specifier: ^8.5.12
|
||||||
version: 8.5.12
|
version: 8.5.12
|
||||||
|
sharp:
|
||||||
|
specifier: ^0.34.5
|
||||||
|
version: 0.34.5
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.2.2
|
specifier: ^4.2.2
|
||||||
version: 4.2.2
|
version: 4.2.2
|
||||||
@@ -637,6 +640,159 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react-hook-form: ^7.55.0
|
react-hook-form: ^7.55.0
|
||||||
|
|
||||||
|
'@img/colour@1.1.0':
|
||||||
|
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||||
|
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||||
|
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.34.5':
|
||||||
|
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-ppc64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-riscv64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.34.5':
|
||||||
|
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.34.5':
|
||||||
|
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [wasm32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.34.5':
|
||||||
|
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@inquirer/ansi@2.0.5':
|
'@inquirer/ansi@2.0.5':
|
||||||
resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==}
|
resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==}
|
||||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||||
@@ -3432,6 +3588,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-70fwnesNrY1GgeD7Kdzn+3SsYeyfibm8immsA5L68+OusoPTvYF01oWExl8/latKpMpvVXcbgdbbE6VFBJQ38w==}
|
resolution: {integrity: sha512-70fwnesNrY1GgeD7Kdzn+3SsYeyfibm8immsA5L68+OusoPTvYF01oWExl8/latKpMpvVXcbgdbbE6VFBJQ38w==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
sharp@0.34.5:
|
||||||
|
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4335,6 +4495,102 @@ snapshots:
|
|||||||
'@standard-schema/utils': 0.3.0
|
'@standard-schema/utils': 0.3.0
|
||||||
react-hook-form: 7.75.0(react@19.2.5)
|
react-hook-form: 7.75.0(react@19.2.5)
|
||||||
|
|
||||||
|
'@img/colour@1.1.0': {}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-ppc64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-riscv64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.34.5':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/runtime': 1.9.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.34.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.34.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.34.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@inquirer/ansi@2.0.5': {}
|
'@inquirer/ansi@2.0.5': {}
|
||||||
|
|
||||||
'@inquirer/confirm@6.0.12(@types/node@24.12.2)':
|
'@inquirer/confirm@6.0.12(@types/node@24.12.2)':
|
||||||
@@ -7148,6 +7404,37 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
|
sharp@0.34.5:
|
||||||
|
dependencies:
|
||||||
|
'@img/colour': 1.1.0
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
semver: 7.7.4
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-darwin-arm64': 0.34.5
|
||||||
|
'@img/sharp-darwin-x64': 0.34.5
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||||
|
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||||
|
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||||
|
'@img/sharp-linux-arm': 0.34.5
|
||||||
|
'@img/sharp-linux-arm64': 0.34.5
|
||||||
|
'@img/sharp-linux-ppc64': 0.34.5
|
||||||
|
'@img/sharp-linux-riscv64': 0.34.5
|
||||||
|
'@img/sharp-linux-s390x': 0.34.5
|
||||||
|
'@img/sharp-linux-x64': 0.34.5
|
||||||
|
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||||
|
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||||
|
'@img/sharp-wasm32': 0.34.5
|
||||||
|
'@img/sharp-win32-arm64': 0.34.5
|
||||||
|
'@img/sharp-win32-ia32': 0.34.5
|
||||||
|
'@img/sharp-win32-x64': 0.34.5
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
shebang-regex: 3.0.0
|
shebang-regex: 3.0.0
|
||||||
|
|||||||
53
scripts/generate-animal-sprite.mjs
Normal file
53
scripts/generate-animal-sprite.mjs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import sharp from 'sharp'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const rootDir = path.resolve(__dirname, '..')
|
||||||
|
const animalDir = path.join(rootDir, 'src/assets/animal')
|
||||||
|
const outputPath = path.join(animalDir, 'animal-sprite.webp')
|
||||||
|
|
||||||
|
const columns = 6
|
||||||
|
const rows = 6
|
||||||
|
const cellWidth = 446
|
||||||
|
const cellHeight = 224
|
||||||
|
const quality = 82
|
||||||
|
|
||||||
|
const composites = await Promise.all(
|
||||||
|
Array.from({ length: columns * rows }, async (_, index) => {
|
||||||
|
const id = index + 1
|
||||||
|
const input = path.join(animalDir, `${id}.webp`)
|
||||||
|
const left = (index % columns) * cellWidth
|
||||||
|
const top = Math.floor(index / columns) * cellHeight
|
||||||
|
|
||||||
|
const buffer = await sharp(input)
|
||||||
|
.resize(cellWidth, cellHeight, {
|
||||||
|
fit: 'cover',
|
||||||
|
position: 'center',
|
||||||
|
})
|
||||||
|
.webp({ quality })
|
||||||
|
.toBuffer()
|
||||||
|
|
||||||
|
return {
|
||||||
|
input: buffer,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await sharp({
|
||||||
|
create: {
|
||||||
|
width: columns * cellWidth,
|
||||||
|
height: rows * cellHeight,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.composite(composites)
|
||||||
|
.webp({ quality })
|
||||||
|
.toFile(outputPath)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Generated ${path.relative(rootDir, outputPath)} (${columns}x${rows}, ${cellWidth}x${cellHeight})`,
|
||||||
|
)
|
||||||
53
scripts/generate-reward-sprite.mjs
Normal file
53
scripts/generate-reward-sprite.mjs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import sharp from 'sharp'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const rootDir = path.resolve(__dirname, '..')
|
||||||
|
const rewardDir = path.join(rootDir, 'src/assets/reward')
|
||||||
|
const outputPath = path.join(rewardDir, 'reward-sprite.webp')
|
||||||
|
|
||||||
|
const columns = 6
|
||||||
|
const rows = 6
|
||||||
|
const cellWidth = 102
|
||||||
|
const cellHeight = 98
|
||||||
|
const quality = 86
|
||||||
|
|
||||||
|
const composites = await Promise.all(
|
||||||
|
Array.from({ length: columns * rows }, async (_, index) => {
|
||||||
|
const id = index + 1
|
||||||
|
const input = path.join(rewardDir, `${id}.webp`)
|
||||||
|
const left = (index % columns) * cellWidth
|
||||||
|
const top = Math.floor(index / columns) * cellHeight
|
||||||
|
|
||||||
|
const buffer = await sharp(input)
|
||||||
|
.resize(cellWidth, cellHeight, {
|
||||||
|
fit: 'contain',
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||||
|
})
|
||||||
|
.webp({ quality })
|
||||||
|
.toBuffer()
|
||||||
|
|
||||||
|
return {
|
||||||
|
input: buffer,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await sharp({
|
||||||
|
create: {
|
||||||
|
width: columns * cellWidth,
|
||||||
|
height: rows * cellHeight,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.composite(composites)
|
||||||
|
.webp({ quality })
|
||||||
|
.toFile(outputPath)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Generated ${path.relative(rootDir, outputPath)} (${columns}x${rows}, ${cellWidth}x${cellHeight})`,
|
||||||
|
)
|
||||||
BIN
src/assets/animal/animal-sprite.webp
Normal file
BIN
src/assets/animal/animal-sprite.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 358 KiB |
BIN
src/assets/reward/reward-sprite.webp
Normal file
BIN
src/assets/reward/reward-sprite.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
@@ -2,7 +2,15 @@ import { type PropsWithChildren, useEffect, useMemo, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const bootImageModules = import.meta.glob(
|
const bootImageModules = import.meta.glob(
|
||||||
|
[
|
||||||
'../assets/**/*.{jpg,jpeg,png,svg,webp}',
|
'../assets/**/*.{jpg,jpeg,png,svg,webp}',
|
||||||
|
'!../assets/animal/[1-9].webp',
|
||||||
|
'!../assets/animal/[12][0-9].webp',
|
||||||
|
'!../assets/animal/3[0-6].webp',
|
||||||
|
'!../assets/reward/[1-9].webp',
|
||||||
|
'!../assets/reward/[12][0-9].webp',
|
||||||
|
'!../assets/reward/3[0-6].webp',
|
||||||
|
],
|
||||||
{
|
{
|
||||||
eager: true,
|
eager: true,
|
||||||
import: 'default',
|
import: 'default',
|
||||||
@@ -12,6 +20,8 @@ const bootImageModules = import.meta.glob(
|
|||||||
const BOOT_MIN_VISIBLE_MS = 900
|
const BOOT_MIN_VISIBLE_MS = 900
|
||||||
const PC_LOADING_BG_URL = '/loading/pc-loading-bg.webp'
|
const PC_LOADING_BG_URL = '/loading/pc-loading-bg.webp'
|
||||||
const MOBILE_LOADING_BG_URL = '/loading/mobile-loading-bg.webp'
|
const MOBILE_LOADING_BG_URL = '/loading/mobile-loading-bg.webp'
|
||||||
|
const SPRITE_SOURCE_IMAGE_PATTERN =
|
||||||
|
/^\.\.\/assets\/(?:animal|reward)\/(?:[1-9]|[12]\d|3[0-6])\.webp$/
|
||||||
|
|
||||||
function preloadImage(url: string) {
|
function preloadImage(url: string) {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
@@ -33,7 +43,16 @@ function waitForFonts() {
|
|||||||
|
|
||||||
function useBootResourceLoader() {
|
function useBootResourceLoader() {
|
||||||
const imageUrls = useMemo(
|
const imageUrls = useMemo(
|
||||||
() => Array.from(new Set(Object.values(bootImageModules))).filter(Boolean),
|
() =>
|
||||||
|
Array.from(
|
||||||
|
new Set(
|
||||||
|
Object.entries(bootImageModules)
|
||||||
|
.filter(
|
||||||
|
([sourcePath]) => !SPRITE_SOURCE_IMAGE_PATTERN.test(sourcePath),
|
||||||
|
)
|
||||||
|
.map(([, url]) => url),
|
||||||
|
),
|
||||||
|
).filter(Boolean),
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
const [progress, setProgress] = useState(0)
|
const [progress, setProgress] = useState(0)
|
||||||
|
|||||||
@@ -16,10 +16,8 @@ import { LottiePlayer } from '@/components/lottie-player.tsx'
|
|||||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||||
import { SmartImage } from '@/components/smart-image'
|
import { SmartImage } from '@/components/smart-image'
|
||||||
import { REWARD_OVERLAY_DURATION_MS } from '@/constants'
|
import { REWARD_OVERLAY_DURATION_MS } from '@/constants'
|
||||||
import {
|
import { AnimalSpriteImage } from '@/features/game/components/shared/animal-sprite-image'
|
||||||
FLOWER_IMAGE_BY_ID,
|
import { groupSelectionsByCell } from '@/features/game/shared'
|
||||||
groupSelectionsByCell,
|
|
||||||
} from '@/features/game/shared'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
import { useGameAutoHostingStore, useGameRoundStore } from '@/store/game'
|
import { useGameAutoHostingStore, useGameRoundStore } from '@/store/game'
|
||||||
@@ -34,7 +32,6 @@ type RewardChildrenStage = 'hidden' | 'visible' | 'exiting'
|
|||||||
type StopBetItem = {
|
type StopBetItem = {
|
||||||
amount: number
|
amount: number
|
||||||
cellId: number
|
cellId: number
|
||||||
imageUrl: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DesktopAnimalOverlayProps {
|
interface DesktopAnimalOverlayProps {
|
||||||
@@ -115,7 +112,6 @@ function getStopBetItems(selections: BetSelection[]) {
|
|||||||
return {
|
return {
|
||||||
amount: meta.amount,
|
amount: meta.amount,
|
||||||
cellId: normalizedCellId,
|
cellId: normalizedCellId,
|
||||||
imageUrl: FLOWER_IMAGE_BY_ID[normalizedCellId]?.animalUrl ?? '',
|
|
||||||
} satisfies StopBetItem
|
} satisfies StopBetItem
|
||||||
})
|
})
|
||||||
.sort((left, right) => left.cellId - right.cellId)
|
.sort((left, right) => left.cellId - right.cellId)
|
||||||
@@ -144,15 +140,11 @@ function StopBetSummary({
|
|||||||
className="flex h-design-90 min-w-design-230 items-center gap-design-10 overflow-hidden rounded-[calc(var(--design-unit)*12)] border border-[rgba(124,232,255,0.2)] bg-[linear-gradient(90deg,rgba(7,42,38,0.94),rgba(5,23,34,0.96)_44%,rgba(18,25,25,0.94))] px-design-8 shadow-[0_0_calc(var(--design-unit)*16)_rgba(51,231,255,0.16),inset_0_calc(var(--design-unit)*1)_0_rgba(255,255,255,0.08)]"
|
className="flex h-design-90 min-w-design-230 items-center gap-design-10 overflow-hidden rounded-[calc(var(--design-unit)*12)] border border-[rgba(124,232,255,0.2)] bg-[linear-gradient(90deg,rgba(7,42,38,0.94),rgba(5,23,34,0.96)_44%,rgba(18,25,25,0.94))] px-design-8 shadow-[0_0_calc(var(--design-unit)*16)_rgba(51,231,255,0.16),inset_0_calc(var(--design-unit)*1)_0_rgba(255,255,255,0.08)]"
|
||||||
>
|
>
|
||||||
<div className="relative h-design-72 w-design-72 shrink-0 overflow-hidden rounded-[calc(var(--design-unit)*10)] bg-[rgba(1,8,13,0.84)] shadow-[0_0_calc(var(--design-unit)*12)_rgba(86,247,255,0.18)]">
|
<div className="relative h-design-72 w-design-72 shrink-0 overflow-hidden rounded-[calc(var(--design-unit)*10)] bg-[rgba(1,8,13,0.84)] shadow-[0_0_calc(var(--design-unit)*12)_rgba(86,247,255,0.18)]">
|
||||||
{item.imageUrl ? (
|
<AnimalSpriteImage
|
||||||
<SmartImage
|
animalId={item.cellId}
|
||||||
src={item.imageUrl}
|
fit="cover"
|
||||||
alt=""
|
|
||||||
showSkeleton={false}
|
|
||||||
className="h-full w-full"
|
className="h-full w-full"
|
||||||
imgClassName="object-cover"
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-design-72 min-w-0 flex-1 flex-col items-stretch justify-center gap-design-7 leading-none">
|
<div className="flex h-design-72 min-w-0 flex-1 flex-col items-stretch justify-center gap-design-7 leading-none">
|
||||||
<div className="flex h-design-28 w-full items-center justify-center rounded-full bg-[rgba(4,31,31,0.9)] text-design-24 font-black leading-none text-[#78FF7F] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(120,255,127,0.08)] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(120,255,127,0.52)]">
|
<div className="flex h-design-28 w-full items-center justify-center rounded-full bg-[rgba(4,31,31,0.9)] text-design-24 font-black leading-none text-[#78FF7F] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(120,255,127,0.08)] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(120,255,127,0.52)]">
|
||||||
@@ -189,8 +181,6 @@ function NoRewardSettlement({
|
|||||||
resultLabel: string
|
resultLabel: string
|
||||||
resultNumber: number
|
resultNumber: number
|
||||||
}) {
|
}) {
|
||||||
const winningImageUrl = FLOWER_IMAGE_BY_ID[resultNumber]?.animalUrl ?? ''
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex w-design-720 max-w-[84%] flex-col items-center justify-center gap-design-18 rounded-[calc(var(--design-unit)*22)] border border-[rgba(124,232,255,0.34)] bg-[linear-gradient(180deg,rgba(7,28,40,0.96),rgba(3,12,20,0.98))] px-design-28 py-design-24 text-center shadow-[0_0_calc(var(--design-unit)*28)_rgba(70,245,255,0.28),0_0_calc(var(--design-unit)*64)_rgba(20,112,210,0.18),inset_0_0_calc(var(--design-unit)*24)_rgba(124,232,255,0.12)]">
|
<div className="relative z-10 flex w-design-720 max-w-[84%] flex-col items-center justify-center gap-design-18 rounded-[calc(var(--design-unit)*22)] border border-[rgba(124,232,255,0.34)] bg-[linear-gradient(180deg,rgba(7,28,40,0.96),rgba(3,12,20,0.98))] px-design-28 py-design-24 text-center shadow-[0_0_calc(var(--design-unit)*28)_rgba(70,245,255,0.28),0_0_calc(var(--design-unit)*64)_rgba(20,112,210,0.18),inset_0_0_calc(var(--design-unit)*24)_rgba(124,232,255,0.12)]">
|
||||||
<div className="text-design-34 font-black tracking-[0.14em] text-[#D8FBFF] [text-shadow:0_0_calc(var(--design-unit)*16)_rgba(84,245,255,0.42)]">
|
<div className="text-design-34 font-black tracking-[0.14em] text-[#D8FBFF] [text-shadow:0_0_calc(var(--design-unit)*16)_rgba(84,245,255,0.42)]">
|
||||||
@@ -203,14 +193,11 @@ function NoRewardSettlement({
|
|||||||
{resultLabel}
|
{resultLabel}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative h-design-76 w-design-76 overflow-hidden rounded-[calc(var(--design-unit)*10)] border border-[rgba(120,255,127,0.36)] bg-[rgba(1,8,13,0.84)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(120,255,127,0.2)]">
|
<div className="relative h-design-76 w-design-76 overflow-hidden rounded-[calc(var(--design-unit)*10)] border border-[rgba(120,255,127,0.36)] bg-[rgba(1,8,13,0.84)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(120,255,127,0.2)]">
|
||||||
{winningImageUrl ? (
|
<AnimalSpriteImage
|
||||||
<img
|
animalId={resultNumber}
|
||||||
src={winningImageUrl}
|
fit="cover"
|
||||||
alt=""
|
className="h-full w-full"
|
||||||
aria-hidden="true"
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-full border border-[rgba(120,255,127,0.38)] bg-[rgba(4,31,31,0.78)] px-design-16 py-design-4 text-design-26 font-black leading-none text-[#78FF7F] [text-shadow:0_0_calc(var(--design-unit)*12)_rgba(120,255,127,0.52)]">
|
<div className="rounded-full border border-[rgba(120,255,127,0.38)] bg-[rgba(4,31,31,0.78)] px-design-16 py-design-4 text-design-26 font-black leading-none text-[#78FF7F] [text-shadow:0_0_calc(var(--design-unit)*12)_rgba(120,255,127,0.52)]">
|
||||||
{String(resultNumber).padStart(2, '0')}
|
{String(resultNumber).padStart(2, '0')}
|
||||||
@@ -228,14 +215,11 @@ function NoRewardSettlement({
|
|||||||
className="flex h-design-104 w-design-88 flex-col items-center justify-center gap-design-4 overflow-hidden rounded-[calc(var(--design-unit)*10)] border border-[rgba(124,232,255,0.22)] bg-[linear-gradient(180deg,rgba(8,34,42,0.9),rgba(3,13,20,0.96))] p-design-4"
|
className="flex h-design-104 w-design-88 flex-col items-center justify-center gap-design-4 overflow-hidden rounded-[calc(var(--design-unit)*10)] border border-[rgba(124,232,255,0.22)] bg-[linear-gradient(180deg,rgba(8,34,42,0.9),rgba(3,13,20,0.96))] p-design-4"
|
||||||
>
|
>
|
||||||
<div className="relative h-design-76 w-design-76 overflow-hidden rounded-[calc(var(--design-unit)*10)] bg-[rgba(1,8,13,0.84)]">
|
<div className="relative h-design-76 w-design-76 overflow-hidden rounded-[calc(var(--design-unit)*10)] bg-[rgba(1,8,13,0.84)]">
|
||||||
{item.imageUrl ? (
|
<AnimalSpriteImage
|
||||||
<img
|
animalId={item.cellId}
|
||||||
src={item.imageUrl}
|
fit="cover"
|
||||||
alt=""
|
className="h-full w-full"
|
||||||
aria-hidden="true"
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-design-20 font-black leading-none text-[#4BFFFE] [text-shadow:0_0_calc(var(--design-unit)*8)_rgba(75,255,254,0.46)]">
|
<div className="text-design-20 font-black leading-none text-[#4BFFFE] [text-shadow:0_0_calc(var(--design-unit)*8)_rgba(75,255,254,0.46)]">
|
||||||
{String(item.cellId).padStart(2, '0')}
|
{String(item.cellId).padStart(2, '0')}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import animalBorderImage from '@/assets/game/animal-border.webp'
|
|||||||
import diamondIcon from '@/assets/system/diamond.webp'
|
import diamondIcon from '@/assets/system/diamond.webp'
|
||||||
import { SmartImage } from '@/components/smart-image'
|
import { SmartImage } from '@/components/smart-image'
|
||||||
import { DesktopAnimalOverlay } from '@/features/game/components/desktop/desktop-animal-overlay.tsx'
|
import { DesktopAnimalOverlay } from '@/features/game/components/desktop/desktop-animal-overlay.tsx'
|
||||||
|
import { AnimalSpriteImage } from '@/features/game/components/shared/animal-sprite-image'
|
||||||
import { RoundBettingStartAlert } from '@/features/game/components/shared/round-betting-start-alert.tsx'
|
import { RoundBettingStartAlert } from '@/features/game/components/shared/round-betting-start-alert.tsx'
|
||||||
import { FLOWER_IMAGE_LIST } from '@/features/game/shared'
|
import { FLOWER_IMAGE_LIST } from '@/features/game/shared'
|
||||||
import { useAnimalVm } from '@/hooks/use-animal-vm'
|
import { useAnimalVm } from '@/hooks/use-animal-vm'
|
||||||
@@ -367,8 +368,8 @@ export function DesktopAnimal({
|
|||||||
className="pointer-events-none absolute inset-[calc(var(--design-unit)*2)] z-20 rounded-[calc(var(--design-unit)*15)] bg-[rgba(4,16,24,0.52)] shadow-[inset_0_0_calc(var(--design-unit)*20)_rgba(3,9,14,0.56)]"
|
className="pointer-events-none absolute inset-[calc(var(--design-unit)*2)] z-20 rounded-[calc(var(--design-unit)*15)] bg-[rgba(4,16,24,0.52)] shadow-[inset_0_0_calc(var(--design-unit)*20)_rgba(3,9,14,0.56)]"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<SmartImage
|
<AnimalSpriteImage
|
||||||
src={item.animalUrl}
|
animalId={item.id}
|
||||||
alt={`animal-${item.id}`}
|
alt={`animal-${item.id}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute left-[1.5%] right-[1.5%] top-[2.9%] bottom-[2.9%] z-10 overflow-hidden rounded-[calc(var(--design-unit)*14)]',
|
'absolute left-[1.5%] right-[1.5%] top-[2.9%] bottom-[2.9%] z-10 overflow-hidden rounded-[calc(var(--design-unit)*14)]',
|
||||||
@@ -378,7 +379,6 @@ export function DesktopAnimal({
|
|||||||
'brightness-110 saturate-120 drop-shadow-[0_0_calc(var(--design-unit)*9)_rgba(106,250,255,0.44)]',
|
'brightness-110 saturate-120 drop-shadow-[0_0_calc(var(--design-unit)*9)_rgba(106,250,255,0.44)]',
|
||||||
imageClassName,
|
imageClassName,
|
||||||
)}
|
)}
|
||||||
imgClassName="object-fill"
|
|
||||||
/>
|
/>
|
||||||
{showCellWarning ? (
|
{showCellWarning ? (
|
||||||
<motion.span
|
<motion.span
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import { useCallback, useRef } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import historyBg from '@/assets/system/history-bg.png'
|
import historyBg from '@/assets/system/history-bg.png'
|
||||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||||
import { SmartImage } from '@/components/smart-image'
|
|
||||||
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
||||||
import { FLOWER_IMAGE_BY_ID } from '@/features/game/shared'
|
import { RewardSpriteImage } from '@/features/game/components/shared/reward-sprite-image'
|
||||||
import { useGameHistoryVm } from '@/hooks/use-game-history-vm.ts'
|
import { useGameHistoryVm } from '@/hooks/use-game-history-vm.ts'
|
||||||
|
|
||||||
function HistoryRewardNumber({
|
function HistoryRewardNumber({
|
||||||
@@ -15,10 +14,9 @@ function HistoryRewardNumber({
|
|||||||
className?: string
|
className?: string
|
||||||
number: number
|
number: number
|
||||||
}) {
|
}) {
|
||||||
const image = FLOWER_IMAGE_BY_ID[number]
|
|
||||||
const label = String(number).padStart(2, '0')
|
const label = String(number).padStart(2, '0')
|
||||||
|
|
||||||
if (!image?.rewardUrl) {
|
if (!Number.isInteger(number) || number < 1 || number > 36) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-flex h-design-38 min-w-design-38 shrink-0 items-center justify-center px-design-5 text-design-15 font-bold text-[#D5FBFF] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(75,233,255,0.12),0_0_calc(var(--design-unit)*10)_rgba(75,233,255,0.1)] align-middle ${className ?? ''}`}
|
className={`inline-flex h-design-38 min-w-design-38 shrink-0 items-center justify-center px-design-5 text-design-15 font-bold text-[#D5FBFF] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(75,233,255,0.12),0_0_calc(var(--design-unit)*10)_rgba(75,233,255,0.1)] align-middle ${className ?? ''}`}
|
||||||
@@ -33,12 +31,10 @@ function HistoryRewardNumber({
|
|||||||
className={`inline-flex h-design-38 w-design-38 shrink-0 items-center justify-center p-design-1 align-middle ${className ?? ''}`}
|
className={`inline-flex h-design-38 w-design-38 shrink-0 items-center justify-center p-design-1 align-middle ${className ?? ''}`}
|
||||||
title={label}
|
title={label}
|
||||||
>
|
>
|
||||||
<SmartImage
|
<RewardSpriteImage
|
||||||
src={image.rewardUrl}
|
rewardId={number}
|
||||||
alt={label}
|
alt={label}
|
||||||
showSkeleton={false}
|
className="h-full w-full"
|
||||||
className="h-full w-full overflow-visible"
|
|
||||||
imgClassName="object-contain"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,10 +16,8 @@ import { LottiePlayer } from '@/components/lottie-player.tsx'
|
|||||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||||
import { SmartImage } from '@/components/smart-image'
|
import { SmartImage } from '@/components/smart-image'
|
||||||
import { REWARD_OVERLAY_DURATION_MS } from '@/constants'
|
import { REWARD_OVERLAY_DURATION_MS } from '@/constants'
|
||||||
import {
|
import { AnimalSpriteImage } from '@/features/game/components/shared/animal-sprite-image'
|
||||||
FLOWER_IMAGE_BY_ID,
|
import { groupSelectionsByCell } from '@/features/game/shared'
|
||||||
groupSelectionsByCell,
|
|
||||||
} from '@/features/game/shared'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
import { useGameAutoHostingStore, useGameRoundStore } from '@/store/game'
|
import { useGameAutoHostingStore, useGameRoundStore } from '@/store/game'
|
||||||
@@ -34,7 +32,6 @@ type RewardChildrenStage = 'hidden' | 'visible' | 'exiting'
|
|||||||
type StopBetItem = {
|
type StopBetItem = {
|
||||||
amount: number
|
amount: number
|
||||||
cellId: number
|
cellId: number
|
||||||
imageUrl: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MobileAnimalOverlayProps {
|
interface MobileAnimalOverlayProps {
|
||||||
@@ -113,7 +110,6 @@ function getStopBetItems(selections: BetSelection[]) {
|
|||||||
return {
|
return {
|
||||||
amount: meta.amount,
|
amount: meta.amount,
|
||||||
cellId: normalizedCellId,
|
cellId: normalizedCellId,
|
||||||
imageUrl: FLOWER_IMAGE_BY_ID[normalizedCellId]?.animalUrl ?? '',
|
|
||||||
} satisfies StopBetItem
|
} satisfies StopBetItem
|
||||||
})
|
})
|
||||||
.sort((left, right) => left.cellId - right.cellId)
|
.sort((left, right) => left.cellId - right.cellId)
|
||||||
@@ -142,15 +138,11 @@ function StopBetSummary({
|
|||||||
className="flex h-design-42 min-w-design-100 items-center gap-design-5 overflow-hidden rounded-[calc(var(--design-unit)*7)] border border-[rgba(124,232,255,0.2)] bg-[linear-gradient(90deg,rgba(7,42,38,0.94),rgba(5,23,34,0.96)_44%,rgba(18,25,25,0.94))] px-design-4"
|
className="flex h-design-42 min-w-design-100 items-center gap-design-5 overflow-hidden rounded-[calc(var(--design-unit)*7)] border border-[rgba(124,232,255,0.2)] bg-[linear-gradient(90deg,rgba(7,42,38,0.94),rgba(5,23,34,0.96)_44%,rgba(18,25,25,0.94))] px-design-4"
|
||||||
>
|
>
|
||||||
<div className="relative h-design-32 w-design-32 shrink-0 overflow-hidden rounded-[calc(var(--design-unit)*5)] bg-[rgba(1,8,13,0.84)]">
|
<div className="relative h-design-32 w-design-32 shrink-0 overflow-hidden rounded-[calc(var(--design-unit)*5)] bg-[rgba(1,8,13,0.84)]">
|
||||||
{item.imageUrl ? (
|
<AnimalSpriteImage
|
||||||
<SmartImage
|
animalId={item.cellId}
|
||||||
src={item.imageUrl}
|
fit="cover"
|
||||||
alt=""
|
|
||||||
showSkeleton={false}
|
|
||||||
className="h-full w-full"
|
className="h-full w-full"
|
||||||
imgClassName="object-cover"
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 flex-1 flex-col items-stretch justify-center gap-design-3 leading-none">
|
<div className="flex min-w-0 flex-1 flex-col items-stretch justify-center gap-design-3 leading-none">
|
||||||
<div className="flex h-design-13 w-full items-center justify-center rounded-full bg-[rgba(4,31,31,0.9)] text-design-11 font-black leading-none text-[#78FF7F] [text-shadow:0_0_calc(var(--design-unit)*7)_rgba(120,255,127,0.52)]">
|
<div className="flex h-design-13 w-full items-center justify-center rounded-full bg-[rgba(4,31,31,0.9)] text-design-11 font-black leading-none text-[#78FF7F] [text-shadow:0_0_calc(var(--design-unit)*7)_rgba(120,255,127,0.52)]">
|
||||||
@@ -187,8 +179,6 @@ function NoRewardSettlement({
|
|||||||
resultLabel: string
|
resultLabel: string
|
||||||
resultNumber: number
|
resultNumber: number
|
||||||
}) {
|
}) {
|
||||||
const winningImageUrl = FLOWER_IMAGE_BY_ID[resultNumber]?.animalUrl ?? ''
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex w-design-336 max-w-[96%] flex-col items-center justify-center gap-design-9 rounded-[calc(var(--design-unit)*13)] border border-[rgba(124,232,255,0.34)] bg-[linear-gradient(180deg,rgba(7,28,40,0.96),rgba(3,12,20,0.98))] px-design-8 py-design-10 text-center shadow-[0_0_calc(var(--design-unit)*18)_rgba(70,245,255,0.28),0_0_calc(var(--design-unit)*34)_rgba(20,112,210,0.18),inset_0_0_calc(var(--design-unit)*16)_rgba(124,232,255,0.12)]">
|
<div className="relative z-10 flex w-design-336 max-w-[96%] flex-col items-center justify-center gap-design-9 rounded-[calc(var(--design-unit)*13)] border border-[rgba(124,232,255,0.34)] bg-[linear-gradient(180deg,rgba(7,28,40,0.96),rgba(3,12,20,0.98))] px-design-8 py-design-10 text-center shadow-[0_0_calc(var(--design-unit)*18)_rgba(70,245,255,0.28),0_0_calc(var(--design-unit)*34)_rgba(20,112,210,0.18),inset_0_0_calc(var(--design-unit)*16)_rgba(124,232,255,0.12)]">
|
||||||
<div className="text-design-20 font-black tracking-[0.12em] text-[#D8FBFF] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(84,245,255,0.42)]">
|
<div className="text-design-20 font-black tracking-[0.12em] text-[#D8FBFF] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(84,245,255,0.42)]">
|
||||||
@@ -201,14 +191,11 @@ function NoRewardSettlement({
|
|||||||
{resultLabel}
|
{resultLabel}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative h-design-46 w-design-46 overflow-hidden rounded-[calc(var(--design-unit)*6)] border border-[rgba(120,255,127,0.32)] bg-[rgba(1,8,13,0.84)]">
|
<div className="relative h-design-46 w-design-46 overflow-hidden rounded-[calc(var(--design-unit)*6)] border border-[rgba(120,255,127,0.32)] bg-[rgba(1,8,13,0.84)]">
|
||||||
{winningImageUrl ? (
|
<AnimalSpriteImage
|
||||||
<img
|
animalId={resultNumber}
|
||||||
src={winningImageUrl}
|
fit="cover"
|
||||||
alt=""
|
className="h-full w-full"
|
||||||
aria-hidden="true"
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-full border border-[rgba(120,255,127,0.36)] bg-[rgba(4,31,31,0.72)] px-design-8 py-design-2 text-design-15 font-black leading-none text-[#78FF7F] [text-shadow:0_0_calc(var(--design-unit)*7)_rgba(120,255,127,0.52)]">
|
<div className="rounded-full border border-[rgba(120,255,127,0.36)] bg-[rgba(4,31,31,0.72)] px-design-8 py-design-2 text-design-15 font-black leading-none text-[#78FF7F] [text-shadow:0_0_calc(var(--design-unit)*7)_rgba(120,255,127,0.52)]">
|
||||||
{String(resultNumber).padStart(2, '0')}
|
{String(resultNumber).padStart(2, '0')}
|
||||||
@@ -226,14 +213,11 @@ function NoRewardSettlement({
|
|||||||
className="flex h-design-52 w-design-42 flex-col items-center justify-center gap-design-2 overflow-hidden rounded-[calc(var(--design-unit)*6)] border border-[rgba(124,232,255,0.2)] bg-[linear-gradient(180deg,rgba(8,34,42,0.88),rgba(3,13,20,0.94))] p-design-2"
|
className="flex h-design-52 w-design-42 flex-col items-center justify-center gap-design-2 overflow-hidden rounded-[calc(var(--design-unit)*6)] border border-[rgba(124,232,255,0.2)] bg-[linear-gradient(180deg,rgba(8,34,42,0.88),rgba(3,13,20,0.94))] p-design-2"
|
||||||
>
|
>
|
||||||
<div className="relative h-design-30 w-design-30 overflow-hidden rounded-[calc(var(--design-unit)*4)] bg-[rgba(1,8,13,0.84)]">
|
<div className="relative h-design-30 w-design-30 overflow-hidden rounded-[calc(var(--design-unit)*4)] bg-[rgba(1,8,13,0.84)]">
|
||||||
{item.imageUrl ? (
|
<AnimalSpriteImage
|
||||||
<img
|
animalId={item.cellId}
|
||||||
src={item.imageUrl}
|
fit="cover"
|
||||||
alt=""
|
className="h-full w-full"
|
||||||
aria-hidden="true"
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-design-11 font-black leading-none text-[#4BFFFE] [text-shadow:0_0_calc(var(--design-unit)*6)_rgba(75,255,254,0.46)]">
|
<div className="text-design-11 font-black leading-none text-[#4BFFFE] [text-shadow:0_0_calc(var(--design-unit)*6)_rgba(75,255,254,0.46)]">
|
||||||
{String(item.cellId).padStart(2, '0')}
|
{String(item.cellId).padStart(2, '0')}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import diamondIcon from '@/assets/system/diamond.webp'
|
|||||||
import { SmartImage } from '@/components/smart-image'
|
import { SmartImage } from '@/components/smart-image'
|
||||||
import { MobileAnimalOverlay } from '@/features/game/components/mobile/mobile-animal-overlay.tsx'
|
import { MobileAnimalOverlay } from '@/features/game/components/mobile/mobile-animal-overlay.tsx'
|
||||||
import { MobileStatusLine } from '@/features/game/components/mobile/mobile-status.tsx'
|
import { MobileStatusLine } from '@/features/game/components/mobile/mobile-status.tsx'
|
||||||
|
import { AnimalSpriteImage } from '@/features/game/components/shared/animal-sprite-image'
|
||||||
import { FLOWER_IMAGE_LIST } from '@/features/game/shared'
|
import { FLOWER_IMAGE_LIST } from '@/features/game/shared'
|
||||||
import { useAnimalVm } from '@/hooks/use-animal-vm'
|
import { useAnimalVm } from '@/hooks/use-animal-vm'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -372,8 +373,8 @@ export function MobileAnimal({
|
|||||||
className="pointer-events-none absolute inset-[calc(var(--design-unit)*1)] z-20 rounded-[calc(var(--design-unit)*6)] bg-[rgba(4,16,24,0.52)] shadow-[inset_0_0_calc(var(--design-unit)*12)_rgba(3,9,14,0.56)]"
|
className="pointer-events-none absolute inset-[calc(var(--design-unit)*1)] z-20 rounded-[calc(var(--design-unit)*6)] bg-[rgba(4,16,24,0.52)] shadow-[inset_0_0_calc(var(--design-unit)*12)_rgba(3,9,14,0.56)]"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<SmartImage
|
<AnimalSpriteImage
|
||||||
src={item.animalUrl}
|
animalId={item.id}
|
||||||
alt={`animal-${item.id}`}
|
alt={`animal-${item.id}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute left-[1.5%] right-[1.5%] top-[2.9%] bottom-[2.9%] z-10 overflow-hidden rounded-[calc(var(--design-unit)*6)]',
|
'absolute left-[1.5%] right-[1.5%] top-[2.9%] bottom-[2.9%] z-10 overflow-hidden rounded-[calc(var(--design-unit)*6)]',
|
||||||
@@ -383,7 +384,6 @@ export function MobileAnimal({
|
|||||||
'brightness-110 saturate-120 drop-shadow-[0_0_calc(var(--design-unit)*9)_rgba(106,250,255,0.44)]',
|
'brightness-110 saturate-120 drop-shadow-[0_0_calc(var(--design-unit)*9)_rgba(106,250,255,0.44)]',
|
||||||
imageClassName,
|
imageClassName,
|
||||||
)}
|
)}
|
||||||
imgClassName="object-fill"
|
|
||||||
/>
|
/>
|
||||||
{showCellWarning ? (
|
{showCellWarning ? (
|
||||||
<motion.span
|
<motion.span
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { History } from 'lucide-react'
|
import { History } from 'lucide-react'
|
||||||
import { useCallback, useRef } from 'react'
|
import { useCallback, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { SmartImage } from '@/components/smart-image'
|
|
||||||
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
||||||
import { FLOWER_IMAGE_BY_ID } from '@/features/game/shared'
|
import { RewardSpriteImage } from '@/features/game/components/shared/reward-sprite-image'
|
||||||
import { useGameHistoryVm } from '@/hooks/use-game-history-vm.ts'
|
import { useGameHistoryVm } from '@/hooks/use-game-history-vm.ts'
|
||||||
|
|
||||||
function HistoryRewardNumber({
|
function HistoryRewardNumber({
|
||||||
@@ -13,10 +12,9 @@ function HistoryRewardNumber({
|
|||||||
className?: string
|
className?: string
|
||||||
number: number
|
number: number
|
||||||
}) {
|
}) {
|
||||||
const image = FLOWER_IMAGE_BY_ID[number]
|
|
||||||
const label = String(number).padStart(2, '0')
|
const label = String(number).padStart(2, '0')
|
||||||
|
|
||||||
if (!image?.rewardUrl) {
|
if (!Number.isInteger(number) || number < 1 || number > 36) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-flex h-design-24 min-w-design-24 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*4)] border border-[rgba(95,233,255,0.22)] bg-[rgba(6,28,39,0.7)] px-design-4 text-design-10 font-bold text-[#D5FBFF] align-middle ${className ?? ''}`}
|
className={`inline-flex h-design-24 min-w-design-24 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*4)] border border-[rgba(95,233,255,0.22)] bg-[rgba(6,28,39,0.7)] px-design-4 text-design-10 font-bold text-[#D5FBFF] align-middle ${className ?? ''}`}
|
||||||
@@ -31,12 +29,10 @@ function HistoryRewardNumber({
|
|||||||
className={`inline-flex h-design-24 w-design-24 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*4)] border border-[rgba(95,233,255,0.16)] bg-[rgba(6,28,39,0.38)] p-design-1 align-middle ${className ?? ''}`}
|
className={`inline-flex h-design-24 w-design-24 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*4)] border border-[rgba(95,233,255,0.16)] bg-[rgba(6,28,39,0.38)] p-design-1 align-middle ${className ?? ''}`}
|
||||||
title={label}
|
title={label}
|
||||||
>
|
>
|
||||||
<SmartImage
|
<RewardSpriteImage
|
||||||
src={image.rewardUrl}
|
rewardId={number}
|
||||||
alt={label}
|
alt={label}
|
||||||
showSkeleton={false}
|
className="h-full w-full"
|
||||||
className="h-full w-full overflow-visible"
|
|
||||||
imgClassName="object-contain"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
||||||
|
import { AnimalSpriteImage } from '@/features/game/components/shared/animal-sprite-image'
|
||||||
import type { PeriodHistoryDisplayItem } from '@/hooks/use-period-history-vm'
|
import type { PeriodHistoryDisplayItem } from '@/hooks/use-period-history-vm'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -107,16 +108,12 @@ export function MobilePeriodHistoryList({
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="flex min-w-0 items-center justify-center overflow-hidden">
|
<span className="flex min-w-0 items-center justify-center overflow-hidden">
|
||||||
{item.image ? (
|
<AnimalSpriteImage
|
||||||
<img
|
animalId={item.animalId}
|
||||||
src={item.image}
|
|
||||||
alt={item.displayResultNumber}
|
alt={item.displayResultNumber}
|
||||||
draggable={false}
|
fit="cover"
|
||||||
className="h-design-34 w-auto max-w-full object-contain drop-shadow-[0_0_calc(var(--design-unit)*6)_rgba(99,243,255,0.12)]"
|
className="h-design-34 w-full drop-shadow-[0_0_calc(var(--design-unit)*6)_rgba(99,243,255,0.12)]"
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<span className="text-design-9 text-[#668C92]">--</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
61
src/features/game/components/shared/animal-sprite-image.tsx
Normal file
61
src/features/game/components/shared/animal-sprite-image.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
import animalSpriteUrl from '@/assets/animal/animal-sprite.webp'
|
||||||
|
import {
|
||||||
|
ANIMAL_SPRITE_CELL_ASPECT_RATIO,
|
||||||
|
ANIMAL_SPRITE_COLUMNS,
|
||||||
|
ANIMAL_SPRITE_ROWS,
|
||||||
|
getAnimalSpritePosition,
|
||||||
|
} from '@/features/game/shared'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface AnimalSpriteImageProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
|
||||||
|
animalId: number
|
||||||
|
alt?: string
|
||||||
|
fit?: 'cover' | 'fill'
|
||||||
|
imageClassName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnimalSpriteImage({
|
||||||
|
animalId,
|
||||||
|
alt = '',
|
||||||
|
className,
|
||||||
|
fit = 'fill',
|
||||||
|
imageClassName,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: AnimalSpriteImageProps) {
|
||||||
|
const position = getAnimalSpritePosition(animalId)
|
||||||
|
const accessibilityProps = alt
|
||||||
|
? ({ 'aria-label': alt, role: 'img' } as const)
|
||||||
|
: ({ 'aria-hidden': 'true' } as const)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
{...accessibilityProps}
|
||||||
|
{...props}
|
||||||
|
className={cn('relative block overflow-hidden', className)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{position ? (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
'block bg-no-repeat',
|
||||||
|
fit === 'cover'
|
||||||
|
? 'absolute left-1/2 top-1/2 h-full min-w-full -translate-x-1/2 -translate-y-1/2'
|
||||||
|
: 'absolute inset-0',
|
||||||
|
imageClassName,
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
aspectRatio:
|
||||||
|
fit === 'cover' ? ANIMAL_SPRITE_CELL_ASPECT_RATIO : undefined,
|
||||||
|
backgroundImage: `url(${animalSpriteUrl})`,
|
||||||
|
backgroundPosition: `${position.xPercent}% ${position.yPercent}%`,
|
||||||
|
backgroundSize: `${ANIMAL_SPRITE_COLUMNS * 100}% ${ANIMAL_SPRITE_ROWS * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
||||||
|
import { AnimalSpriteImage } from '@/features/game/components/shared/animal-sprite-image'
|
||||||
import type { PeriodHistoryDisplayItem } from '@/hooks/use-period-history-vm'
|
import type { PeriodHistoryDisplayItem } from '@/hooks/use-period-history-vm'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -95,16 +96,12 @@ export function PeriodHistoryList({
|
|||||||
{item.displayResultNumber}
|
{item.displayResultNumber}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex h-design-50 w-design-72 shrink-0 items-center justify-end overflow-hidden">
|
<span className="flex h-design-50 w-design-72 shrink-0 items-center justify-end overflow-hidden">
|
||||||
{item.image ? (
|
<AnimalSpriteImage
|
||||||
<img
|
animalId={item.animalId}
|
||||||
src={item.image}
|
|
||||||
alt={item.displayResultNumber}
|
alt={item.displayResultNumber}
|
||||||
draggable={false}
|
fit="cover"
|
||||||
className="h-full w-auto max-w-none shrink-0 object-contain"
|
className="h-full w-full"
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<span className="text-design-13 text-[#668C92]">--</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
52
src/features/game/components/shared/reward-sprite-image.tsx
Normal file
52
src/features/game/components/shared/reward-sprite-image.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
import rewardSpriteUrl from '@/assets/reward/reward-sprite.webp'
|
||||||
|
import {
|
||||||
|
getRewardSpritePosition,
|
||||||
|
REWARD_SPRITE_CELL_ASPECT_RATIO,
|
||||||
|
REWARD_SPRITE_COLUMNS,
|
||||||
|
REWARD_SPRITE_ROWS,
|
||||||
|
} from '@/features/game/shared'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface RewardSpriteImageProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
|
||||||
|
alt?: string
|
||||||
|
imageClassName?: string
|
||||||
|
rewardId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RewardSpriteImage({
|
||||||
|
alt = '',
|
||||||
|
className,
|
||||||
|
imageClassName,
|
||||||
|
rewardId,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: RewardSpriteImageProps) {
|
||||||
|
const position = getRewardSpritePosition(rewardId)
|
||||||
|
const accessibilityProps = alt
|
||||||
|
? ({ 'aria-label': alt, role: 'img' } as const)
|
||||||
|
: ({ 'aria-hidden': 'true' } as const)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
{...accessibilityProps}
|
||||||
|
{...props}
|
||||||
|
className={cn('relative block overflow-hidden', className)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{position ? (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn('absolute inset-0 block bg-no-repeat', imageClassName)}
|
||||||
|
style={{
|
||||||
|
aspectRatio: REWARD_SPRITE_CELL_ASPECT_RATIO,
|
||||||
|
backgroundImage: `url(${rewardSpriteUrl})`,
|
||||||
|
backgroundPosition: `${position.xPercent}% ${position.yPercent}%`,
|
||||||
|
backgroundSize: `${REWARD_SPRITE_COLUMNS * 100}% ${REWARD_SPRITE_ROWS * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,27 +1,64 @@
|
|||||||
import type { FlowerImageAsset } from '@/type'
|
import type { FlowerImageAsset } from '@/type'
|
||||||
|
|
||||||
const animalModules = import.meta.glob('../../../assets/animal/*.webp', {
|
export const ANIMAL_SPRITE_COLUMNS = 6
|
||||||
eager: true,
|
export const ANIMAL_SPRITE_ROWS = 6
|
||||||
import: 'default',
|
export const ANIMAL_SPRITE_CELL_WIDTH = 446
|
||||||
}) as Record<string, string>
|
export const ANIMAL_SPRITE_CELL_HEIGHT = 224
|
||||||
|
export const ANIMAL_SPRITE_CELL_ASPECT_RATIO = `${ANIMAL_SPRITE_CELL_WIDTH} / ${ANIMAL_SPRITE_CELL_HEIGHT}`
|
||||||
|
export const ANIMAL_SPRITE_ITEM_COUNT =
|
||||||
|
ANIMAL_SPRITE_COLUMNS * ANIMAL_SPRITE_ROWS
|
||||||
|
export const REWARD_SPRITE_COLUMNS = 6
|
||||||
|
export const REWARD_SPRITE_ROWS = 6
|
||||||
|
export const REWARD_SPRITE_CELL_WIDTH = 102
|
||||||
|
export const REWARD_SPRITE_CELL_HEIGHT = 98
|
||||||
|
export const REWARD_SPRITE_CELL_ASPECT_RATIO = `${REWARD_SPRITE_CELL_WIDTH} / ${REWARD_SPRITE_CELL_HEIGHT}`
|
||||||
|
export const REWARD_SPRITE_ITEM_COUNT =
|
||||||
|
REWARD_SPRITE_COLUMNS * REWARD_SPRITE_ROWS
|
||||||
|
|
||||||
const rewardModules = import.meta.glob('../../../assets/reward/*.webp', {
|
export function getAnimalSpritePosition(animalId: number) {
|
||||||
eager: true,
|
if (
|
||||||
import: 'default',
|
!Number.isInteger(animalId) ||
|
||||||
}) as Record<string, string>
|
animalId < 1 ||
|
||||||
|
animalId > ANIMAL_SPRITE_ITEM_COUNT
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = animalId - 1
|
||||||
|
const column = index % ANIMAL_SPRITE_COLUMNS
|
||||||
|
const row = Math.floor(index / ANIMAL_SPRITE_COLUMNS)
|
||||||
|
|
||||||
|
return {
|
||||||
|
xPercent: (column / (ANIMAL_SPRITE_COLUMNS - 1)) * 100,
|
||||||
|
yPercent: (row / (ANIMAL_SPRITE_ROWS - 1)) * 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRewardSpritePosition(rewardId: number) {
|
||||||
|
if (
|
||||||
|
!Number.isInteger(rewardId) ||
|
||||||
|
rewardId < 1 ||
|
||||||
|
rewardId > REWARD_SPRITE_ITEM_COUNT
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = rewardId - 1
|
||||||
|
const column = index % REWARD_SPRITE_COLUMNS
|
||||||
|
const row = Math.floor(index / REWARD_SPRITE_COLUMNS)
|
||||||
|
|
||||||
|
return {
|
||||||
|
xPercent: (column / (REWARD_SPRITE_COLUMNS - 1)) * 100,
|
||||||
|
yPercent: (row / (REWARD_SPRITE_ROWS - 1)) * 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const FLOWER_IMAGE_LIST: FlowerImageAsset[] = Array.from(
|
export const FLOWER_IMAGE_LIST: FlowerImageAsset[] = Array.from(
|
||||||
{ length: 36 },
|
{ length: 36 },
|
||||||
(_, index) => {
|
(_, index) => ({
|
||||||
const id = index + 1
|
id: index + 1,
|
||||||
|
}),
|
||||||
return {
|
)
|
||||||
animalUrl: animalModules[`../../../assets/animal/${id}.webp`] ?? '',
|
|
||||||
id,
|
|
||||||
rewardUrl: rewardModules[`../../../assets/reward/${id}.webp`] ?? '',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
).filter((item) => item.animalUrl && item.rewardUrl)
|
|
||||||
|
|
||||||
export const FLOWER_IMAGE_BY_ID = FLOWER_IMAGE_LIST.reduce<
|
export const FLOWER_IMAGE_BY_ID = FLOWER_IMAGE_LIST.reduce<
|
||||||
Record<number, FlowerImageAsset>
|
Record<number, FlowerImageAsset>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useQuery } from '@tanstack/react-query'
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import { getGamePeriodHistory } from '@/api'
|
import { getGamePeriodHistory } from '@/api'
|
||||||
import { FLOWER_IMAGE_BY_ID } from '@/features/game/shared'
|
|
||||||
import type { GamePeriodHistoryItemDto, PeriodHistoryDisplayItem } from '@/type'
|
import type { GamePeriodHistoryItemDto, PeriodHistoryDisplayItem } from '@/type'
|
||||||
|
|
||||||
export type { PeriodHistoryDisplayItem } from '@/type'
|
export type { PeriodHistoryDisplayItem } from '@/type'
|
||||||
@@ -23,9 +22,9 @@ export function toPeriodHistoryDisplayItem(
|
|||||||
item: GamePeriodHistoryItemDto,
|
item: GamePeriodHistoryItemDto,
|
||||||
): PeriodHistoryDisplayItem {
|
): PeriodHistoryDisplayItem {
|
||||||
return {
|
return {
|
||||||
|
animalId: item.result_number,
|
||||||
displayPeriodNo: formatPeriodNo(item.period_no),
|
displayPeriodNo: formatPeriodNo(item.period_no),
|
||||||
displayResultNumber: formatResultNumber(item.result_number),
|
displayResultNumber: formatResultNumber(item.result_number),
|
||||||
image: FLOWER_IMAGE_BY_ID[item.result_number]?.animalUrl ?? '',
|
|
||||||
isOdd: item.result_number % 2 === 1,
|
isOdd: item.result_number % 2 === 1,
|
||||||
openTime: item.open_time,
|
openTime: item.open_time,
|
||||||
periodNo: item.period_no,
|
periodNo: item.period_no,
|
||||||
|
|||||||
@@ -158,9 +158,9 @@ export interface SelectionSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PeriodHistoryDisplayItem {
|
export interface PeriodHistoryDisplayItem {
|
||||||
|
animalId: number
|
||||||
displayPeriodNo: string
|
displayPeriodNo: string
|
||||||
displayResultNumber: string
|
displayResultNumber: string
|
||||||
image: string
|
|
||||||
isOdd: boolean
|
isOdd: boolean
|
||||||
openTime: number
|
openTime: number
|
||||||
periodNo: string
|
periodNo: string
|
||||||
@@ -168,9 +168,7 @@ export interface PeriodHistoryDisplayItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FlowerImageAsset {
|
export interface FlowerImageAsset {
|
||||||
animalUrl: string
|
|
||||||
id: number
|
id: number
|
||||||
rewardUrl: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RevealAnimationState {
|
export interface RevealAnimationState {
|
||||||
|
|||||||
Reference in New Issue
Block a user