gui: add modern loading screen and asset preview overlay

This commit is contained in:
Toontown Super 2026-07-13 14:08:36 -04:00
parent 04599db6d6
commit e681053ea2
5 changed files with 863 additions and 20 deletions

View File

@ -105,6 +105,7 @@ class PlayGame(StateData.StateData):
self.hood = None
self.quietZoneDoneEvent = uniqueName('quietZoneDone')
self.quietZoneStateData = None
self._reusePickAToonTtcHood = False
return
def enter(self, hoodId, zoneId, avId):
@ -120,13 +121,26 @@ class PlayGame(StateData.StateData):
else:
loaderName = ZoneUtil.getLoaderName(zoneId)
whereName = ZoneUtil.getToonWhereName(zoneId)
self.fsm.request('quietZone', [{'loader': loaderName,
requestStatus = {'loader': loaderName,
'where': whereName,
'how': 'teleportIn',
'hoodId': hoodId,
'zoneId': zoneId,
'shardId': None,
'avId': avId}])
'avId': avId}
# Revamp: when coming from Pick-a-Toon, Toontown Central may already be
# loaded and visible behind the GUI. We still must go through quietZone
# (FSM/network interest setup), but we can suppress the fade and reuse
# the already-loaded TTC hood in handleWaitForSetZoneResponse.
try:
preHood = getattr(base.cr, '_pickAToonTTCBackdrop', None)
if preHood and ZoneUtil.getCanonicalZoneId(hoodId) == ToontownGlobals.ToontownCentral and requestStatus['loader'] == 'safeZoneLoader':
requestStatus['noFade'] = True
except Exception:
pass
self.fsm.request('quietZone', [requestStatus])
return
def exit(self):
@ -244,6 +258,34 @@ class PlayGame(StateData.StateData):
loaderName = requestStatus['loader']
avId = requestStatus.get('avId', -1)
ownerId = requestStatus.get('ownerId', avId)
# Revamp: if we already preloaded Toontown Central during Pick-a-Toon,
# reuse it only when we're actually entering TTC playground. Otherwise
# drop the preload first — otherwise the backdrop and the real hood
# both load (assertion failures).
try:
preHood = getattr(base.cr, '_pickAToonTTCBackdrop', None)
reusePickAToonTTC = (
preHood
and canonicalHoodId == ToontownGlobals.ToontownCentral
and loaderName == 'safeZoneLoader'
)
if preHood and not reusePickAToonTTC:
base.cr.cleanupPickAToonTTCBackdrop()
elif reusePickAToonTTC:
self._reusePickAToonTtcHood = True
self.hood = preHood
base.cr._pickAToonTTCBackdrop = None
base.cr._pickAToonTTCBackdropRequestStatus = None
try:
if hasattr(self.hood, 'loader') and hasattr(self.hood.loader, 'geom'):
self.hood.loader.geom.reparentTo(render)
except Exception:
pass
return
except Exception:
pass
if base.config.GetBool('want-qa-regression', 0):
self.notify.info('QA-REGRESSION: NEIGHBORHOODS: Visit %s' % hoodName)
count = ToontownGlobals.hoodCountMap[canonicalHoodId]
@ -300,7 +342,9 @@ class PlayGame(StateData.StateData):
self.quietZoneStateData.exit()
self.quietZoneStateData.unload()
self.quietZoneStateData = None
loader.endBulkLoad('hood')
if not getattr(self, '_reusePickAToonTtcHood', False):
loader.endBulkLoad('hood')
self._reusePickAToonTtcHood = False
else:
self.handleLeftQuietZone()
return

View File

