"""Author the task journal, seven-day rewards and mailbox via JSON -> Prefab.""" import json from pathlib import Path ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / 'Doc/UIPrefabCreater' INK = '#29465FFF' MUTED = '#647B8BFF' class Page: def __init__(self, key, title, subtitle, icon, height): self.name = 'UI_Prototype' + key self.data = dict(schemaVersion=1, prefabName=self.name, prefabPath=f'Assets/Bundles/UI/Prefab/{self.name}.prefab', description='Normal 层营地冒险手册:安全区、可滚动内容、奖励优先;由 JSON 生成,沿用服务端奖励规则。', canvas={'canvasScaler': {'referenceResolution': [1080, 1920], 'matchWidthOrHeight': 0}}, assets={'sprites': {}}, nodes=[], bindings=[], events=[]) self.node(self.name, None, self.stretch(), [dict(type='Canvas', sortingOrder=100), dict(type='CanvasScaler', referenceResolution=[1080, 1920], matchWidthOrHeight=0), dict(type='GraphicRaycaster')]) self.data['assets']['sprites']['journal_paper_v1'] = 'Assets/Bundles/UI/Bg/journal_paper_v1.png' self.node('Background', self.name, self.stretch(), [ dict(type='Image', sprite='journal_paper_v1', imageType='Simple', color='#FFFFFFFF', raycastTarget=True), dict(type='MonoBehaviour', typeName='AOT.UI.BootArtworkCover')]) self.node('SafeArea', self.name, self.stretch(), [dict(type='MonoBehaviour', typeName='AOT.UI.UiSafeArea')]) self.node('Header', 'SafeArea', dict(anchorMin=[0, 1], anchorMax=[1, 1], pivot=[.5, 1], anchoredPosition=[0, 0], sizeDelta=[0, 164])) self.button('Back', 'Header', 36, 26, 104, 104, '', icon='btn_back') self.text('Title', 'Header', 166, 22, 690, 68, title, 50) self.text('Subtitle', 'Header', 170, 94, 690, 42, subtitle, 26, MUTED) self.icon('HeaderEmblem', 'Header', 910, 24, 110, 110, icon) self.node('Scroll_Page', 'SafeArea', self.stretch([0, 22], [0, -170]), [dict(type='ScrollRect', horizontal=False, vertical=True, viewport='Viewport_Page', content='Content_Page')]) self.node('Viewport_Page', 'Scroll_Page', self.stretch(), [dict(type='Image', color='#FFFFFF00', raycastTarget=True), dict(type='RectMask2D')]) self.node('Content_Page', 'Viewport_Page', dict(anchorMin=[0, 1], anchorMax=[1, 1], pivot=[.5, 1], anchoredPosition=[0, 0], sizeDelta=[0, height])) @staticmethod def stretch(lo=None, hi=None): return dict(anchorMin=[0, 0], anchorMax=[1, 1], offsetMin=lo or [0, 0], offsetMax=hi or [0, 0]) @staticmethod def rect(x, y, w, h): return dict(anchorMin=[0, 1], anchorMax=[0, 1], pivot=[0, 1], anchoredPosition=[x, -y], sizeDelta=[w, h]) def node(self, name, parent, rect, components=None, active=True): self.data['nodes'].append(dict(name=name, parent=parent, active=active, rect=rect, components=components or [])) return name def sprite(self, name): folder = 'Journal' if name.startswith('journal_') else 'Home' if name.startswith('home_') else 'Prototype' if name.startswith('proto_') else 'Common' self.data['assets']['sprites'][name] = f'Assets/Bundles/UI/Texture/{folder}/{name}.png' return name def image(self, name, parent, x, y, w, h, sprite, color='#FFFFFFFF', simple=False, raycast=False): return self.node(name, parent, self.rect(x, y, w, h), [dict(type='Image', sprite=self.sprite(sprite), imageType='Simple' if simple else 'Sliced', color=color, preserveAspect=simple, raycastTarget=raycast)]) def icon(self, name, parent, x, y, w, h, sprite): return self.image(name, parent, x, y, w, h, sprite, simple=True) def card(self, name, y, h, sprite='journal_card', color='#FFFFFFFF', x=40, w=1000, parent='Content_Page'): return self.image(name, parent, x, y, w, h, sprite, color) def text(self, name, parent, x, y, w, h, text, size=32, color=INK, align='Left'): node = 'Txt_' + name self.node(node, parent, self.rect(x, y, w, h), [dict(type='TextMeshProUGUI', text=text, fontSize=size, color=color, alignment={'MidlineLeft':'Left','MidlineRight':'Right','TopLeft':'Left'}.get(align,align), raycastTarget=False)]) if parent in ['TaskSummary','MailWallet']: self.data['nodes'][-1]['components'].append(dict(type='Shadow',effectColor='#10203360',effectDistance=[0,-1])) self.data['bindings'].append(node) return node def button(self, name, parent, x, y, w, h, text, primary=False, icon=None, active=True): sprite = icon or ('journal_primary_normal' if primary else 'journal_secondary_normal') node = self.image('Btn_' + name, parent, x, y, w, h, sprite, simple=icon is not None, raycast=True) obj = self.data['nodes'][-1] obj['active'] = active obj['components'].append(dict(type='Button', transition='ColorTint' if icon else 'SpriteSwap', spriteHighlighted=self.sprite(sprite), spritePressed=self.sprite(sprite if icon else sprite.replace('normal', 'pressed')), spriteDisabled=self.sprite('journal_disabled'), onClick='Prototype.' + name)) if not icon: label = self.text(name, node, 12, 8, w-24, h-16, text, 32, '#5D431FFF' if primary else INK, 'Center') self.data['bindings'].append(node) self.data['events'].append(dict(name='Prototype.' + name, node=node)) return node def progress(self, name, parent, x, y, w, h=22): self.image(name + 'Track', parent, x, y, w, h, 'journal_progress_bg') self.image(name + 'Fill', parent, x, y, w, h, 'journal_progress_fill') def save(self): (OUT / (self.name + '.json')).write_text(json.dumps(self.data, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') def tasks(): p = Page('Tasks', '冒险手册', '每天一小步,营地更热闹', 'home_ico_tasks', 1718) p.card('TaskSummary', 12, 250, 'journal_hero') p.text('TodayCaption', 'TaskSummary', 36, 28, 580, 44, '今日旅程', 30, '#F4CE88FF') p.text('TaskSummary', 'TaskSummary', 36, 82, 660, 70, '0 / 3 份奖励已领取', 46, '#FFFFFFFF') p.text('TaskReset', 'TaskSummary', 38, 170, 660, 42, '每日 00:00 更新', 26, '#C2D5DFFF') p.icon('TodayReward', 'TaskSummary', 778, 32, 176, 176, 'home_ico_chest') for i in range(3): p.image('DailyMilestone' + str(i), 'TaskSummary', 38+i*216, 222, 198, 12, 'journal_progress_fill', '#ACC5D6FF') p.text('DailyHeading', 'Content_Page', 52, 290, 500, 54, '每日委托', 36) p.text('DailyHint', 'Content_Page', 600, 296, 422, 42, '完成即可领取奖励', 26, MUTED, 'MidlineRight') icons = ['home_ico_pinball', 'home_ico_battle', 'home_ico_pets'] titles = ['完成3次弹珠发射', '完成1场冒险', '与伙伴互动1次'] for i in range(3): card = 'TaskCard' + str(i+1) p.card(card, 360+i*236, 216) p.icon('DailyIcon'+str(i), card, 24, 40, 112, 112, icons[i]) p.text('DailyName'+str(i), card, 158, 24, 560, 50, titles[i], 36) p.text('DailyInfo'+str(i), card, 160, 80, 540, 40, '完成一次结算即可计入', 26, MUTED) p.progress('DailyProgress'+str(i), card, 160, 145, 350, 20) p.text('DailyCount'+str(i), card, 532, 128, 170, 50, '0 / 3', 30, MUTED, 'MidlineRight') p.text('DailyReward'+str(i), card, 734, 26, 236, 54, '弹珠 ×5', 34, '#99641FFF', 'Center') p.button('ClaimDaily'+str(i), card, 738, 102, 224, 92, '领取奖励', True, active=False) p.button('GoDaily'+str(i), card, 738, 102, 224, 92, '去完成') p.text('DailyDone'+str(i), card, 738, 118, 224, 60, '已领取', 32, '#698479FF', 'Center') p.data['nodes'][-1]['active'] = False p.text('GrowthHeading', 'Content_Page', 52, 1090, 800, 54, '成长足迹', 36) p.card('TaskCard0', 1162, 200, color='#E5EDF2FF') p.icon('GoalIcon', 'TaskCard0', 24, 40, 112, 112, 'home_ico_home') p.text('GoalName', 'TaskCard0', 158, 22, 550, 50, '营地初守', 36) p.text('GoalInfo', 'TaskCard0', 160, 82, 540, 74, '首次通关 1-1\n成长奖励永久保留', 26, MUTED) p.text('GoalReward', 'TaskCard0', 734, 22, 236, 54, '弹珠 ×5', 34, '#99641FFF', 'Center') p.button('ClaimGoal', 'TaskCard0', 738, 98, 224, 88, '领取奖励', True, active=False) p.button('GoGoal', 'TaskCard0', 738, 98, 224, 88, '去冒险') p.text('GoalDone', 'TaskCard0', 738, 114, 224, 60, '已领取', 32, '#698479FF', 'Center') p.data['nodes'][-1]['active'] = False p.card('SigninEntry', 1396, 156, 'journal_gold_card') p.icon('SigninEntryIcon', 'SigninEntry', 24, 20, 116, 116, 'home_ico_calendar') p.text('SigninEntryTitle', 'SigninEntry', 164, 28, 520, 52, '七日同行礼', 36) p.text('SigninEntryHint', 'SigninEntry', 166, 90, 520, 40, '累计登录,每一天都有收获', 26, MUTED) p.button('ToSignin', 'SigninEntry', 738, 34, 224, 88, '查看奖励') p.text('TaskRules', 'Content_Page', 64, 1590, 952, 84, '每日奖励请在重置前领取\n成长奖励仅可领取一次', 25, MUTED, 'Center') p.save() def signin(): p = Page('Signin', '七日同行礼', '每一次回到营地,都值得期待', 'home_ico_calendar', 1700) p.card('SigninSummary', 12, 258, 'journal_gold_card') p.text('SigninCaption', 'SigninSummary', 36, 28, 650, 44, '把小小的收获,攒成大大的惊喜', 30) p.text('SigninSummary', 'SigninSummary', 36, 82, 690, 70, '已相伴 1 天', 48) p.text('SigninHint', 'SigninSummary', 38, 176, 650, 42, '漏登不重置 · 已解锁奖励可补领', 26, MUTED) p.icon('SigninHeroGift', 'SigninSummary', 786, 42, 176, 176, 'home_ico_gift') p.text('SigninSection', 'Content_Page', 52, 302, 800, 54, '你的七日礼物', 36) for i in range(7): special = i == 6 card = 'SigninCard' + str(i) x, y, w, h = (40, 1138, 1000, 300) if special else (40+(i%3)*340, 382+(i//3)*366, 320, 342) p.card(card, y, h, 'journal_gold_card' if special else 'journal_card', x=x, w=w) p.text('SigninDay'+str(i), card, 28, 18, w-56, 48, '第 7 天 · 同行大礼' if special else f'第 {i+1} 天', 34, INK, 'MidlineLeft' if special else 'Center') p.icon('SigninIcon'+str(i), card, 44 if special else 104, 90 if special else 72, 170 if special else 112, 170 if special else 112, 'home_ico_chest' if special else 'home_ico_pinball') p.text('SigninReward'+str(i), card, 260 if special else 18, 96 if special else 194, 420 if special else 284, 50, '弹珠 ×'+str([10,5,5,10,5,10,20][i]), 40 if special else 32, '#99641FFF', 'MidlineLeft' if special else 'Center') if special: p.text('FinalGiftHint', card, 260, 174, 420, 48, '累计登录七天即可解锁', 26, MUTED) p.button('Signin'+str(i), card, 718 if special else 28, 174 if special else 252, 246 if special else 264, 88 if special else 78, '领取', True) # Locked/claimed days are labels, never disabled fake actions. p.text('SigninState'+str(i), card, 718 if special else 28, 174 if special else 270, 246 if special else 264, 88 if special else 60, '等待解锁', 28, MUTED, 'Center') p.button('ToTasks', 'Content_Page', 260, 1484, 560, 96, '去完成今日委托') p.text('SigninRules', 'Content_Page', 64, 1604, 952, 60, '每日首次登录计入天数 · 每份奖励仅可领取一次', 25, MUTED, 'Center') p.save() def mail(): p = Page('Mail', '营地信箱', '来自小镇的问候与补给', 'home_ico_mail', 1840) p.card('MailWallet', 12, 172, 'journal_hero') p.icon('MailHero', 'MailWallet', 24, 24, 120, 120, 'home_ico_mail') p.text('MailWallet', 'MailWallet', 170, 26, 780, 62, '营地来信', 42, '#FFFFFFFF') p.text('MailSummaryHint', 'MailWallet', 172, 100, 780, 42, '点击来信,查看消息与随信礼物', 26, '#C2D5DFFF') p.text('InboxHeading', 'Content_Page', 52, 210, 500, 54, '收到的来信', 36) p.node('MailStack', 'Content_Page', p.rect(40, 284, 1000, 0), [ dict(type='VerticalLayoutGroup', spacing=24, childControlWidth=True, childControlHeight=True, childForceExpandWidth=True, childForceExpandHeight=False), dict(type='ContentSizeFitter', verticalFit='PreferredSize')]) for i in range(5): b=p.button('Mail'+str(i), 'Content_Page', 40, 284+i*126, 1000, 114, '') n=next(n for n in p.data['nodes'] if n['name']==b) n['components'][0]['sprite']=p.sprite('journal_card') n['components'][1].update(spriteHighlighted=p.sprite('journal_card'),spritePressed=p.sprite('journal_secondary_pressed'),spriteDisabled=p.sprite('journal_card')) # The generic label is intentionally replaced by title and status. p.data['nodes']=[n for n in p.data['nodes'] if n['name']!='Txt_Mail'+str(i)] p.data['bindings'].remove('Txt_Mail'+str(i)) p.icon('MailIcon'+str(i), b, 20, 17, 80, 80, 'home_ico_mail') p.text('MailTitle'+str(i), b, 128, 12, 800, 48, '营地来信', 34) p.text('MailStatus'+str(i), b, 130, 66, 800, 36, '未读 · 附件待领取', 25, MUTED) p.image('MailUnread'+str(i), b, 950, 28, 16, 16, 'journal_progress_fill') p.text('MailEmpty', 'Content_Page', 100, 440, 880, 180, '信箱暂时空空的\n下一份问候,正在路上', 36, MUTED, 'Center') p.data['nodes'][-1]['active']=False p.node('MailPager', 'MailStack', p.rect(0,0,1000,76), [dict(type='LayoutElement', preferredHeight=76)]) p.button('MailPrev', 'Content_Page', 48, 930, 184, 76, '上一页') p.text('MailPage', 'Content_Page', 260, 938, 560, 60, '1 / 1 页', 28, MUTED, 'Center') p.button('MailNext', 'Content_Page', 848, 930, 184, 76, '下一页') p.card('MailDetail', 1040, 502) p.text('MailDetailTitle', 'MailDetail', 32, 26, 936, 60, '请选择一封邮件', 38) p.text('MailDetailStatus', 'MailDetail', 34, 96, 932, 42, '查看来自营地的消息', 26, MUTED) p.node('MailBodyScroll', 'MailDetail', p.rect(34, 156, 932, 164), [dict(type='ScrollRect',horizontal=False,vertical=True,viewport='MailBodyViewport',content='Txt_MailBody')]) p.node('MailBodyViewport', 'MailBodyScroll', p.stretch(), [dict(type='Image',color='#FFFFFF00',raycastTarget=True),dict(type='RectMask2D')]) p.text('MailBody','MailBodyViewport',0,0,920,164,'营地来信和奖励补给会显示在这里。',30,INK,'TopLeft') p.data['nodes'][-1]['components'].append(dict(type='ContentSizeFitter',verticalFit='PreferredSize')) p.text('MailAttachment','MailDetail',34,338,932,58,'',30,'#99641FFF') p.button('ClaimMail','MailDetail',590,414,374,70,'领取附件',True) p.button('ClaimAllMail','Content_Page',240,1580,600,104,'领取全部附件',True) p.text('MailRules','Content_Page',64,1720,952,80,'附件领取后直接到账\n一键最多领取 20 封,请在到期前领取',25,MUTED,'Center') stack_heights = {'Btn_Mail'+str(i):114 for i in range(5)} stack_heights.update(Txt_MailEmpty=240,MailDetail=502,Btn_ClaimAllMail=104,Txt_MailRules=80) for n in p.data['nodes']: if n['name'] in stack_heights: n['parent']='MailStack' n['components'].append(dict(type='LayoutElement',preferredHeight=stack_heights[n['name']])) if n['name'] in ['Btn_MailPrev','Txt_MailPage','Btn_MailNext']: n['parent']='MailPager' n['rect']['anchoredPosition'][0]-=40 n['rect']['anchoredPosition'][1]+=930 p.save() def challenges(): p = Page('Challenges', '属性试炼', '元素相生相克 · 携同调伙伴挑战强敌', 'home_ico_battle', 1830) # 1. Hero Card p.card('ChallengeSummary', 12, 210, 'journal_hero') p.text('HeroCaption', 'ChallengeSummary', 36, 26, 620, 44, '元素试炼 · 专属徽章与伙伴碎片', 30, '#F4CE88FF') p.text('Summary', 'ChallengeSummary', 36, 78, 680, 64, '通过 1-1 后开放。只上阵对应元素,首次通过获得徽章和碎片。', 32, '#FFFFFFFF') p.text('HeroHint', 'ChallengeSummary', 38, 150, 680, 40, '试炼不消耗弹珠 · 首胜获10碎片与专属徽章', 26, '#C2D5DFFF') p.icon('ChallengeHero', 'ChallengeSummary', 778, 24, 160, 160, 'home_ico_battle') # 2. Section Heading p.text('TrialHeading', 'Content_Page', 52, 244, 500, 54, '五行试炼', 36) p.text('TrialSub', 'Content_Page', 560, 250, 460, 42, '点击切换试炼项目', 26, MUTED, 'MidlineRight') # 3. 5 Challenge Buttons pet_icons = ['proto_pet_1', 'proto_pet_2', 'proto_pet_0', 'proto_pet_3', 'proto_pet_4'] default_titles = ['火 · 灼草之试', '草 · 碎岩之试', '岩 · 避雷之试', '电 · 破浪之试', '水 · 灭火之试'] for i in range(5): b = p.button('Challenge' + str(i), 'Content_Page', 40, 308 + i * 140, 1000, 124, '') n = next(n for n in p.data['nodes'] if n['name'] == b) n['components'][0]['sprite'] = p.sprite('journal_card') n['components'][1].update( spriteHighlighted=p.sprite('journal_card'), spritePressed=p.sprite('journal_secondary_pressed'), spriteDisabled=p.sprite('journal_disabled')) p.data['nodes'] = [n for n in p.data['nodes'] if n['name'] != 'Txt_Challenge' + str(i)] p.data['bindings'].remove('Txt_Challenge' + str(i)) p.icon('ChallengeIcon' + str(i), b, 18, 14, 96, 96, pet_icons[i]) p.text('Challenge' + str(i), b, 134, 16, 640, 48, default_titles[i], 34, INK, 'Left') p.text('ChallengeState' + str(i), b, 136, 68, 640, 40, '可挑战', 26, MUTED, 'Left') p.text('ChallengeTag' + str(i), b, 780, 40, 180, 44, '选择 >', 26, '#8FA6B5FF', 'Center') # 4. Detail Panel p.text('DetailSectionHeading', 'Content_Page', 52, 1028, 500, 50, '试炼详情与阵容', 36) p.text('DetailSectionHint', 'Content_Page', 560, 1034, 460, 42, '同调元素自动上阵', 26, MUTED, 'MidlineRight') p.card('DetailPanel', 1088, 450, 'journal_gold_card') p.icon('DetailEmblem', 'DetailPanel', 28, 22, 64, 64, 'home_ico_trophy') p.text('DetailTitle', 'DetailPanel', 106, 28, 400, 44, '试炼情报', 34, INK) p.text('DetailTag', 'DetailPanel', 720, 30, 240, 36, '不消耗弹珠', 26, '#99641FFF', 'MidlineRight') p.text('Detail', 'DetailPanel', 34, 90, 932, 220, '选择试炼查看详细情报…', 28, INK, 'TopLeft') p.button('Start', 'DetailPanel', 32, 324, 936, 96, '开始试炼', primary=True) # 5. Quick Actions p.text('QuickHeading', 'Content_Page', 52, 1558, 500, 48, '冒险整备', 36) p.text('QuickHint', 'Content_Page', 560, 1564, 460, 40, '缺少对应元素伙伴?', 26, MUTED, 'MidlineRight') p.button('Capture', 'Content_Page', 40, 1618, 485, 92, '结识伙伴') p.button('Fish', 'Content_Page', 555, 1618, 485, 92, '小镇钓鱼') # 6. Rules p.text('Rules', 'Content_Page', 64, 1730, 952, 76, '属性试炼不消耗弹珠 · 首胜即发专属徽章与10枚伙伴碎片\n再次挑战仅在刷新个人最短用时纪录时计入排行榜', 25, MUTED, 'Center') p.save() if __name__ == '__main__': tasks() signin() mail() challenges() print('Authored Tasks / Signin / Mail / Challenges JSON. Build and verify with Unity Pipeline.')