Files
SoundGame/web/hunt/gen/gen_cover.py
T
xsl 005851d01a feat(hunt): 《围猎》2-4人实时联机纯音频协作守卫——纯音频MMORPG第一阶段切片
- 设计: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端到端冒烟通过
2026-09-13 11:08:01 +08:00

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)