Files
AIGame/Tools/generate_home_ui.py
T

114 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Clash of Critters-inspired home layout; JSON is the source of every UI node."""
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ICONS = ['coin', 'diamond', 'energy', 'avatar', 'tasks', 'gift', 'mail', 'trophy',
'bag', 'home', 'battle', 'pets', 'chest', 'calendar', 'settings', 'pinball']
def generate():
theme = 'candy'
name = 'UI_MainHUD'
panel, primary, secondary, ink, muted, light = ('ui_panel_content', 'btn_primary_normal', 'btn_secondary_normal', '#5A4632', '#687C8C', '#F6F8FF')
nodes, events = [], []
def box(x, y, w, h, anchor=(0, 1), pivot=(0, 1)):
return dict(anchorMin=list(anchor), anchorMax=list(anchor), pivot=list(pivot), anchoredPosition=[x, y], sizeDelta=[w, h])
def full():
return dict(anchorMin=[0, 0], anchorMax=[1, 1], offsetMin=[0, 0], offsetMax=[0, 0])
def node(n, p, rect, comps=(), active=True):
nodes.append(dict(name=n, parent=p, active=active, rect=rect, components=list(comps)))
def image(sprite, color='#FFFFFF', ray=False, sliced=True):
return dict(type='Image', sprite=sprite, color=color, raycastTarget=ray, imageType='Sliced' if sliced else 'Simple')
def img(n, p, x, y, w, h, sprite, color='#FFFFFF', ray=False, sliced=True):
node(n, p, box(x, -y, w, h), [image(sprite, color, ray, sliced)])
def label(n, p, x, y, w, h, value, size=30, color=None, align='Left', outlined=False):
comps = [dict(type='TextMeshProUGUI', text=value, fontSize=size, color=color or ink, alignment=align, raycastTarget=False)]
if outlined:
comps.append(dict(type='Outline', effectColor='#102033CC', effectDistance=[1.5, -1.5]))
node(n, p, box(x, -y, w, h), comps)
def action(n, p, x, y, w, h, sprite, event, tint='#FFFFFF'):
b = dict(type='Button', transition='ColorTint', normalColor='#FFFFFF', highlightedColor='#F2FAFF', pressedColor='#C5D6C0', disabledColor='#B9C0C8', onClick=event)
if sprite in [primary, secondary]:
b = dict(type='Button', transition='SpriteSwap', spritePressed=sprite.replace('normal', 'pressed'), onClick=event)
node(n, p, box(x, -y, w, h), [image(sprite, tint, True), b])
events.append(dict(name=event, node=n))
panel_color = '#FFFFFF'
primary_color = '#FFFFFF'
node(name, None, full(), [dict(type='Canvas', sortingOrder=100), dict(type='CanvasScaler', referenceResolution=[1080, 1920], matchWidthOrHeight=0), dict(type='GraphicRaycaster')])
node('Img_Camp', name, full(), [image('home_camp', sliced=False)])
# A fixed portrait composition inside a stretch canvas remains readable on taller phones.
node('Layout_Top', name, box(0, 0, 1080, 460, (0.5, 1), (0.5, 1)))
node('Layout_Sides', name, box(0, 0, 1080, 650, (0.5, 0.5), (0.5, 0.5)))
node('Layout_Bottom', name, box(0, 0, 1080, 600, (0.5, 0), (0.5, 0)))
action('Btn_Profile', 'Layout_Top', 32, 48, 310, 134, panel, 'MainHUD.Profile', panel_color)
img('Img_Avatar', 'Btn_Profile', 10, 6, 114, 114, 'home_ico_avatar', sliced=False)
label('Txt_PlayerName', 'Btn_Profile', 132, 20, 158, 46, '探险家', 34)
label('Txt_Level', 'Btn_Profile', 132, 72, 158, 34, 'Lv. 1', 26, muted)
for key, x, value in [('coin', 356, '0'), ('diamond', 590, '0'), ('pinballs', 824, '20')]:
n = 'Btn_' + key.capitalize()
action(n, 'Layout_Top', x, 72, 220, 82, panel, 'MainHUD.' + key.capitalize(), panel_color)
img('Img_' + key.capitalize(), n, 8, 10, 60, 60, 'home_ico_' + ('pinball' if key == 'pinballs' else key), sliced=False)
label('Txt_' + key.capitalize(), n, 70, 14, 134, 50, value, 30, align='Center')
node('Panel_Top', name, box(0, -214, 452, 86, (0.5, 1), (0.5, 1)), [image('ui_panel_header', panel_color)])
label('Txt_TownTitle', 'Panel_Top', 30, 16, 392, 52, '小镇营地', 42, light, 'Center', True)
label('Txt_Status', 'Layout_Top', 290, 304, 500, 36, '欢迎回到小镇', 22, '#FFFFFF', 'Center', True)
node('Txt_Pid', 'Layout_Top', box(0, 0, 1, 1), [dict(type='TextMeshProUGUI', text='pid=-', fontSize=20, color=muted, raycastTarget=False)], False)
action('Btn_Tasks', 'Layout_Top', 32, 358, 458, 108, panel, 'MainHUD.Tasks', panel_color)
img('Img_TaskIcon', 'Btn_Tasks', 14, 10, 82, 82, 'home_ico_tasks', sliced=False)
label('Txt_TaskCaption', 'Btn_Tasks', 108, 14, 318, 36, '教学目标', 24, muted)
label('Txt_Task', 'Btn_Tasks', 108, 50, 318, 44, '发射第一枚弹珠', 28)
img('Dot_Tasks', 'Btn_Tasks', 422, 0, 24, 24, 'toggle_knob', '#FF5B5B', sliced=False)
action('Btn_Settings', 'Layout_Top', 948, 222, 96, 96, panel, 'MainHUD.Settings', panel_color)
img('Img_Settings', 'Btn_Settings', 16, 16, 64, 64, 'home_ico_settings', sliced=False)
for key, title, x, y, ico, dot in [
('Events', '活动', 32, 4, 'gift', True), ('Daily', '签到', 32, 174, 'calendar', True),
('Rank', '排行', 32, 344, 'trophy', False), ('Mail', '邮件', 902, 4, 'mail', True),
('Rewards', '宝箱', 902, 174, 'chest', True), ('Bag', '背包', 902, 344, 'bag', False)]:
n = 'Btn_' + key
action(n, 'Layout_Sides', x, y, 146, 148, panel, 'MainHUD.' + key, panel_color)
img('Img_' + key, n, 26, 8, 94, 94, 'home_ico_' + ico, sliced=False)
label('Txt_' + key, n, 8, 102, 130, 36, title, 28, align='Center')
if dot:
img('Dot_' + key, n, 116, 2, 24, 24, 'toggle_knob', '#FF5B5B', sliced=False)
img('Panel_Greeting', 'Layout_Sides', 428, 510, 394, 64, panel, panel_color)
label('Txt_Greeting', 'Panel_Greeting', 16, 10, 362, 44, '今天也一起出发吧!', 27, align='Center')
img('Panel_Adventure', 'Layout_Bottom', 32, 12, 1016, 320, panel, panel_color)
label('Txt_Chapter', 'Panel_Adventure', 30, 20, 600, 46, '第 1 章 · 绿野初程', 34)
label('Txt_Stage', 'Panel_Adventure', 740, 26, 240, 36, '当前关卡 1-1', 26, muted, 'Right')
label('Txt_RewardCaption', 'Panel_Adventure', 30, 80, 300, 34, '冒险奖励', 24, muted)
for key, x, value in [('coin', 24, '×80'), ('pinball', 162, '×3')]:
img('Img_Reward_' + key, 'Panel_Adventure', x, 116, 80, 80, 'home_ico_' + ('pinball' if key == 'pinballs' else key), sliced=False)
label('Txt_Reward_' + key, 'Panel_Adventure', x - 6, 200, 100, 36, value, 26, align='Center')
action('Btn_Adventure', 'Panel_Adventure', 362, 90, 618, 138, primary, 'MainHUD.Adventure', primary_color)
img('Img_Adventure', 'Btn_Adventure', 30, 22, 92, 92, 'home_ico_battle', sliced=False)
label('Txt_Adventure', 'Btn_Adventure', 134, 34, 450, 62, '开始冒险', 48, light, 'Center', True)
label('Txt_AdventureHint', 'Panel_Adventure', 362, 244, 618, 42, '首通另获糖果 40 · 不消耗体力', 25, muted, 'Center')
img('Panel_Navigation', 'Layout_Bottom', 32, 356, 1016, 180, panel, panel_color)
for i, (key, caption, ico) in enumerate([('Shop', '商店', 'chest'), ('Pets', '伙伴', 'pets'), ('Home', '营地', 'home'), ('Battle', '冒险', 'battle'), ('Pinball', '弹珠', 'pinball')]):
n = 'Btn_Nav' + key
action(n, 'Panel_Navigation', 12 + i * 198, 8, 194, 164, primary if key == 'Home' else panel, 'MainHUD.' + key, primary_color if key == 'Home' else panel_color)
img('Img_Nav' + key, n, 49, 8, 96, 96, 'home_ico_' + ico, sliced=False)
label('Txt_Nav' + key, n, 12, 114, 170, 40, caption, 30, light if key == 'Home' else ink, 'Center', key == 'Home')
# Last sibling overlays the scene but does not cover bottom navigation.
node('Panel_Reconnect', name, box(0, 632, 1016, 154, (0.5, 0), (0.5, 0)), [image(panel, panel_color, True)], False)
label('Txt_Reconnect', 'Panel_Reconnect', 26, 24, 638, 104, '网络已断开,正在重连…', 27)
action('Btn_Reconnect', 'Panel_Reconnect', 690, 34, 294, 88, primary, 'MainHUD.Reconnect', primary_color)
label('Txt_ReconnectBtn', 'Btn_Reconnect', 14, 16, 266, 56, '立即重连', 30, light, 'Center', True)
bindings = ['Txt_Status', 'Txt_Pid', 'Panel_Reconnect', 'Txt_Reconnect', 'Btn_Reconnect', 'Txt_PlayerName', 'Txt_Level', 'Txt_Coin', 'Txt_Diamond', 'Txt_Pinballs', 'Txt_Chapter', 'Txt_Stage', 'Txt_Task']
bindings += [e['node'] for e in events if e['node'] not in bindings]
sprites = {c[k] for n in nodes for c in n['components'] for k in ['sprite', 'spritePressed'] if c.get(k)}
def path(s):
folder = 'Home' if s.startswith('home_') else 'Common'
return f'Assets/Bundles/UI/Texture/{folder}/{s}.png'
doc = dict(schemaVersion=1, prefabName=name, prefabPath=f'Assets/Bundles/UI/Prefab/{name}.prefab',
description=f'HUD 主界面 / {theme}。参考 Clash of Critters 营地式入口布局,采用项目主题;1080×1920,JSON→Prefab。中央为原创营地插画,G0 资源与训练/教学关进度由 PrototypeGame 存档绑定,功能入口通过 MainHUD.* 事件接入。保留网络状态和重连绑定,无主题切换按钮。',
canvas=dict(canvasScaler=dict(referenceResolution=[1080, 1920], matchWidthOrHeight=0)),
assets=dict(sprites={s:path(s) for s in sorted(sprites)}), nodes=nodes, bindings=bindings, events=events)
(ROOT / 'Doc/UIPrefabCreater' / (name + '.json')).write_text(json.dumps(doc, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
print(f'{name}: {len(nodes)} nodes, {len(events)} events')
if __name__ == '__main__':
generate()