114 lines
9.2 KiB
Python
114 lines
9.2 KiB
Python
"""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()
|