@ -3,6 +3,7 @@ from panda3d.toontown import *
from direct.directnotify.DirectNotifyGlobal import *
from direct.showbase import Loader
from toontown.toontowngui import ToontownLoadingScreen
import time
class ToontownLoader(Loader.Loader):
TickPeriod = 0.01
@ -14,9 +15,74 @@ class ToontownLoader(Loader.Loader):
self.loadingScreen = ToontownLoadingScreen.ToontownLoadingScreen()
self._tickCounter = 0
self._tickSkip = 10
self._prefetch_handles = []
return
def cancelPrefetchRequests(self):
for h in list(self._prefetch_handles):
try:
if hasattr(h, 'cancel') and not h.cancelled() and not h.done():
h.cancel()
except Exception:
pass
self._prefetch_handles = []
def schedulePrefetchForQuietZone(self, request_status):
if not ConfigVariableBool('want-async-zone-prefetch', True).value:
return
self.cancelPrefetchRequests()
try:
from toontown.toonbase import ZonePrefetchCatalog
paths = ZonePrefetchCatalog.paths_for_quiet_zone(request_status)
except Exception:
paths = []
for i, path in enumerate(paths):
self._startPrefetchModel(path, priority=max(1, 80 - i))
def _startPrefetchModel(self, path, priority=10):
holder = {'cb': None}
def _done(model):
cb = holder['cb']
try:
if cb is not None and cb in self._prefetch_handles:
self._prefetch_handles.remove(cb)
except Exception:
pass
try:
if model is not None and not model.isEmpty():
ModelPool.addModel(path, model.node())
except Exception:
pass
try:
cb = Loader.Loader.loadModel(
self,
path,
callback=_done,
blocking=False,
okMissing=True,
priority=priority,
)
holder['cb'] = cb
if cb is not None and hasattr(cb, 'requests'):
self._prefetch_handles.append(cb)
except Exception:
pass
def _notifyModernAsset(self, path, node=None, texture=None, kind='model'):
try:
ml = getattr(self.base, 'modernLoading', None)
if ml is None or not ml.enabled():
return
ml.on_asset_loaded(path=path, model=node, texture=texture, kind=kind)
except Exception:
pass
def destroy(self):
self.cancelPrefetchRequests()
self.loadingScreen.destroy()
del self.loadingScreen
Loader.Loader.destroy(self)
@ -28,9 +94,26 @@ class ToontownLoader(Loader.Loader):
Loader.Loader.notify.warning("Tried to start a block ('%s'), but am already in a block ('%s')" % (name, self.blockName))
return None
self.inBulkBlock = 1
self._bulkPrevTickSkip = self._tickSkip
self._tickSkip = 1
self._lastTickT = globalClock.getRealTime()
self.blockName = name
self.loadingScreen.begin(range, label, gui, tipCategory)
try:
ml = getattr(self.base, 'modernLoading', None)
if ml and ml.enabled():
ml.enter_bulk_load(name, label, range)
if ConfigVariableBool('minimal-legacy-loading-with-modern', True).value:
gui = 0
except Exception:
pass
minimal = False
try:
ml = getattr(self.base, 'modernLoading', None)
if ml and ml.enabled() and ConfigVariableBool('minimal-legacy-loading-with-modern', True).value:
minimal = True
except Exception:
pass
self.loadingScreen.begin(range, label, gui, tipCategory, minimal=minimal)
return None
def endBulkLoad(self, name):
@ -41,19 +124,33 @@ class ToontownLoader(Loader.Loader):
Loader.Loader.notify.warning("Tried to end a block ('%s'), other then the current one ('%s')" % (name, self.blockName))
return None
self.inBulkBlock = None
self._tickSkip = self._bulkPrevTickSkip
expectedCount, loadedCount = self.loadingScreen.end()
now = globalClock.getRealTime()
Loader.Loader.notify.info("At end of block '%s', expected %s, loaded %s, duration=%s" % (self.blockName,
expectedCount,
loadedCount,
now - self._loadStartT))
try:
ml = getattr(self.base, 'modernLoading', None)
if ml and ml.enabled():
ml.leave_bulk_load()
except Exception:
pass
return
def abortBulkLoad(self):
if self.inBulkBlock:
Loader.Loader.notify.info("Aborting block ('%s')" % self.blockName)
self.inBulkBlock = None
self._tickSkip = self._bulkPrevTickSkip
self.loadingScreen.abort()
try:
ml = getattr(self.base, 'modernLoading', None)
if ml and ml.enabled():
ml.leave_bulk_load()
except Exception:
pass
return
def tick(self):
@ -66,18 +163,42 @@ class ToontownLoader(Loader.Loader):
self._lastTickT += self.TickPeriod
self.loadingScreen.tick()
try:
base.cr.considerHeartbeat()
ml = getattr(self.base, 'modernLoading', None)
if ml and ml.enabled():
if not ConfigVariableBool('want-zero-load-ui', False).value:
ml.set_progress(self.loadingScreen.get_progress_fraction() * 100.0)
except Exception:
pass
try:
if getattr(self.base, 'cr', None):
self.base.cr.considerHeartbeat()
except:
pass
# Critical: bulk loads can otherwise run as one giant "frame"
# (unplayable hitch). Yield so the OS and render thread can
# breathe, spreading the work across multiple frames.
try:
time.sleep(0)
except Exception:
pass
def loadModel(self, *args, **kw):
ret = Loader.Loader.loadModel(self, *args, **kw)
if kw.get('callback') is not None:
return ret
self.tick()
path = str(args[0]) if args else None
if ret is not None and not ret.isEmpty():
self._notifyModernAsset(path, node=ret)
else:
self._notifyModernAsset(path, node=None)
return ret
def loadFont(self, *args, **kw):
ret = Loader.Loader.loadFont(self, *args, **kw)
self.tick()
path = str(args[0]) if args else None
self._notifyModernAsset(path, node=None, kind='font')
return ret
def loadTexture(self, texturePath, alphaPath = None, okMissing = False):
@ -85,16 +206,19 @@ class ToontownLoader(Loader.Loader):
self.tick()
if alphaPath:
self.tick()
self._notifyModernAsset(str(texturePath), texture=ret)
return ret
def loadSfx(self, soundPath):
ret = Loader.Loader.loadSfx(self, soundPath)
self.tick()
self._notifyModernAsset(str(soundPath), kind='audio')
return ret
def loadMusic(self, soundPath):
ret = Loader.Loader.loadMusic(self, soundPath)
self.tick()
self._notifyModernAsset(str(soundPath), kind='audio')
return ret
def loadDNAFileAI(self, dnaStore, dnaFile):

