276 lines
19 KiB
Python
276 lines
19 KiB
Python
"""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.')
|