"""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()