View File

@ -0,0 +1,223 @@
"""
Live 3D / texture preview for the unified loading UI.
Uses an offscreen buffer with Camera.setScene() so preview geometry never mixes
with the main world render.
"""
import os
from typing import Optional
from direct.directnotify import DirectNotifyGlobal
from direct.showbase.ShowBaseGlobal import base
from direct.task import Task
from panda3d.core import (
AmbientLight,
CardMaker,
DirectionalLight,
NodePath,
PerspectiveLens,
Texture,
TransparencyAttrib,
VBase4,
)
notify = DirectNotifyGlobal.directNotify.newCategory('LoadingAssetPreview')
_PREVIEW_CLEAR = VBase4(0.08, 0.12, 0.19, 1.0)
class LoadingAssetPreview:
def __init__(self):
self._buffer = None
self._cam_np = None
self._world = NodePath('loadingAssetPreviewWorld')
self._holder = self._world.attachNewNode('assetHolder')
self._card = None
self._border = None
self._spin_task = None
self._last_note_t = 0.0
self._min_interval = 0.06
def attach_card(self, parent: NodePath, pos, frame, sort: int = 100002):
"""
parent: aspect2d parent
pos: (x, y, z) in aspect2d space
frame: (left, right, bottom, top) CardMaker frame (relative scale)
"""
self.detach_card()
if not base.win:
return
try:
w, h = 512, 288
self._buffer = base.win.makeTextureBuffer('loadingAssetPreview', w, h, to_ram=False, fbp=False)
self._buffer.setClearColor(True)
self._buffer.setClearColor(_PREVIEW_CLEAR)
amb = AmbientLight('lap_amb')
amb.setColor(VBase4(0.5, 0.5, 0.58, 1))
anp = self._world.attachNewNode(amb)
self._world.setLight(anp)
sun = DirectionalLight('lap_sun')
sun.setColor(VBase4(0.92, 0.9, 0.82, 1))
snp = self._world.attachNewNode(sun)
snp.setHpr(-68, -32, 0)
self._world.setLight(snp)
lens = PerspectiveLens()
lens.setFov(40)
lens.setNearFar(0.08, 500.0)
self._cam_np = base.makeCamera(
self._buffer,
scene=self._world,
lens=lens,
aspectRatio=float(w) / float(h),
clearColor=_PREVIEW_CLEAR,
sort=-60,
)
self._cam_np.setPos(0, -16, 6.5)
self._cam_np.lookAt(0, 0, 3.0)
tex = self._buffer.getTexture()
bl, br, bb, bt = frame
bwide = 0.02
bcm = CardMaker('lapBorder')
bcm.setFrame(bl - bwide, br + bwide, bb - bwide, bt + bwide)
self._border = parent.attachNewNode(bcm.generate())
self._border.setColor(0.12, 0.65, 0.95, 0.55)
self._border.setTransparency(TransparencyAttrib.M_alpha)
self._border.setBin('fixed', sort - 1)
self._border.setPos(*pos)
cm = CardMaker('lapCard')
cm.setFrame(bl, br, bb, bt)
self._card = parent.attachNewNode(cm.generate())
self._card.setTexture(tex)
self._card.setTransparency(TransparencyAttrib.M_alpha)
self._card.setBin('fixed', sort)
self._card.setPos(*pos)
except Exception as e:
notify.warning('loading asset preview unavailable: %s' % e)
self.detach_card()
def detach_card(self):
self._stop_spin()
if self._card:
try:
self._card.removeNode()
except Exception:
pass
self._card = None
if self._border:
try:
self._border.removeNode()
except Exception:
pass
self._border = None
self._teardown_buffer()
self._clear_holder()
def _teardown_buffer(self):
if self._buffer:
try:
base.graphicsEngine.removeWindow(self._buffer)
except Exception:
pass
self._buffer = None
self._cam_np = None
def _clear_holder(self):
self._stop_spin()
try:
for c in self._holder.getChildren():
c.removeNode()
except Exception:
pass
def _stop_spin(self):
if self._spin_task:
try:
base.taskMgr.remove(self._spin_task)
except Exception:
pass
self._spin_task = None
def _spin(self, task):
try:
self._holder.setH(self._holder.getH() + 0.9)
except Exception:
pass
return Task.cont
def note_asset(
self,
path: Optional[str],
model: Optional[NodePath] = None,
texture: Optional[Texture] = None,
force: bool = False,
):
if not self._buffer or not self._card:
return
now = base.globalClock.getRealTime()
if not force and (now - self._last_note_t) < self._min_interval:
return
self._last_note_t = now
self._clear_holder()
self._stop_spin()
if model is not None and not model.isEmpty():
try:
inst = model.copyTo(self._holder)
self._frame_node(inst)
self._holder.setH(15)
self._spin_task = base.taskMgr.add(self._spin, 'loadingAssetSpin', priority=50)
except Exception as e:
notify.debug('model preview failed: %s' % e)
return
if texture is not None:
try:
cm = CardMaker('texFlat')
cm.setFrame(-6, 6, -6, 6)
flat = self._holder.attachNewNode(cm.generate())
flat.setTexture(texture)
flat.setTransparency(TransparencyAttrib.M_alpha)
flat.lookAt(0, -1, 0)
except Exception as e:
notify.debug('texture preview failed: %s' % e)
def _frame_node(self, np: NodePath):
try:
p = NodePath('pivot')
p.reparentTo(self._holder)
np.wrtReparentTo(p)
bounds = np.getBounds()
if bounds.isEmpty():
return
center = bounds.getApproxCenter()
radius = max(bounds.getRadius(), 0.01)
np.setPos(-center)
scale = 7.5 / max(radius, 1.0)
s = min(max(scale, 0.35), 14.0)
p.setScale(s)
except Exception:
pass
def format_caption(self, path: Optional[str]) -> str:
if not path:
return ''
try:
base_name = os.path.basename(str(path).replace('\\', '/'))
if len(base_name) > 42:
return base_name[:39] + '…'
return base_name
except Exception:
return str(path)[:42]
def destroy(self):
self.detach_card()
try:
self._world.removeNode()
except Exception:
pass

