gui: add modern loading screen and asset preview overlay
This commit is contained in:
parent
04599db6d6
commit
e681053ea2
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue