- 设计:docs/design/23-hunt.md + voice-ids(84 id,166条)+ 13号断言登记,§15记录10+1条设计裁决 - 服务器:零依赖WebSocket(ws.js)+ 房间管理/服务端权威战斗/bot AI(hunt-room.js),断线30s宽限resume回槽、超时转bot;修复package.json模块类型误判的既有破坏 - 客户端:web/hunt/ 全套(教学T0-T4/听觉视野裁剪R1-R8/结算排行榜/guide接入),playtest 211断言 - 引擎:audio.js 后向兼容 opts.pan(StereoPannerNode),web-engine-api.md 同步 - 资产:166条TTS语音(QC无离群)+ 13件合成sfx(真峰值≤-3dBFS)+ 2首Mureka BGM,终检全绿 - 平衡:bot调参至挂机局71%失守/C、3bot局71% A/B(hunt-balance-scan.mjs 24种子扫描) - 接线:大厅卡片、profiles.js hunt桶、README/roadmap MMO路线条目 验证:hunt-sim 16/16 + api-smoke 12/12 + hunt playtest 211/211 + 既有10游戏与引擎回归零回退 + 真服务器WS端到端冒烟通过
117 lines
4.7 KiB
Python
117 lines
4.7 KiB
Python
"""围猎 cover.png 生成:1080x1920,夜幕山林底 + 篝火辉光 + 八方位刻度 + 游戏名 + 玩法提示。"""
|
|
import math
|
|
import random
|
|
|
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageChops
|
|
|
|
W, H = 1080, 1920
|
|
FONT = "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc"
|
|
OUT = "/home/xsl/AutoVideo/SoundGame/web/hunt/cover.png"
|
|
|
|
random.seed(23)
|
|
|
|
img = Image.new("RGB", (W, H))
|
|
px = img.load()
|
|
# 夜幕竖向渐变:上 #060913 -> 下 #1a1206(底部篝火映暖)
|
|
top, bottom = (6, 9, 19), (26, 18, 6)
|
|
for y in range(H):
|
|
t = y / (H - 1)
|
|
for x in range(W):
|
|
px[x, y] = tuple(int(a + (b - a) * t) for a, b in zip(top, bottom))
|
|
|
|
d = ImageDraw.Draw(img)
|
|
# 星点
|
|
for _ in range(260):
|
|
x, y = random.randint(0, W - 1), random.randint(0, int(H * 0.55))
|
|
c = random.randint(100, 220)
|
|
d.point((x, y), fill=(c, c, min(255, c + 15)))
|
|
|
|
# 月亮(右上,冷光)
|
|
moon = Image.new("RGB", (W, H), (0, 0, 0))
|
|
md = ImageDraw.Draw(moon)
|
|
md.ellipse([W * 0.66, H * 0.07, W * 0.86, H * 0.07 + W * 0.20], fill=(200, 214, 235))
|
|
moon = moon.filter(ImageFilter.GaussianBlur(2))
|
|
img = ImageChops.add(img, ImageChops.multiply(moon, Image.new("RGB", (W, H), (1, 1, 1)).point(lambda v: v)))
|
|
# 直接画月盘 + 光晕
|
|
d = ImageDraw.Draw(img)
|
|
d.ellipse([W * 0.68, H * 0.075, W * 0.84, H * 0.075 + W * 0.16], fill=(214, 224, 240))
|
|
mglow = Image.new("RGB", (W, H), (0, 0, 0))
|
|
mg = ImageDraw.Draw(mglow)
|
|
mg.ellipse([W * 0.63, H * 0.05, W * 0.89, H * 0.05 + W * 0.26], fill=(40, 48, 66))
|
|
mglow = mglow.filter(ImageFilter.GaussianBlur(40))
|
|
img = ImageChops.add(img, mglow)
|
|
d = ImageDraw.Draw(img)
|
|
d.ellipse([W * 0.68, H * 0.075, W * 0.84, H * 0.075 + W * 0.16], fill=(214, 224, 240))
|
|
|
|
# 远山与林木剪影(两层)
|
|
d.polygon([(0, H * 0.52), (W * 0.22, H * 0.40), (W * 0.45, H * 0.50),
|
|
(W * 0.70, H * 0.38), (W, H * 0.50), (W, H), (0, H)], fill=(8, 10, 16))
|
|
for i in range(26):
|
|
x = random.randint(0, W)
|
|
base = H * 0.52 + random.randint(-8, 8)
|
|
h = random.randint(60, 150)
|
|
w = random.randint(18, 40)
|
|
d.polygon([(x, base - h), (x - w, base), (x + w, base)], fill=(5, 7, 11))
|
|
|
|
# 八方位刻度环(猎场罗盘,围绕篝火):刻线 + 四正向加亮
|
|
cx, cy, r = W / 2, H * 0.76, W * 0.30
|
|
for k in range(8):
|
|
az = math.radians(k * 45 - 90) # N 在上
|
|
x0, y0 = cx + (r - 26) * math.cos(az), cy + (r - 26) * math.sin(az)
|
|
x1, y1 = cx + r * math.cos(az), cy + r * math.sin(az)
|
|
main = k % 2 == 0
|
|
d.line([x0, y0, x1, y1], fill=(232, 168, 92) if main else (120, 96, 70), width=10 if main else 6)
|
|
# 外环暗弧
|
|
d.ellipse([cx - r - 14, cy - r - 14, cx + r + 14, cy + r + 14], outline=(66, 52, 38), width=4)
|
|
|
|
# 篝火:地面暖光晕 + 火堆
|
|
glow = Image.new("RGB", (W, H), (0, 0, 0))
|
|
gd = ImageDraw.Draw(glow)
|
|
gd.ellipse([cx - 300, cy - 130, cx + 300, cy + 190], fill=(96, 46, 10))
|
|
gd.ellipse([cx - 170, cy - 70, cx + 170, cy + 110], fill=(150, 70, 14))
|
|
glow = glow.filter(ImageFilter.GaussianBlur(60))
|
|
img = ImageChops.add(img, glow)
|
|
d = ImageDraw.Draw(img)
|
|
# 柴堆
|
|
d.line([cx - 90, cy + 66, cx + 90, cy + 30], fill=(26, 16, 8), width=18)
|
|
d.line([cx - 90, cy + 30, cx + 90, cy + 66], fill=(30, 18, 9), width=18)
|
|
# 火焰(三层锥形)
|
|
flame = Image.new("RGB", (W, H), (0, 0, 0))
|
|
fd = ImageDraw.Draw(flame)
|
|
fd.polygon([(cx, cy - 150), (cx - 84, cy + 44), (cx + 84, cy + 44)], fill=(178, 62, 8))
|
|
fd.polygon([(cx, cy - 108), (cx - 56, cy + 40), (cx + 56, cy + 40)], fill=(232, 120, 16))
|
|
fd.polygon([(cx, cy - 62), (cx - 30, cy + 36), (cx + 30, cy + 36)], fill=(255, 196, 66))
|
|
flame = flame.filter(ImageFilter.GaussianBlur(6))
|
|
img = ImageChops.add(img, flame)
|
|
d = ImageDraw.Draw(img)
|
|
|
|
|
|
def text_center(y, s, size, fill):
|
|
f = ImageFont.truetype(FONT, size)
|
|
bbox = d.textbbox((0, 0), s, font=f)
|
|
w = bbox[2] - bbox[0]
|
|
d.text(((W - w) / 2 - bbox[0], y), s, font=f, fill=fill)
|
|
|
|
|
|
# 标题(大字 + 辉光)
|
|
title = "围 猎"
|
|
f = ImageFont.truetype(FONT, 280)
|
|
bbox = d.textbbox((0, 0), title, font=f)
|
|
tx, ty = (W - (bbox[2] - bbox[0])) / 2 - bbox[0], 330
|
|
tglow = Image.new("RGB", (W, H), (0, 0, 0))
|
|
td = ImageDraw.Draw(tglow)
|
|
td.text((tx, ty), title, font=f, fill=(140, 64, 12))
|
|
tglow = tglow.filter(ImageFilter.GaussianBlur(26))
|
|
img = ImageChops.add(img, tglow)
|
|
d = ImageDraw.Draw(img)
|
|
d.text((tx, ty), title, font=f, fill=(248, 226, 188))
|
|
|
|
text_center(700, "NIGHT HUNT", 44, (150, 118, 82))
|
|
text_center(880, "听声辨向,背背守夜", 64, (238, 214, 176))
|
|
text_center(980, "野兽从八个方向围上来,朝声音的方向滑", 40, (160, 136, 104))
|
|
text_center(1058, "单击呼喊支援 · 双击旋风斩 · 撑过五波天亮就赢", 40, (160, 136, 104))
|
|
text_center(1780, "2-4 人联机 · 建议戴耳机", 40, (120, 104, 82))
|
|
|
|
img.save(OUT)
|
|
print("saved", OUT, img.size)
|