View File

@ -0,0 +1,414 @@
import math
import time
from panda3d.core import CardMaker, NodePath, TextNode, Vec4, ConfigVariableBool
from direct.gui.DirectGui import DirectFrame, DirectLabel, DirectWaitBar, DGG
from direct.interval.IntervalGlobal import (
Sequence,
Parallel,
LerpColorScaleInterval,
LerpScaleInterval,
LerpFunc,
Wait,
Func,
)
from toontown.toontowngui.LoadingAssetPreview import LoadingAssetPreview
class ModernLoadingScreen:
"""
Unified launcher / zone-load overlay:
- Full-screen polished startup view (logo, gradient, progress, live asset preview)
- Compact bottom bar after login (game stays visible; preview + status during loads)
"""
def __init__(self, parent=None):
self._enabled = ConfigVariableBool('want-modern-launcher-ui', True).value
self._want_preview = ConfigVariableBool('want-loading-asset-preview', True).value
self._start_t = time.time()
self._pulse_ival = None
self._outro_ival = None
self._launcher_done = False
self._bulk_active = False
self._preview = LoadingAssetPreview() if self._enabled else None
if not self._enabled:
self.root = None
self.compact_root = None
return
if parent is None:
parent = aspect2d
# Full-screen launcher panel
self.root = DirectFrame(
parent=parent,
relief=None,
frameColor=(0, 0, 0, 0),
sortOrder=100000,
)
self._bg = self._make_gradient_bg(self.root)
self.logoModel = None
self.logo = None
try:
self.logoModel = loader.loadModel('phase_3/models/gui/tt_m_gui_ups_logo_noText')
if not self.logoModel or self.logoModel.isEmpty():
raise Exception('logo model empty')
self.logo = DirectFrame(
parent=self.root,
relief=None,
image=self.logoModel,
image_scale=0.45,
pos=(0, 0, 0.58),
)
except Exception:
self.logoModel = None
self.logo = DirectLabel(
parent=self.root,
relief=None,
text='Toontown',
text_align=TextNode.ACenter,
text_scale=0.12,
text_fg=(1, 1, 1, 1),
pos=(0, 0, 0.62),
)
self.subtitle = DirectLabel(
parent=self.root,
relief=None,
text='Launching…',
text_align=TextNode.ACenter,
text_scale=0.06,
text_fg=(1, 1, 1, 0.85),
pos=(0, 0, 0.50),
)
self.status = DirectLabel(
parent=self.root,
relief=None,
text='Initializing…',
text_align=TextNode.ACenter,
text_scale=0.052,
text_fg=(1, 1, 1, 0.9),
pos=(0, 0, -0.05),
)
self.asset_caption = DirectLabel(
parent=self.root,
relief=None,
text='',
text_align=TextNode.ACenter,
text_scale=0.034,
text_fg=(0.75, 0.9, 1.0, 0.85),
pos=(0, 0, -0.2),
)
self.progress = DirectWaitBar(
parent=self.root,
relief=None,
range=100,
value=0,
pos=(0, 0, -0.16),
frameSize=(-0.75, 0.75, -0.055, 0.055),
frameColor=(1, 1, 1, 0.12),
barColor=(0.2, 0.8, 1.0, 0.85),
borderWidth=(0.02, 0.02),
)
self._detail = DirectLabel(
parent=self.root,
relief=None,
text='',
text_align=TextNode.ACenter,
text_scale=0.028,
text_fg=(0.55, 0.7, 0.85, 0.75),
pos=(0, 0, -0.28),
)
if self._want_preview and self._preview:
try:
self._preview.attach_card(self.root, (0.68, 0, 0.02), (-0.38, 0.38, -0.22, 0.22))
except Exception:
pass
self._start_pulse()
# Compact in-world load bar (bottom)
self.compact_root = DirectFrame(
parent=hidden,
relief=DGG.FLAT,
frameColor=(0.03, 0.07, 0.12, 0.78),
frameSize=(-1.4, 1.4, -0.12, 0.1),
pos=(0, 0, -0.88),
sortOrder=100015,
borderWidth=(0.008, 0.008),
)
self.compact_status = DirectLabel(
parent=self.compact_root,
relief=None,
text='',
text_align=TextNode.ALeft,
text_scale=0.04,
text_fg=(1, 1, 1, 0.95),
pos=(-1.32, 0, 0.035),
)
self.compact_asset = DirectLabel(
parent=self.compact_root,
relief=None,
text='',
text_align=TextNode.ALeft,
text_scale=0.032,
text_fg=(0.65, 0.85, 1.0, 0.9),
pos=(-1.32, 0, -0.025),
)
self.compact_progress = DirectWaitBar(
parent=self.compact_root,
relief=None,
range=100,
value=0,
pos=(0.15, 0, -0.055),
frameSize=(-0.55, 0.55, -0.028, 0.028),
frameColor=(1, 1, 1, 0.14),
barColor=(0.25, 0.85, 1.0, 0.9),
borderWidth=(0.015, 0.015),
)
self._layout_compact_bar()
def _layout_compact_bar(self):
try:
left = float(base.a2dLeft) + 0.04
right = float(base.a2dRight) - 0.04
self.compact_root['frameSize'] = (left, right, -0.13, 0.1)
self.compact_status.setPos(left + 0.02, 0, 0.04)
self.compact_asset.setPos(left + 0.02, 0, -0.03)
self.compact_progress.setPos((left + right) * 0.5 + 0.1, 0, -0.055)
except Exception:
pass
def enabled(self):
return bool(self._enabled and self.root)
def _make_gradient_bg(self, parent) -> NodePath:
cm = CardMaker('modernLoadingBG')
cm.setFrameFullscreenQuad()
card = NodePath(cm.generate())
card.reparentTo(parent)
card.setBin('fixed', -100)
card.setDepthTest(False)
card.setDepthWrite(False)
card.setColorScale(Vec4(0.06, 0.10, 0.16, 1.0))
return card
def _start_pulse(self):
if not self.root:
return
def pulse(t):
s = 0.5 + 0.5 * math.sin(t * 2 * math.pi)
r = 0.06 + s * 0.02
g = 0.10 + s * 0.03
b = 0.16 + s * 0.05
try:
self._bg.setColorScale(Vec4(r, g, b, 1.0))
except Exception:
pass
self._pulse_ival = Sequence(
LerpFunc(pulse, fromData=0.0, toData=1.0, duration=1.6, blendType='easeInOut'),
LerpFunc(pulse, fromData=1.0, toData=2.0, duration=1.6, blendType='easeInOut'),
)
self._pulse_ival.loop()
def set_title(self, title: str, subtitle=None):
if not self.root:
return
if subtitle is not None:
self.subtitle['text'] = subtitle
def set_status(self, text: str):
if self.root:
self.status['text'] = text
if self._launcher_done and self.compact_root:
self.compact_status['text'] = text
def set_detail(self, text: str):
if not self.root:
return
self._detail['text'] = text or ''
def set_progress(self, pct_0_to_100: float):
try:
v = max(0.0, min(100.0, float(pct_0_to_100)))
except Exception:
v = 0.0
if self.root:
self.progress['value'] = v
if self.compact_root:
self.compact_progress['value'] = v
def on_asset_loaded(self, path=None, model=None, texture=None, kind='model'):
if ConfigVariableBool('want-zero-load-ui', False).value and self._launcher_done:
return
preview = self._preview if self._want_preview else None
if kind == 'audio':
if preview and path:
cap = 'SFX: ' + preview.format_caption(path)
else:
cap = path or ''
if self.root and not self._launcher_done:
self.asset_caption['text'] = cap
if self._launcher_done and self.compact_root:
self.compact_asset['text'] = cap
return
if kind == 'font':
if preview and path:
cap = 'Font: ' + preview.format_caption(path)
else:
cap = path or ''
if self.root and not self._launcher_done:
self.asset_caption['text'] = cap
if self._launcher_done and self.compact_root:
self.compact_asset['text'] = cap
return
if not preview:
return
cap = preview.format_caption(path)
if self.root and not self._launcher_done:
self.asset_caption['text'] = cap
if path:
short = path.replace('\\', '/')
if len(short) > 56:
short = short[:53] + '…'
self.set_detail(short)
if self._launcher_done and self.compact_root:
self.compact_asset['text'] = cap
try:
preview.note_asset(path, model=model, texture=texture)
except Exception:
pass
def enter_bulk_load(self, block_name: str, label: str, expected_steps: int):
if not self._enabled:
return
self._bulk_active = True
if ConfigVariableBool('want-zero-load-ui', False).value:
return
self._layout_compact_bar()
if self._launcher_done:
try:
self.compact_root.reparentTo(aspect2d, DGG.NO_FADE_SORT_INDEX)
except Exception:
pass
self.compact_status['text'] = label
self.compact_asset['text'] = ''
self.compact_progress['value'] = 0
if self._want_preview and self._preview:
self._preview.detach_card()
self._preview.attach_card(self.compact_root, (1.15, 0, 0.02), (-0.2, 0.2, -0.1, 0.1))
else:
self.set_status(label)
def leave_bulk_load(self):
self._bulk_active = False
if ConfigVariableBool('want-zero-load-ui', False).value:
if self._preview:
try:
self._preview.detach_card()
except Exception:
pass
return
if self._launcher_done and self.compact_root:
try:
self.compact_root.reparentTo(hidden)
except Exception:
pass
if self._preview:
self._preview.detach_card()
if self.root and not self._launcher_done and self._want_preview:
self._preview.attach_card(self.root, (0.68, 0, 0.02), (-0.38, 0.38, -0.22, 0.22))
def transition_out(self, done_cb=None):
if not self.root:
if callable(done_cb):
done_cb()
return
if self._outro_ival:
return
def _finish():
try:
if self._pulse_ival:
self._pulse_ival.finish()
self._pulse_ival = None
except Exception:
pass
try:
self.root.reparentTo(hidden)
except Exception:
pass
if self._preview:
self._preview.detach_card()
self._launcher_done = True
if callable(done_cb):
done_cb()
self._outro_ival = Sequence(
Parallel(
LerpColorScaleInterval(self.root, 0.45, Vec4(1, 1, 1, 0.0), blendType='easeInOut'),
LerpScaleInterval(self.root, 0.45, 1.06, blendType='easeInOut'),
),
Wait(0.02),
Func(_finish),
)
self._outro_ival.start()
def destroy(self):
if self._pulse_ival:
try:
self._pulse_ival.finish()
except Exception:
pass
self._pulse_ival = None
if self._outro_ival:
try:
self._outro_ival.finish()
except Exception:
pass
self._outro_ival = None
if self._preview:
self._preview.destroy()
self._preview = None
if self.compact_root:
try:
self.compact_root.destroy()
except Exception:
try:
self.compact_root.removeNode()
except Exception:
pass
self.compact_root = None
if self.root:
try:
self.root.destroy()
except Exception:
try:
self.root.removeNode()
except Exception:
pass
self.root = None
if self.logoModel:
try:
self.logoModel.removeNode()
except Exception:
pass
self.logoModel = None

