diff --git a/bun.lock b/bun.lock index 8f742aa..067676d 100644 --- a/bun.lock +++ b/bun.lock @@ -50,17 +50,20 @@ "version": "0.1.0", "dependencies": { "@elysiajs/eden": "^1.4.8", + "@formkit/auto-animate": "^0.9.0", "@heroicons/react": "^2.2.0", "@heroui/react": "^3.0.0-beta.8", "@heroui/styles": "^3.0.0-beta.8", "@tailwindcss/postcss": "^4.2.1", "ahooks": "^3.9.6", "better-auth": "^1.5.4", + "html-to-image": "^1.11.13", "lucide-react": "^0.577.0", "postcss": "^8.5.8", "primeicons": "^7.0.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-icons": "^5.5.0", "react-router": "^7.13.1", "react-router-dom": "^7.13.1", "swr": "^2.4.1", @@ -233,6 +236,8 @@ "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "https://registry.npmmirror.com/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="], + "@formkit/auto-animate": ["@formkit/auto-animate@0.9.0", "https://registry.npmmirror.com/@formkit/auto-animate/-/auto-animate-0.9.0.tgz", {}, "sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA=="], + "@heroicons/react": ["@heroicons/react@2.2.0", "https://registry.npmmirror.com/@heroicons/react/-/react-2.2.0.tgz", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="], "@heroui/react": ["@heroui/react@3.0.0-beta.8", "https://registry.npmmirror.com/@heroui/react/-/react-3.0.0-beta.8.tgz", { "dependencies": { "@heroui/styles": "3.0.0-beta.8", "@radix-ui/react-avatar": "1.1.11", "@react-aria/i18n": "3.12.15", "@react-aria/ssr": "3.9.10", "@react-aria/utils": "3.33.0", "@react-stately/utils": "3.11.0", "@react-types/color": "3.1.3", "@react-types/shared": "3.33.0", "input-otp": "1.4.2", "react-aria-components": "1.15.1", "tailwind-merge": "3.4.0", "tailwind-variants": "3.2.2" }, "peerDependencies": { "react": ">=19.0.0", "react-dom": ">=19.0.0", "tailwindcss": ">=4.0.0" } }, "sha512-1wh7N689LKLgXf/FbGg2Y7fMtiwdHcANHdRKDKFly4UWsraxEXdpfJaeL788TE3rpsMmzuKl3FZBOewjBGKKLQ=="], @@ -1069,6 +1074,8 @@ "hono": ["hono@4.11.4", "https://registry.npmmirror.com/hono/-/hono-4.11.4.tgz", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], + "html-to-image": ["html-to-image@1.11.13", "https://registry.npmmirror.com/html-to-image/-/html-to-image-1.11.13.tgz", {}, "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg=="], + "http-status-codes": ["http-status-codes@2.3.0", "https://registry.npmmirror.com/http-status-codes/-/http-status-codes-2.3.0.tgz", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], "iconv-lite": ["iconv-lite@0.7.2", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], @@ -1373,6 +1380,8 @@ "react-fast-compare": ["react-fast-compare@3.2.2", "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], + "react-icons": ["react-icons@5.5.0", "https://registry.npmmirror.com/react-icons/-/react-icons-5.5.0.tgz", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="], + "react-is": ["react-is@16.13.1", "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "react-refresh": ["react-refresh@0.17.0", "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], diff --git a/web/package.json b/web/package.json index d8afc6d..19c781e 100644 --- a/web/package.json +++ b/web/package.json @@ -13,17 +13,20 @@ }, "dependencies": { "@elysiajs/eden": "^1.4.8", + "@formkit/auto-animate": "^0.9.0", "@heroicons/react": "^2.2.0", "@heroui/react": "^3.0.0-beta.8", "@heroui/styles": "^3.0.0-beta.8", "@tailwindcss/postcss": "^4.2.1", "ahooks": "^3.9.6", "better-auth": "^1.5.4", + "html-to-image": "^1.11.13", "lucide-react": "^0.577.0", "postcss": "^8.5.8", "primeicons": "^7.0.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-icons": "^5.5.0", "react-router": "^7.13.1", "react-router-dom": "^7.13.1", "swr": "^2.4.1", diff --git a/web/src/app/hud.tsx b/web/src/app/hud.tsx new file mode 100644 index 0000000..9bd53c0 --- /dev/null +++ b/web/src/app/hud.tsx @@ -0,0 +1,9 @@ +import { Outlet } from 'react-router-dom' + +export default function HudLayout() { + return ( +
+ +
+ ) +} diff --git a/web/src/app/hud/crosshair/index.tsx b/web/src/app/hud/crosshair/index.tsx new file mode 100644 index 0000000..e615ef4 --- /dev/null +++ b/web/src/app/hud/crosshair/index.tsx @@ -0,0 +1,3 @@ +export default function Crosshair() { + return
准星生成
+} diff --git a/web/src/app/hud/deathmsg/canvas.ts b/web/src/app/hud/deathmsg/canvas.ts new file mode 100644 index 0000000..8555840 --- /dev/null +++ b/web/src/app/hud/deathmsg/canvas.ts @@ -0,0 +1,68 @@ +import { toPng } from 'html-to-image' + +export async function Canvas2Image(e: HTMLElement, name: string, dpi: number) { + // 滚动条置顶解决生成图片不全的问题 + window.scrollY = 0 + document.documentElement.scrollTop = 0 + document.documentElement.scrollLeft = 0 + document.body.scrollTop = 0 + + // 保存原始样式 + const originalPosition = e.style.position + const originalTop = e.style.top + const originalBottom = e.style.bottom + const originalLeft = e.style.left + const originalRight = e.style.right + const originalVisibility = e.style.visibility + + try { + // 临时调整元素位置,确保在视口内可见 + e.style.position = 'relative' + e.style.top = '0' + e.style.bottom = 'auto' + e.style.left = '0' + e.style.right = 'auto' + e.style.visibility = 'visible' + + // 等待样式应用 + await new Promise(resolve => setTimeout(resolve, 10)) + + const dataUrl = await toPng(e, { + backgroundColor: 'transparent', // 透明背景 + pixelRatio: dpi, + quality: 1.0, + style: { + letterSpacing: 'normal', + lineHeight: 'normal', + }, + }) + + if (dataUrl !== '') { + const link = document.createElement('a') + link.href = dataUrl + link.setAttribute('download', name + '.png') + link.style.display = 'none' + + document.body.appendChild(link) + link.click() + + // 等待一小段时间确保下载已触发,然后再移除元素 + await new Promise(resolve => setTimeout(resolve, 100)) + + // 确保元素存在且仍在 DOM 中再移除 + if (link.parentNode) { + document.body.removeChild(link) + } + } + } catch (error) { + console.error('生成 PNG 失败:', error) + } finally { + // 恢复原始样式 + e.style.position = originalPosition + e.style.top = originalTop + e.style.bottom = originalBottom + e.style.left = originalLeft + e.style.right = originalRight + e.style.visibility = originalVisibility + } +} diff --git a/web/src/app/hud/deathmsg/dm.css b/web/src/app/hud/deathmsg/dm.css new file mode 100644 index 0000000..b476d6c --- /dev/null +++ b/web/src/app/hud/deathmsg/dm.css @@ -0,0 +1,113 @@ +/* 参考用 */ +@font-face { + font-family: 'Stratum2'; + src: url('@/assets/font/Stratum2.ttf'); + font-style: normal; + font-weight: bold; +} + +.setting-input { + @apply px-3 py-1 rounded-lg outline-none; +} + +.dn-input { + @apply px-3 py-1 rounded-lg outline-none bg-gray-100 w-full; +} + +.dn-item { + @apply flex flex-col gap-3 bg-white rounded-lg p-3; +} + +.dn-camp { + @apply bg-gray-100 rounded w-12; +} + +.dn-fix-icon { + @apply rounded p-1 w-8 transition duration-200 hover:bg-gray-300 active:scale-95; +} + +/* .dn-button { */ +/* } */ + +#OutputDiv { + clear: both; + /*width: 1920px;*/ + /*height: 1080px;*/ + /*position: fixed;*/ + /*float: end;*/ + margin: 30px auto; + /*background: pink; !*debug用的颜色*!*/ + background: rgba(0, 0, 0, 0); + /**/ + /*font-weight: bold;*/ + /*font-family: 'Stratum2';*/ + /* src: url("../assets/font/Stratum2.ttf") format('truetype'); */ +} + +.deathNotice { + font-family: 'Stratum2', 'Arial Unicode MS', 'Helvetica Neue', 'Helvetica', 'PingFang SC', 'Sarasa Gothic SC', 'Source Han Sans', 'Noto Sans SC', + 'Alibaba Puhuiti 2', 'Alibaba Puhuiti', Avenir, Arial, sans-serif; + font-size: 18px; + line-height: 18px; + font-weight: bold; + width: max-content; + /*float: right;*/ + right: 0; + padding: 6px 12px 6px 12px; + transition-property: opacity; + transition-timing-function: ease-out; + background-color: rgba(0, 0, 0, 0.65); + border-radius: 4px; + text-align: center; + /*box-shadow: inset #e10000e6 0px 0px 1px;*/ + /*border: 2px solid #e10000;*/ + /*border: 2px outset rgba(0,0,0,0.44);*/ +} + +.dn-gen { + line-height: 0.5 !important; +} + +.CT { + color: rgb(111, 156, 230); +} + +.T { + color: rgb(234, 190, 84); +} + +.weapon { + background: rgba(0, 0, 0, 0); + height: 23px; + vertical-align: middle; + visibility: visible; +} + +.prefix { + height: 24px; +} + +.suffix { + height: 24px; +} + +.attacker { + padding-right: 4px; + font-size: 16px; +} + +.victim { + padding-left: 4px; + font-size: 16px; +} + +.DispRedBorder { + /*height: 30px;*/ + border: 2px solid #e10000; + padding: 4px 10px 4px 10px; +} + +.dn-transparent { + background-color: rgba(0, 0, 0, 0); + opacity: 0; +} diff --git a/web/src/app/hud/deathmsg/dmsg.ts b/web/src/app/hud/deathmsg/dmsg.ts new file mode 100644 index 0000000..af93d3d --- /dev/null +++ b/web/src/app/hud/deathmsg/dmsg.ts @@ -0,0 +1,554 @@ +export type DeathMsg = { + attacker: string + attackerCamp: Camp + victim: string + victimCamp: Camp + weapon: Weapon + prefixIcons: prefixIcon[] + suffixIcons: suffixIcon[] + redBorder: boolean + hide?: boolean +} + +export const DefaultDeathMsgs: DeathMsg[] = [ + { + attacker: 'Attacker', + attackerCamp: 'T', + victim: 'Victim', + victimCamp: 'CT', + weapon: 'ak47', + prefixIcons: ['blind_kill'], + suffixIcons: ['headshot'], + redBorder: false, + hide: false, + }, + { + attacker: '中文字体样式.gg', + attackerCamp: 'CT', + victim: 'Purp1e', + victimCamp: 'T', + weapon: 'awp', + prefixIcons: ['revenge', 'blind_kill'], + suffixIcons: [], + redBorder: true, + hide: false, + }, +] + +export const CStrikeDefaultDeathMsgs: DeathMsg[] = [ + { + attacker: 'Attacker', + attackerCamp: 'T', + victim: 'Victim', + victimCamp: 'CT', + weapon: 'ak47', + prefixIcons: [], + suffixIcons: ['headshot'], + redBorder: false, + hide: false, + }, + { + attacker: 'CS1.6样式', + attackerCamp: 'CT', + victim: 'Purp1e', + victimCamp: 'T', + weapon: 'awp', + prefixIcons: [], + suffixIcons: [], + redBorder: true, + hide: false, + }, +] + +export const DefaultDeathMsg = DefaultDeathMsgs[0] +export const CStrikeDefaultDeathMsg = CStrikeDefaultDeathMsgs[0] + +export type Camp = 'CT' | 'T' | '' + +export const CampValues: Camp[] = ['CT', 'T'] + +export type CStrikePrefixIcon = '' +export type CS2PrefixIcon = 'revenge' | 'domination' | 'blind_kill' | '' +export type prefixIcon = CStrikePrefixIcon | CS2PrefixIcon + +export const CStrikePrefixIconValues: CStrikePrefixIcon[] = [] +export const CS2PrefixIconValues: CS2PrefixIcon[] = ['revenge', 'domination', 'blind_kill'] + +export type CStrikeSuffixIcon = 'headshot' | '' +export type CS2SuffixIcon = 'noscope' | 'jumpkill' | 'throughsmoke' | 'penetrate' | 'headshot' | 'suicide' | 'kill360' | '' +export type suffixIcon = CStrikeSuffixIcon | CS2SuffixIcon + +export const CStrikeSuffixIconValues: CStrikeSuffixIcon[] = ['headshot'] +export const CS2SuffixIconValues: CS2SuffixIcon[] = ['noscope', 'jumpkill', 'throughsmoke', 'penetrate', 'headshot', 'suicide', 'kill360'] + +export type CStrikeWeapon = + | '228-compact' + | '550-commando' + | 'ak47' + | 'aug' + | 'awp' + | 'cs-grenade' + | 'd3_au-1' + | 'deagle' + | 'dual-elites' + | 'famas-1' + | 'famas' + | 'flare' + | 'gauge-super' + | 'glock' + | 'he-grenade' + | 'idf-defender' + | 'knife' + | 'krieg-552' + | 'law-grenade' + | 'm249-2' + | 'm249' + | 'mac10-1' + | 'mac10' + | 'melee' + | 'mp5' + | 'nade' + | 'p90' + | 'rifle' + | 'scout' + | 'sg552' + | 'suicide' + | 'train' + | 'ump45' + | 'usp-silencer' + | 'vault' + | 'wtf' + | 'yg1265' + | '' +export type CS2Weapon = + | 'ak47' + | 'ammobox' + | 'ammobox_threepack' + | 'armor' + | 'armor_helmet' + | 'assaultsuit_helmet_only' + | 'assaultsuit' + | 'aug' + | 'awp' + | 'axe' + | 'bayonet' + | 'bizon' + | 'breachcharge' + | 'breachcharge_projectile' + | 'bumpmine' + | 'c4' + | 'clothing_hands' + | 'controldrone' + | 'customplayer' + | 'cz75a' + | 'deagle' + | 'decoy' + | 'defuser' + | 'disconnect' + | 'diversion' + | 'dronegun' + | 'elite' + | 'famas' + | 'firebomb' + | 'fists' + | 'fiveseven' + | 'flair0' + | 'flashbang' + | 'flashbang_assist' + | 'frag_grenade' + | 'g3sg1' + | 'galilar' + | 'glock' + | 'grenadepack' + | 'grenadepack2' + | 'hammer' + | 'healthshot' + | 'heavy_armor' + | 'hegrenade' + | 'helmet' + | 'hkp2000' + | 'incgrenade' + | 'inferno' + | 'kevlar' + | 'knife' + | 'knife_bowie' + | 'knife_butterfly' + | 'knife_canis' + | 'knife_cord' + | 'knife_css' + | 'knife_falchion' + | 'knife_flip' + | 'knife_gut' + | 'knife_gypsy_jackknife' + | 'knife_karambit' + | 'knife_kukri' + | 'knife_m9_bayonet' + | 'knife_push' + | 'knife_skeleton' + | 'knife_stiletto' + | 'knife_survival_bowie' + | 'knife_t' + | 'knife_tactical' + | 'knife_twinblade' + | 'knife_ursus' + | 'knife_widowmaker' + | 'knifegg' + | 'm4a1' + | 'm4a1_silencer' + | 'm4a1_silencer_off' + | 'm249' + | 'mac10' + | 'mag7' + | 'melee' + | 'molotov' + | 'movelinear' + | 'mp5sd' + | 'mp7' + | 'mp9' + | 'negev' + | 'nova' + | 'p90' + | 'p250' + | 'p2000' + | 'planted_c4_survival' + | 'planted_c4' + | 'prop_exploding_barrel' + | 'radarjammer' + | 'revolver' + | 'sawedoff' + | 'scar20' + | 'sg556' + | 'shield' + | 'smokegrenade' + | 'snowball' + | 'spanner' + | 'spray0' + | 'ssg08' + | 'stomp_damage' + | 'tablet' + | 'tagrenade' + | 'taser' + | 'tec9' + | 'tripwirefire_projectile' + | 'tripwirefire' + | 'ump45' + | 'usp_silencer' + | 'usp_silencer_off' + | 'xm1014' + | 'zone_repulsor' + | '' +export type Weapon = CStrikeWeapon | CS2Weapon + +export const CStrikeWeaponValues: CStrikeWeapon[] = [ + '228-compact', + '550-commando', + 'ak47', + 'aug', + 'awp', + 'cs-grenade', + 'd3_au-1', + 'deagle', + 'dual-elites', + 'famas-1', + 'famas', + 'flare', + 'gauge-super', + 'glock', + 'he-grenade', + 'idf-defender', + 'knife', + 'krieg-552', + 'law-grenade', + 'm249-2', + 'm249', + 'mac10-1', + 'mac10', + 'melee', + 'mp5', + 'nade', + 'p90', + 'rifle', + 'scout', + 'sg552', + 'suicide', + 'train', + 'ump45', + 'usp-silencer', + 'vault', + 'wtf', + 'yg1265', +] +export const CS2WeaponValues: CS2Weapon[] = [ + 'ak47', + 'ammobox', + 'ammobox_threepack', + 'armor', + 'armor_helmet', + 'assaultsuit_helmet_only', + 'assaultsuit', + 'aug', + 'awp', + 'axe', + 'bayonet', + 'bizon', + 'breachcharge', + 'breachcharge_projectile', + 'bumpmine', + 'c4', + 'clothing_hands', + 'controldrone', + 'customplayer', + 'cz75a', + 'deagle', + 'decoy', + 'defuser', + 'disconnect', + 'diversion', + 'dronegun', + 'elite', + 'famas', + 'firebomb', + 'fists', + 'fiveseven', + 'flair0', + 'flashbang', + 'flashbang_assist', + 'frag_grenade', + 'g3sg1', + 'galilar', + 'glock', + 'grenadepack', + 'grenadepack2', + 'hammer', + 'healthshot', + 'heavy_armor', + 'hegrenade', + 'helmet', + 'hkp2000', + 'incgrenade', + 'inferno', + 'kevlar', + 'knife', + 'knife_bowie', + 'knife_butterfly', + 'knife_canis', + 'knife_cord', + 'knife_css', + 'knife_falchion', + 'knife_flip', + 'knife_gut', + 'knife_gypsy_jackknife', + 'knife_karambit', + 'knife_kukri', + 'knife_m9_bayonet', + 'knife_push', + 'knife_skeleton', + 'knife_stiletto', + 'knife_survival_bowie', + 'knife_t', + 'knife_tactical', + 'knife_twinblade', + 'knife_ursus', + 'knife_widowmaker', + 'knifegg', + 'm4a1', + 'm4a1_silencer', + 'm4a1_silencer_off', + 'm249', + 'mac10', + 'mag7', + 'melee', + 'molotov', + 'movelinear', + 'mp5sd', + 'mp7', + 'mp9', + 'negev', + 'nova', + 'p90', + 'p250', + 'p2000', + 'planted_c4_survival', + 'planted_c4', + 'prop_exploding_barrel', + 'radarjammer', + 'revolver', + 'sawedoff', + 'scar20', + 'sg556', + 'shield', + 'smokegrenade', + 'snowball', + 'spanner', + 'spray0', + 'ssg08', + 'stomp_damage', + 'tablet', + 'tagrenade', + 'taser', + 'tec9', + 'tripwirefire_projectile', + 'tripwirefire', + 'ump45', + 'usp_silencer', + 'usp_silencer_off', + 'xm1014', + 'zone_repulsor', +] + +export const CStrikeWeaponMap: Record = { + '228-compact': '228-compact', + '550-commando': '550-commando', + ak47: 'ak47', + aug: 'aug', + awp: 'awp', + 'cs-grenade': 'CS手雷', + 'd3_au-1': 'D3/AU-1', + deagle: 'deagle', + 'dual-elites': 'dual-elites', + 'famas-1': 'famas-1', + famas: 'famas', + flare: 'flare', + 'gauge-super': 'gauge-super', + glock: 'glock', + 'he-grenade': 'HE手雷', + 'idf-defender': 'idf-defender', + knife: 'knife', + 'krieg-552': 'krieg-552', + 'law-grenade': 'Law手雷', + 'm249-2': 'm249-2', + m249: 'm249', + 'mac10-1': 'mac10-1', + mac10: 'mac10', + melee: 'melee', + mp5: 'mp5', + nade: 'nade', + p90: 'p90', + rifle: 'rifle', + scout: 'scout', + sg552: 'sg552', + suicide: '自杀', + train: 'train', + ump45: 'ump45', + 'usp-silencer': 'usp-silencer', + vault: 'vault', + wtf: 'wtf', + yg1265: 'yg1265', +} +export const CS2WeaponMap: Record = { + ak47: 'AK47', + ammobox: '弹药箱', + ammobox_threepack: '弹药箱三件套', + armor: '护甲', + armor_helmet: '护甲头盔', + assaultsuit_helmet_only: '突击套装头盔', + assaultsuit: '突击套装', + aug: 'AUG', + awp: 'AWP', + axe: '斧头', + bayonet: '刺刀', + bizon: 'PP野牛', + breachcharge: '遥控炸弹', + breachcharge_projectile: '遥控炸弹投射物', + bumpmine: '弹射地雷', + c4: 'C4炸弹', + clothing_hands: '服装手', + controldrone: '无人机', + customplayer: '自定义玩家', + cz75a: 'CZ75', + deagle: '沙漠之鹰', + decoy: '诱饵弹', + defuser: '拆弹器', + disconnect: '断开连接', + diversion: '分散注意', + dronegun: '无人机枪', + elite: '精英', + famas: '法玛斯', + firebomb: '燃烧弹', + fists: '拳头', + fiveseven: 'FN57', + flair0: '天赋', + flashbang: '闪光弹', + flashbang_assist: '闪光弹助攻', + frag_grenade: '破片手榴弹', + g3sg1: 'G3SG1', + galilar: '加利尔', + glock: '格洛克', + grenadepack: '手榴弹包', + grenadepack2: '手榴弹包2', + hammer: '锤子', + healthshot: '医疗针', + heavy_armor: '重装甲', + hegrenade: '高爆手榴弹', + helmet: '头盔', + hkp2000: 'P2000', + incgrenade: '燃烧手榴弹', + inferno: '地狱火', + kevlar: '凯夫拉', + knife: '刀', + knife_bowie: '鲍伊猎刀', + knife_butterfly: '蝴蝶刀', + knife_canis: '求生匕首', + knife_cord: '系绳匕首', + knife_css: '海豹短刀', + knife_falchion: '弯刀', + knife_flip: '折叠刀', + knife_gut: '穿肠刀', + knife_gypsy_jackknife: '折刀', + knife_karambit: '爪子刀', + knife_kukri: '廓尔喀弯刀', + knife_m9_bayonet: 'M9刺刀', + knife_push: '暗影双匕', + knife_skeleton: '骷髅匕首', + knife_stiletto: '短剑', + knife_survival_bowie: '求生鲍伊猎刀', + knife_t: 'T刀', + knife_tactical: '猎杀者匕首', + knife_twinblade: '双刃匕首', + knife_ursus: '熊刀', + knife_widowmaker: '锯齿爪刀', + knifegg: 'gg刀', + m4a1: 'M4A4', + m4a1_silencer: 'M4A1消音版', + m4a1_silencer_off: 'M4A1无消音器', + m249: 'M249', + mac10: 'MAC-10', + mag7: 'MAG-7', + melee: '幽灵之刃', + molotov: '燃烧弹', + movelinear: '拳击', + mp5sd: 'MP5', + mp7: 'MP7', + mp9: 'MP9', + negev: '内格夫', + nova: '新星', + p90: 'P90', + p250: 'P250', + p2000: 'P2000', + planted_c4_survival: '放置C4生存', + planted_c4: '放置C4', + prop_exploding_barrel: '爆炸桶', + radarjammer: '雷达干扰器', + revolver: '左轮手枪', + sawedoff: '匪喷', + scar20: 'SCAR', + sg556: 'SG553', + shield: '盾牌', + smokegrenade: '烟雾弹', + snowball: '雪球', + spanner: '扳手', + spray0: '喷漆', + ssg08: 'SSG08', + stomp_damage: '踩踏伤害', + tablet: '平板', + tagrenade: '标记手榴弹', + taser: '电击枪', + tec9: 'Tec-9', + tripwirefire_projectile: '绊网火投射物', + tripwirefire: '绊网火', + ump45: 'UMP45', + usp_silencer: 'USP消音版', + usp_silencer_off: 'USP无消音器', + xm1014: 'XM1014连喷', + zone_repulsor: '区域排斥装置', +} diff --git a/web/src/app/hud/deathmsg/index.tsx b/web/src/app/hud/deathmsg/index.tsx new file mode 100644 index 0000000..d89f988 --- /dev/null +++ b/web/src/app/hud/deathmsg/index.tsx @@ -0,0 +1,530 @@ + +import { H2, H3 } from '@/components/Heading' +import { useAutoAnimate } from '@formkit/auto-animate/react' +import { + DeathMsg, + DefaultDeathMsg, + CS2PrefixIconValues, + CS2SuffixIconValues, + CS2Weapon, + CS2WeaponMap, + CS2WeaponValues, + CStrikeWeaponMap, + CStrikeWeaponValues, + CStrikePrefixIconValues, + CStrikeSuffixIconValues, + Camp, + CampValues, +} from './dmsg' +import useDMStore from './store' +import { Save, RefreshCcw, Loader, RotateCcw, Plus, Sparkles, Trash2, Square } from 'lucide-react' +import { Key, useState } from 'react' +import { + Button, + Chip, + Input, + NumberField, + Switch, + Tabs, + cn, + Autocomplete, + useFilter, + ListBox, + SearchField, + EmptyState, +} from '@heroui/react' +import { IoSwapHorizontal } from 'react-icons/io5' + +export default function Page() { + return ( +
+

击杀信息生成

+ + + {/*

施工中... 功能尚未完善

*/} + +
+ + + +
+ +
+ ) +} + +function GameTypeTabs() { + const { gameType, setGameType } = useDMStore() + return ( + setGameType(key.toString())} aria-label="Game Type Variants" variant="secondary"> + + + + CS2 & GO + + + {/* */} + +
+ CStrike + + 测试中 + +
+ +
+
+
+
+ ) +} + +function SettingPanel() { + const { w, h, hidpi, prefix, mockLayout, setW, setH, setHidpi, setPrefix, setMockLayout, reset } = useDMStore() + + return ( +
+
+
+

偏好设置

+ +
+
+ + setW(e)} aria-label="宽度"> + + + + +
+
+ + setH(h)} aria-label="高度"> + + + + +
+
+ 渲染倍率 + setHidpi(hidpi)} aria-label="渲染倍率"> + + + + +
+
+ 文件名前缀 + setPrefix(e.target.value)} aria-label="文件名前缀" /> +
+
+ 模拟游戏布局 + setMockLayout(mockLayout)} aria-label="模拟游戏布局"> + + + + +
+
+
+ ) +} + +function PreviewPanel() { + return ( +
+

预览

+ +
+ ) +} + +function DeathNoticePanel() { + const { dNotices, setDNotice, cstrikeDNotices, saveDNotices, loadDNotices, resetDNotices, addDNotice, generateDNotice, gameType } = useDMStore() + const [parent /* , enableAnimations */] = useAutoAnimate(/* optional config */) + + return ( +
+
+

击杀信息调整

+ + + + + +
+
    + {(gameType === 'cs2' ? dNotices : cstrikeDNotices).map((dNotice: DeathMsg, i: number) => ( + + ))} +
+
+ ) +} + +type DeathNoticeItemProps = { + index: number + deathNotice: DeathMsg + setDNotice: (index: number, dNotice: DeathMsg) => void +} +function DeathNoticeItem({ index, deathNotice, setDNotice }: DeathNoticeItemProps) { + const { removeDNotice, gameType } = useDMStore() + const PrefixIconValues = gameType == 'cs2' ? CS2PrefixIconValues : CStrikePrefixIconValues + const SuffixIconValues = gameType == 'cs2' ? CS2SuffixIconValues : CStrikeSuffixIconValues + + return ( + + ) +} + +type CampButtonProps = { + value: Camp + onChange: (value: Camp) => void + label?: string +} + +function CampButton({ value, onChange, label }: CampButtonProps) { + const { gameType } = useDMStore() + + const getNextCamp = (current: Camp): Camp => { + return current === 'CT' ? 'T' : 'CT' + } + + const getCampColor = (camp: Camp) => { + if (camp === '') return 'border-zinc-300 dark:border-zinc-600 text-zinc-600 dark:text-zinc-400' + if (gameType === 'cs2') { + return camp === 'CT' ? 'border-[#6F9CE6] text-[#6F9CE6]' : 'border-[#EABE54] text-[#EABE54]' + } else { + return camp === 'CT' ? 'border-[#a8d5fe] text-[#a8d5fe]' : 'border-[#f84444] text-[#f84444]' + } + } + + const displayValue = value || '—' + + return ( + + ) +} + +type SelectSearchProps = { + value: string + onChange: (value: string) => void + values: string[] + valueMap: Record +} + +function SelectSearch({ value, onChange, values, valueMap }: SelectSearchProps) { + const { contains } = useFilter({ sensitivity: 'base' }) + const { gameType } = useDMStore() + + // Prepare items for Autocomplete + const items = values.map(v => ({ id: v, name: valueMap[v] || v })) + + return ( + onChange((key as string) || '')} + aria-label="选择武器" + > + + + {({ selectedItem, isPlaceholder }) => { + const v = value + if (isPlaceholder || !v) return 选择武器 + return ( +
+ {v + {valueMap[v] || v} +
+ ) + }} +
+ +
+ + + + + + + + + 没有找到武器装备}> + {items.map(item => ( + +
+ weapon + {item.name} +
+ +
+ ))} +
+
+
+
+ ) +} + +type DeathNoticeRenderProps = { + hide?: boolean +} + +function DeathNoticeRender({ hide = false }: DeathNoticeRenderProps) { + const { dNotices, cstrikeDNotices, gameType } = useDMStore() + const [parent /* , enableAnimations */] = useAutoAnimate(/* optional config */) + + return ( +
    + {(gameType === 'cs2' ? dNotices : cstrikeDNotices).map((dNotice: DeathMsg, index: number) => + gameType === 'cs2' ? ( + + ) : ( + + ) + )} +
+ ) +} + +function CS2DeathNoticeItem({ dNotice, index, hide }: { dNotice: DeathMsg; index: number; hide: boolean }) { + return ( +
  • +

    {dNotice.attacker}

    + {dNotice.prefixIcons && + dNotice.prefixIcons.map((prefixIcon: string, i: number) => prefix)} + weapon + {dNotice.suffixIcons && + dNotice.suffixIcons.map((suffixIcon: string, i: number) => suffix)} + +

    {dNotice.victim}

    +
  • + ) +} + +function CStrikeDeathNoticeItem({ dNotice, index, hide }: { dNotice: DeathMsg; index: number; hide: boolean }) { + // TODO 图片后处理 + return ( +
  • + {dNotice.prefixIcons && + dNotice.prefixIcons.map((prefixIcon: string, i: number) => ( + prefix + ))} +

    {dNotice.attacker}

    + weapon + {dNotice.suffixIcons && + dNotice.suffixIcons.map((suffixIcon: string, i: number) => ( + suffix + ))} + +

    {dNotice.victim}

    +
  • + ) +} + +function DeathNoticeCanvas() { + const { w, h } = useDMStore() + const dw = `${w}px` + const dh = `${h}px` + + return ( +
    + +
    + ) +} diff --git a/web/src/app/hud/deathmsg/store.ts b/web/src/app/hud/deathmsg/store.ts new file mode 100644 index 0000000..65111e6 --- /dev/null +++ b/web/src/app/hud/deathmsg/store.ts @@ -0,0 +1,179 @@ +import { create } from 'zustand' +import { CStrikeDefaultDeathMsgs, DeathMsg, DefaultDeathMsgs } from './dmsg' +import { persist, createJSONStorage } from 'zustand/middleware' +import { Canvas2Image } from './canvas' +import { toast } from '@heroui/react' + +interface DMState { + w: number + h: number + hidpi: number + prefix: string + gameType: string + dNotices: DeathMsg[] + cstrikeDNotices: DeathMsg[] + mockLayout: boolean + setW: (w: number) => void + setH: (h: number) => void + setHidpi: (hidpi: number) => void + setPrefix: (prefix: string) => void + setGameType: (gameType: string) => void + reset: () => void + setDNotices: (dNotices: DeathMsg[]) => void + setDNotice: (index: number, dNotice: DeathMsg) => void + addDNotice: (dNotice: DeathMsg) => void + removeDNotice: (index: number) => void + resetDNotices: () => void + saveDNotices: () => void + loadDNotices: (json: string) => void + generateDNotice: () => void + setMockLayout: (mockLayout: boolean) => void +} + +// 创建 store +const useDMStore = create()( + persist( + (set, get) => ({ + w: 1920, + h: 1080, + hidpi: 2, + prefix: '击杀生成', + gameType: 'cs2', + dNotices: DefaultDeathMsgs, + cstrikeDNotices: CStrikeDefaultDeathMsgs, + mockLayout: true, + setW: (w: number) => set({ w }), + setH: (h: number) => set({ h }), + setHidpi: (hidpi: number) => set({ hidpi }), + setPrefix: (prefix: string) => set({ prefix }), + setGameType: (gameType: string) => set({ gameType }), + reset: () => set({ w: 1920, h: 1080, hidpi: 2, prefix: '击杀生成', dNotices: DefaultDeathMsgs, mockLayout: true }), + setDNotices: (dNotices: DeathMsg[]) => { + get().gameType === 'cs2' ? set({ dNotices: dNotices }) : set({ cstrikeDNotices: dNotices }) + }, + setDNotice: (index: number, dNotice: DeathMsg) => { + get().gameType === 'cs2' + ? set((state: DMState) => ({ dNotices: [...state.dNotices.slice(0, index), dNotice, ...state.dNotices.slice(index + 1)] })) + : set((state: DMState) => ({ cstrikeDNotices: [...state.cstrikeDNotices.slice(0, index), dNotice, ...state.cstrikeDNotices.slice(index + 1)] })) + }, + addDNotice: (dNotice: DeathMsg) => { + get().gameType === 'cs2' + ? set((state: DMState) => ({ dNotices: [...state.dNotices, dNotice] })) + : set((state: DMState) => ({ cstrikeDNotices: [...state.cstrikeDNotices, dNotice] })) + }, + removeDNotice: (index: number) => { + get().gameType === 'cs2' + ? set((state: DMState) => ({ dNotices: state.dNotices.filter((_, i: number) => i !== index) })) + : set((state: DMState) => ({ cstrikeDNotices: state.cstrikeDNotices.filter((_, i: number) => i !== index) })) + }, + resetDNotices: () => { + get().gameType === 'cs2' ? set({ dNotices: DefaultDeathMsgs }) : set({ cstrikeDNotices: CStrikeDefaultDeathMsgs }) + }, + saveDNotices: () => { + const jsonData = JSON.stringify({ dNotices: get().gameType === 'cs2' ? get().dNotices : get().cstrikeDNotices }) + // 弹出下载 + const blob = new Blob([jsonData], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = get().prefix + '_dNotices.json' + a.style.display = 'none' + document.body.appendChild(a) + a.click() + // 等待一小段时间确保下载已触发,然后再移除元素 + setTimeout(() => { + if (a.parentNode) { + document.body.removeChild(a) + } + URL.revokeObjectURL(url) + }, 100) + }, + loadDNotices: (json: string) => { + // 上传文件并读取json + // 弹出文件上传对话框 + // set({ dNotices: JSON.parse(json).dNotices }) + }, + generateDNotice: async () => { + const { w, h, hidpi, prefix, dNotices, cstrikeDNotices, gameType } = get() + const currentDNotices = gameType === 'cs2' ? dNotices : cstrikeDNotices + + // html2canvas获取元素、生成图片、并跳转下载 + const e = document.getElementById('deathnotice') + if (e === null) return + + // 生产环境适配 + const style = document.createElement('style') + if (process.env.NODE_ENV === 'production') { + document.head.appendChild(style) + style.sheet?.insertRule('body > div:last-child img { display: inline-block; }') + } + + const dpi = hidpi ? hidpi : 1 // 缩放倍率,不随浏览器缩放改变 + + const mockLayoutGeneration = async () => { + // 生成过程 模拟游戏布局 + for (let i = 0; i < currentDNotices.length; i++) { + // 隐藏其他击杀条 + for (let j = 0; j < currentDNotices.length; j++) { + const dNotice = currentDNotices[j] + get().setDNotice(j, { ...dNotice, hide: j !== i }) + } + + await sleep(50) + + await Canvas2Image(e, prefix + '-' + (i + 1), dpi) + } + + for (let j = 0; j < currentDNotices.length; j++) { + const dNotice = currentDNotices[j] + get().setDNotice(j, { ...dNotice, hide: false }) + } + } + + const simpleGeneration = async () => { + // 分别渲染 DeathNoticeRender 的所有 li组件,并下载png + for (let i = 0; i < currentDNotices.length; i++) { + get().setDNotice(i, { ...currentDNotices[i], hide: false }) + } + + const dnList = e.getElementsByTagName('li') + let i = 1 + for (const dn of dnList) { + await sleep(10) + await Canvas2Image(dn, prefix + '-' + i, dpi) + i++ + } + } + + try { + if (get().mockLayout) { + await mockLayoutGeneration() + } else { + await simpleGeneration() + } + } finally { + // 确保 style 元素被移除 + if (style.parentNode) { + style.remove() + } + } + toast('生成成功', { + description: '请查看下载文件夹', + }) + }, + setMockLayout: (mockLayout: boolean) => set({ mockLayout }), + }), + { + name: 'deathmsg', + storage: createJSONStorage(() => sessionStorage), + } + ) +) + +function sleep(time: number) { + return new Promise(function (resolve) { + setTimeout(resolve, time) + }) +} + +export default useDMStore diff --git a/web/src/app/hud/layout.tsx b/web/src/app/hud/layout.tsx new file mode 100644 index 0000000..7f3c41d --- /dev/null +++ b/web/src/app/hud/layout.tsx @@ -0,0 +1,7 @@ +export default function HudLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return
    {children}
    +} diff --git a/web/src/app/index.tsx b/web/src/app/index.tsx index 2d337dc..9cb93e7 100644 --- a/web/src/app/index.tsx +++ b/web/src/app/index.tsx @@ -6,6 +6,7 @@ import { CreateResourceModal } from '../components/CreateResourceModal' import { EditResourceModal } from '../components/EditResourceModal' import { useSession } from '../lib/auth-client' import { Card } from '../components/Card' +import { CardSkeleton } from '../components/CardSkeleton' import { ResourceCardList } from '../components/ResourceCardList' export default function HomePage() { @@ -25,7 +26,11 @@ export default function HomePage() {

    传送门

    {isPortalsLoading ? ( -
    加载中...
    +
      + {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
    ) : (
      {portals?.map((portal) => ( @@ -49,7 +54,7 @@ export default function HomePage() { {session && (