View File

@ -11,53 +11,91 @@ class ToontownLoadingScreen:
self.__count = 0
self.__updateSkip = 5
self.__updateCounter = 0
self.gui = loader.loadModel('phase_3/models/gui/progress-background')
# Background art was authored for 4:3; we widen it for 16:9+ without
# stretching text/UI by keeping UI elements parented to aspect2d.
self.background = loader.loadModel('phase_3/models/gui/progress-background')
self.banner = loader.loadModel('phase_3/models/gui/toon_council').find('**/scroll')
self.banner.reparentTo(self.gui)
self.banner.setScale(0.4, 0.4, 0.4)
self.tip = DirectLabel(guiId='ToontownLoadingScreenTip', parent=self.banner, relief=None, text='', text_scale=TTLocalizer.TLStip, textMayChange=1, pos=(-1.2, 0.0, 0.1), text_fg=(0.4, 0.3, 0.2, 1), text_wordwrap=13, text_align=TextNode.ALeft)
self.title = DirectLabel(guiId='ToontownLoadingScreenTitle', parent=self.gui, relief=None, pos=(-1.06, 0, -0.77), text='', textMayChange=1, text_scale=0.08, text_fg=(0, 0, 0.5, 1), text_align=TextNode.ALeft)
self.waitBar = DirectWaitBar(guiId='ToontownLoadingScreenWaitBar', parent=self.gui, frameSize=(-1.06,
self.title = DirectLabel(guiId='ToontownLoadingScreenTitle', parent=hidden, relief=None, pos=(-1.06, 0, -0.77), text='', textMayChange=1, text_scale=0.08, text_fg=(0, 0, 0.5, 1), text_align=TextNode.ALeft)
self.waitBar = DirectWaitBar(guiId='ToontownLoadingScreenWaitBar', parent=hidden, frameSize=(-1.06,
1.06,
-0.03,
0.03), pos=(0, 0, -0.85), text='')
return
def __getBackgroundXScale(self):
# aspect2d is a fixed-height space; its width expands with aspect ratio.
# The original art is laid out for 4:3 (1.333...).
try:
currentAspect = float(base.camLens.getAspectRatio())
except Exception:
currentAspect = 4.0 / 3.0
return max(1.0, currentAspect / (4.0 / 3.0))
def __applyWidescreenLayout(self):
# Keep a consistent margin from the left/right edges regardless of aspect.
leftMargin = 0.273333 # (-1.06) - (-4/3)
rightMargin = 0.273333 # (4/3) - (1.06)
xLeft = base.a2dLeft + leftMargin
xRight = base.a2dRight - rightMargin
self.title.setPos(xLeft, 0, -0.77)
self.waitBar['frameSize'] = (xLeft, xRight, -0.03, 0.03)
self.waitBar.setPos(0, 0, -0.85)
def destroy(self):
self.tip.destroy()
self.title.destroy()
self.waitBar.destroy()
self.banner.removeNode()
self.gui.removeNode()
self.background.removeNode()
def getTip(self, tipCategory):
return TTLocalizer.TipTitle + '\n' + random.choice(TTLocalizer.TipDict.get(tipCategory))
def begin(self, range, label, gui, tipCategory):
def begin(self, range, label, gui, tipCategory, minimal=False):
self.waitBar['range'] = range
self.title['text'] = label
self.tip['text'] = self.getTip(tipCategory)
self.__count = 0
self.__expectedCount = range
if gui:
self.waitBar.reparentTo(self.gui)
self.title.reparentTo(self.gui)
self.gui.reparentTo(aspect2dp, DGG.NO_FADE_SORT_INDEX)
if minimal:
self.waitBar.reparentTo(hidden)
self.title.reparentTo(hidden)
self.banner.reparentTo(hidden)
self.background.reparentTo(hidden)
else:
self.waitBar.reparentTo(aspect2dp, DGG.NO_FADE_SORT_INDEX)
self.title.reparentTo(aspect2dp, DGG.NO_FADE_SORT_INDEX)
self.gui.reparentTo(hidden)
self.banner.reparentTo(aspect2dp, DGG.NO_FADE_SORT_INDEX)
if gui:
self.background.reparentTo(aspect2dp, DGG.NO_FADE_SORT_INDEX)
self.background.setScale(self.__getBackgroundXScale(), 1.0, 1.0)
else:
self.background.reparentTo(hidden)
self.banner.reparentTo(hidden)
self.__applyWidescreenLayout()
self.waitBar.update(self.__count)
def get_progress_fraction(self):
if self.__expectedCount <= 0:
return 1.0
return max(0.0, min(1.0, float(self.__count) / float(self.__expectedCount)))
def end(self):
self.waitBar.finish()
self.waitBar.reparentTo(self.gui)
self.title.reparentTo(self.gui)
self.gui.reparentTo(hidden)
self.waitBar.reparentTo(hidden)
self.title.reparentTo(hidden)
self.banner.reparentTo(hidden)
self.background.reparentTo(hidden)
return (self.__expectedCount, self.__count)
def abort(self):
self.gui.reparentTo(hidden)
self.banner.reparentTo(hidden)
self.background.reparentTo(hidden)
def tick(self):
self.__count = self.__count + 1