mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 22:31:26 +02:00
refactor: resolve avatar visuals through render kit
This commit is contained in:
@@ -76,6 +76,7 @@
|
||||
<script src="js/render/charSprites.js"></script>
|
||||
<script src="js/render/assetPacks.js"></script>
|
||||
<script src="js/render/charCustomize.js"></script>
|
||||
<script src="js/render/avatarVisuals.js"></script>
|
||||
<script src="js/core/singlePlayerAvatar.js"></script>
|
||||
|
||||
<!-- Other modules -->
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// SinglePlayerAvatar bridges the shared character identity/render kit into the legacy ROT.js
|
||||
// dungeon display. The dungeon still draws through ROT; this module owns a sprite overlay that
|
||||
// can be cleared safely back to the classic @ tile whenever assets or identity are unavailable.
|
||||
// SinglePlayerAvatar stores the selected identity for the legacy single-player page and manages
|
||||
// the overlay canvas used by old ROT.js screens. It delegates visual decisions to RK.avatarVisuals.
|
||||
(function (root) {
|
||||
'use strict';
|
||||
|
||||
@@ -11,16 +10,15 @@
|
||||
return DEFAULT_APPEARANCE;
|
||||
}
|
||||
|
||||
function baseKind(appearance) {
|
||||
if (!appearance || !root.RK) return 'color';
|
||||
if (root.RK.isChar && root.RK.isChar(appearance.avatar)) return 'char';
|
||||
if (root.RK.isSkin && root.RK.isSkin(appearance.avatar)) return 'skin';
|
||||
return 'color';
|
||||
}
|
||||
|
||||
function requestGameRedraw() {
|
||||
function requestRedraw() {
|
||||
if (root.Game && root.Game._gameActive && root.Game._drawGameScreen) {
|
||||
root.Game._drawGameScreen();
|
||||
} else if (root.ScreenManager && root.DisplayManager && root.DisplayManager.ensureDisplay && root.DisplayManager.ensureDisplay()) {
|
||||
if (root.ScreenManager.isShowingWaitingScreen && root.ScreenManager.isShowingWaitingScreen() && root.ScreenManager.drawWaitingScreen) {
|
||||
root.ScreenManager.drawWaitingScreen(true);
|
||||
} else if (root.ScreenManager.drawWelcomeScreen) {
|
||||
root.ScreenManager.drawWelcomeScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +62,7 @@
|
||||
this._appearance = normalize(appearance);
|
||||
if (opts.save !== false && root.RK && root.RK.saveIdentity) root.RK.saveIdentity(this._appearance);
|
||||
this.updateButton();
|
||||
if (opts.redraw !== false) requestGameRedraw();
|
||||
if (opts.redraw !== false) requestRedraw();
|
||||
},
|
||||
|
||||
applyIdentity: function (data) {
|
||||
@@ -136,41 +134,21 @@
|
||||
return this._appearance || normalize(DEFAULT_APPEARANCE);
|
||||
},
|
||||
|
||||
_ensureAssets: function (appearance) {
|
||||
var kind = baseKind(appearance);
|
||||
var self = this;
|
||||
if (kind === 'char') {
|
||||
if (root.RK.charAtlas && root.RK.charAtlas()) return true;
|
||||
if (root.RK.loadCharAtlas && !this._loading.char) {
|
||||
this._loading.char = true;
|
||||
root.RK.loadCharAtlas(function () {
|
||||
self._loading.char = false;
|
||||
requestGameRedraw();
|
||||
});
|
||||
}
|
||||
return false;
|
||||
resolveVisual: function (opts) {
|
||||
opts = opts || {};
|
||||
if (!root.RK || !root.RK.avatarVisuals) {
|
||||
return { appearance: this.currentAppearance(), kind: 'legacy', fallbackTile: opts.fallbackTile || '@' };
|
||||
}
|
||||
if (kind === 'skin') {
|
||||
if (root.RK.skinSheet && root.RK.skinSheet(appearance.avatar)) return true;
|
||||
if (root.RK.loadSkin && !this._loading[appearance.avatar]) {
|
||||
this._loading[appearance.avatar] = true;
|
||||
var rec = root.RK.loadSkin(appearance.avatar);
|
||||
if (rec && rec.cbs) rec.cbs.push(function () {
|
||||
self._loading[appearance.avatar] = false;
|
||||
requestGameRedraw();
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
return root.RK.avatarVisuals.resolve(this.currentAppearance(), opts);
|
||||
},
|
||||
|
||||
_ensureVisual: function (visual) {
|
||||
if (!root.RK || !root.RK.avatarVisuals || !visual || !visual.canvas) return false;
|
||||
return root.RK.avatarVisuals.ensureCanvasReady(visual, requestRedraw);
|
||||
},
|
||||
|
||||
canDrawPlayer: function () {
|
||||
if (!root.RK) return false;
|
||||
var appearance = this.currentAppearance();
|
||||
var kind = baseKind(appearance);
|
||||
if (kind !== 'char' && kind !== 'skin') return false;
|
||||
return this._ensureAssets(appearance);
|
||||
return this._ensureVisual(this.resolveVisual({ projection: 'legacy-rot', context: 'dungeon-player' }));
|
||||
},
|
||||
|
||||
_updateFacing: function (player) {
|
||||
@@ -188,7 +166,8 @@
|
||||
},
|
||||
|
||||
drawPlayer: function (gameState, viewport) {
|
||||
if (!gameState || !gameState.player || !viewport || !this.canDrawPlayer()) {
|
||||
var visual = this.resolveVisual({ projection: 'legacy-rot', context: 'dungeon-player' });
|
||||
if (!gameState || !gameState.player || !viewport || !this._ensureVisual(visual)) {
|
||||
this.clearOverlay();
|
||||
return false;
|
||||
}
|
||||
@@ -196,62 +175,30 @@
|
||||
if (!ctx) return false;
|
||||
|
||||
this.clearOverlay();
|
||||
var appearance = this.currentAppearance();
|
||||
var kind = baseKind(appearance);
|
||||
var player = gameState.player;
|
||||
var facing = this._updateFacing(player);
|
||||
var cell = viewport.cell || (root.options && root.options.tileWidth) || 32;
|
||||
var now = Date.now();
|
||||
|
||||
if (kind === 'char') return this._drawChar(ctx, appearance, player, viewport.screenX, viewport.screenY, cell, facing, now);
|
||||
if (kind === 'skin') return this._drawSkin(ctx, appearance, player, viewport.screenX, viewport.screenY, cell, facing, now);
|
||||
return false;
|
||||
return root.RK.avatarVisuals.drawTopdownWorld(ctx, visual, {
|
||||
id: 'single-player',
|
||||
x: player.x,
|
||||
y: player.y,
|
||||
facing: facing
|
||||
}, viewport, { onReady: requestRedraw });
|
||||
},
|
||||
|
||||
_drawChar: function (ctx, appearance, player, sx, sy, cell, facing, now) {
|
||||
var ch = root.RK.CHARS && root.RK.CHARS[appearance.avatar];
|
||||
if (!ch || !root.RK.charFrame || !root.RK.drawCharTileCanvas) return false;
|
||||
var e = { id: 'single-player', x: player.x, y: player.y, facing: facing };
|
||||
var frame = root.RK.charFrame(ch, e, now);
|
||||
var dw = cell * 1.8;
|
||||
var dh = dw * frame.squash;
|
||||
var cx = sx * cell + cell / 2;
|
||||
var dy = (sy * cell + cell) - dh + frame.bob;
|
||||
var ap = root.RK.charAppearance({ avatar: appearance.avatar, appearance: appearance });
|
||||
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
function drawComposite(dx, dy2) {
|
||||
root.RK.drawCharTileCanvas(ctx, ch.frame, ap.tint, dx, dy2, dw, dh, ap.colors, 'base');
|
||||
root.RK.charOverlayParts(ap).forEach(function (part) {
|
||||
root.RK.drawCharTileCanvas(ctx, part.frame, part.tint, dx, dy2, dw, dh, part.colorable ? ap.colors : null, part.slot);
|
||||
});
|
||||
}
|
||||
if (frame.flip) {
|
||||
ctx.save();
|
||||
ctx.translate(cx + dw / 2, dy);
|
||||
ctx.scale(-1, 1);
|
||||
drawComposite(0, 0);
|
||||
ctx.restore();
|
||||
} else {
|
||||
drawComposite(cx - dw / 2, dy);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
_drawSkin: function (ctx, appearance, player, sx, sy, cell, facing, now) {
|
||||
var rec = root.RK.skinSheet && root.RK.skinSheet(appearance.avatar);
|
||||
var skin = root.RK.SKINS && root.RK.SKINS[appearance.avatar];
|
||||
if (!rec || !skin || !root.RK.skinFrame) return false;
|
||||
var e = { id: 'single-player-skin', x: player.x, y: player.y, facing: facing };
|
||||
var fr = root.RK.skinFrame(skin, e, now);
|
||||
var dh = cell * (skin.scale || 1.7);
|
||||
var dw = dh * (skin.frameW / skin.frameH);
|
||||
var cx = sx * cell + cell / 2;
|
||||
var dx = cx - dw / 2;
|
||||
var dy = (sy * cell + cell) - dh;
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.drawImage(rec.img, fr.col * skin.frameW, fr.row * skin.frameH, skin.frameW, skin.frameH, dx, dy, dw, dh);
|
||||
return true;
|
||||
drawLegendIcon: function (tileX, tileY, opts) {
|
||||
opts = opts || {};
|
||||
var visual = this.resolveVisual({ projection: 'legacy-rot', context: 'legend', fallbackTile: opts.fallbackTile || '@2' });
|
||||
if (!this._ensureVisual(visual)) return false;
|
||||
var ctx = this._syncOverlay();
|
||||
if (!ctx || !root.RK || !root.RK.avatarVisuals) return false;
|
||||
var cell = opts.cell || (root.options && root.options.tileWidth) || 32;
|
||||
var cols = opts.cols || 2;
|
||||
return root.RK.avatarVisuals.drawIcon(ctx, visual, {
|
||||
x: tileX * cell,
|
||||
y: tileY * cell,
|
||||
w: cols * cell,
|
||||
h: cell
|
||||
}, { onReady: requestRedraw });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -105,23 +105,38 @@ var ScreenManager = {
|
||||
? GameTiles.getTreasureTile()
|
||||
: (this._cryptoType === 'XMR' ? '$M' : '$W');
|
||||
const legendItems = [
|
||||
{ tile: "@2", text: "This is you" },
|
||||
{ tile: "@2", text: "This is you", role: "player", span: 2 },
|
||||
{ tile: ">", text: " Escape the dungeon" },
|
||||
{ tile: "~", text: " Avoid the monster" },
|
||||
{ tile: treasureTile, text: "Secure the bag" }
|
||||
];
|
||||
|
||||
if (window.SinglePlayerAvatar && window.SinglePlayerAvatar.clearOverlay) {
|
||||
window.SinglePlayerAvatar.clearOverlay();
|
||||
}
|
||||
|
||||
const centerX = Math.floor(this._screenWidth / 2) - 10;
|
||||
for (let item of legendItems) {
|
||||
let x = centerX;
|
||||
const span = item.span || item.tile.length || 1;
|
||||
let iconDrawn = false;
|
||||
|
||||
// Draw the special tile
|
||||
if (window.options?.tileMap?.hasOwnProperty(item.tile)) {
|
||||
if (item.role === "player" && window.SinglePlayerAvatar && window.SinglePlayerAvatar.drawLegendIcon) {
|
||||
iconDrawn = window.SinglePlayerAvatar.drawLegendIcon(x, y, {
|
||||
fallbackTile: item.tile,
|
||||
cols: span
|
||||
});
|
||||
}
|
||||
|
||||
// Draw the special tile when the resolved avatar visual is not canvas-ready.
|
||||
if (!iconDrawn && window.options?.tileMap?.hasOwnProperty(item.tile)) {
|
||||
display.draw(x, y, item.tile, "transparent", "transparent");
|
||||
x += item.tile.length;
|
||||
} else {
|
||||
x += span;
|
||||
} else if (!iconDrawn) {
|
||||
display.draw(x, y, "?", "transparent", "transparent");
|
||||
x += 1;
|
||||
} else {
|
||||
x += span;
|
||||
}
|
||||
|
||||
// Draw the descriptive text
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
// AvatarVisuals is the render-kit resolver for turning a saved appearance identity into a
|
||||
// drawable visual for a specific projection/context. Legacy ROT screens can use fallback tiles;
|
||||
// canvas-capable renderers can draw the same identity as a composed character or premium skin.
|
||||
(function (root) {
|
||||
'use strict';
|
||||
|
||||
var RK = root.RK = root.RK || {};
|
||||
var DEFAULT_EQUIPMENT = { body: 'none', head: 'none', shield: 'none', weapon: 'none' };
|
||||
var DEFAULT_APPEARANCE = { avatar: 'default', tint: 'none', equipment: DEFAULT_EQUIPMENT };
|
||||
var loading = {};
|
||||
|
||||
function normalize(appearance) {
|
||||
if (RK.normalizeAppearance) return RK.normalizeAppearance(appearance || DEFAULT_APPEARANCE);
|
||||
return DEFAULT_APPEARANCE;
|
||||
}
|
||||
|
||||
function kindFor(appearance) {
|
||||
if (!appearance) return 'legacy';
|
||||
if (RK.isChar && RK.isChar(appearance.avatar)) return 'char';
|
||||
if (RK.isSkin && RK.isSkin(appearance.avatar)) return 'skin';
|
||||
if (appearance.avatar && /^kenney-/.test(appearance.avatar)) return 'model3d';
|
||||
return 'legacy';
|
||||
}
|
||||
|
||||
function fallbackTile(opts) {
|
||||
opts = opts || {};
|
||||
if (opts.fallbackTile) return opts.fallbackTile;
|
||||
if (root.GameTiles && root.GameTiles.getPlayerTile) return root.GameTiles.getPlayerTile();
|
||||
return '@';
|
||||
}
|
||||
|
||||
function labelFor(appearance) {
|
||||
if (RK.appearance) {
|
||||
var info = RK.appearance(appearance);
|
||||
if (info && info.label) return info.label;
|
||||
}
|
||||
return 'Character';
|
||||
}
|
||||
|
||||
function resolve(appearance, opts) {
|
||||
opts = opts || {};
|
||||
var ap = normalize(appearance);
|
||||
var kind = kindFor(ap);
|
||||
return {
|
||||
appearance: ap,
|
||||
kind: kind,
|
||||
projection: opts.projection || 'legacy-rot',
|
||||
context: opts.context || 'player',
|
||||
label: labelFor(ap),
|
||||
fallbackTile: fallbackTile(opts),
|
||||
canvas: kind === 'char' || kind === 'skin'
|
||||
};
|
||||
}
|
||||
|
||||
function rememberLoad(key, start, onReady) {
|
||||
if (loading[key]) {
|
||||
if (onReady) loading[key].push(onReady);
|
||||
return;
|
||||
}
|
||||
loading[key] = onReady ? [onReady] : [];
|
||||
start(function () {
|
||||
var cbs = loading[key] || [];
|
||||
delete loading[key];
|
||||
cbs.forEach(function (cb) { try { cb(); } catch (_) { /* ignore */ } });
|
||||
});
|
||||
}
|
||||
|
||||
function ensureCanvasReady(visual, onReady) {
|
||||
visual = visual && visual.appearance ? visual : resolve(visual);
|
||||
if (visual.kind === 'char') {
|
||||
if (RK.charAtlas && RK.charAtlas()) return true;
|
||||
if (!RK.loadCharAtlas) return false;
|
||||
rememberLoad('char-atlas', function (done) { RK.loadCharAtlas(done); }, onReady);
|
||||
return false;
|
||||
}
|
||||
if (visual.kind === 'skin') {
|
||||
if (RK.skinSheet && RK.skinSheet(visual.appearance.avatar)) return true;
|
||||
if (!RK.loadSkin) return false;
|
||||
rememberLoad('skin:' + visual.appearance.avatar, function (done) {
|
||||
var rec = RK.loadSkin(visual.appearance.avatar);
|
||||
if (rec && rec.cbs) rec.cbs.push(done);
|
||||
}, onReady);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function drawCharIcon(ctx, visual, x, y, size) {
|
||||
if (!RK.drawCharCompositeCanvas) return false;
|
||||
return RK.drawCharCompositeCanvas(ctx, visual.appearance, x, y, size);
|
||||
}
|
||||
|
||||
function drawSkinIcon(ctx, visual, x, y, size) {
|
||||
var rec = RK.skinSheet && RK.skinSheet(visual.appearance.avatar);
|
||||
var skin = RK.SKINS && RK.SKINS[visual.appearance.avatar];
|
||||
if (!rec || !skin) return false;
|
||||
var row = (skin.dirRows && skin.dirRows.down) || 0;
|
||||
var dh = size;
|
||||
var dw = dh * (skin.frameW / skin.frameH);
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.drawImage(rec.img, 0, row * skin.frameH, skin.frameW, skin.frameH, x + (size - dw) / 2, y, dw, dh);
|
||||
return true;
|
||||
}
|
||||
|
||||
function drawIcon(ctx, visual, rect, opts) {
|
||||
opts = opts || {};
|
||||
visual = visual && visual.appearance ? visual : resolve(visual, opts);
|
||||
if (!ctx || !rect || !ensureCanvasReady(visual, opts.onReady)) return false;
|
||||
var size = Math.min(rect.w || rect.size || 32, rect.h || rect.size || 32);
|
||||
var x = rect.x + Math.max(0, ((rect.w || size) - size) / 2);
|
||||
var y = rect.y + Math.max(0, ((rect.h || size) - size) / 2);
|
||||
ctx.clearRect(rect.x, rect.y, rect.w || size, rect.h || size);
|
||||
if (visual.kind === 'char') return drawCharIcon(ctx, visual, x, y, size);
|
||||
if (visual.kind === 'skin') return drawSkinIcon(ctx, visual, x, y, size);
|
||||
return false;
|
||||
}
|
||||
|
||||
function drawTopdownChar(ctx, visual, entity, viewport, opts) {
|
||||
opts = opts || {};
|
||||
var ch = RK.CHARS && RK.CHARS[visual.appearance.avatar];
|
||||
if (!ch || !RK.charFrame || !RK.drawCharTileCanvas) return false;
|
||||
var cell = viewport.cell || 32;
|
||||
var e = {
|
||||
id: entity.id || 'avatar',
|
||||
x: entity.x || 0,
|
||||
y: entity.y || 0,
|
||||
facing: entity.facing || 'down'
|
||||
};
|
||||
var frame = RK.charFrame(ch, e, opts.now || Date.now());
|
||||
var dw = cell * (opts.scale || 1.8);
|
||||
var dh = dw * frame.squash;
|
||||
var cx = viewport.screenX * cell + cell / 2;
|
||||
var dy = (viewport.screenY * cell + cell) - dh + frame.bob;
|
||||
var ap = RK.charAppearance({ avatar: visual.appearance.avatar, appearance: visual.appearance });
|
||||
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
function drawComposite(dx, dy2) {
|
||||
RK.drawCharTileCanvas(ctx, ch.frame, ap.tint, dx, dy2, dw, dh, ap.colors, 'base');
|
||||
RK.charOverlayParts(ap).forEach(function (part) {
|
||||
RK.drawCharTileCanvas(ctx, part.frame, part.tint, dx, dy2, dw, dh, part.colorable ? ap.colors : null, part.slot);
|
||||
});
|
||||
}
|
||||
if (frame.flip) {
|
||||
ctx.save();
|
||||
ctx.translate(cx + dw / 2, dy);
|
||||
ctx.scale(-1, 1);
|
||||
drawComposite(0, 0);
|
||||
ctx.restore();
|
||||
} else {
|
||||
drawComposite(cx - dw / 2, dy);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function drawTopdownSkin(ctx, visual, entity, viewport, opts) {
|
||||
opts = opts || {};
|
||||
var rec = RK.skinSheet && RK.skinSheet(visual.appearance.avatar);
|
||||
var skin = RK.SKINS && RK.SKINS[visual.appearance.avatar];
|
||||
if (!rec || !skin || !RK.skinFrame) return false;
|
||||
var cell = viewport.cell || 32;
|
||||
var e = {
|
||||
id: entity.id || 'avatar-skin',
|
||||
x: entity.x || 0,
|
||||
y: entity.y || 0,
|
||||
facing: entity.facing || 'down'
|
||||
};
|
||||
var frame = RK.skinFrame(skin, e, opts.now || Date.now());
|
||||
var dh = cell * (skin.scale || 1.7);
|
||||
var dw = dh * (skin.frameW / skin.frameH);
|
||||
var cx = viewport.screenX * cell + cell / 2;
|
||||
var dx = cx - dw / 2;
|
||||
var dy = (viewport.screenY * cell + cell) - dh;
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.drawImage(rec.img, frame.col * skin.frameW, frame.row * skin.frameH, skin.frameW, skin.frameH, dx, dy, dw, dh);
|
||||
return true;
|
||||
}
|
||||
|
||||
function drawTopdownWorld(ctx, visual, entity, viewport, opts) {
|
||||
opts = opts || {};
|
||||
visual = visual && visual.appearance ? visual : resolve(visual, opts);
|
||||
if (!ctx || !entity || !viewport || !ensureCanvasReady(visual, opts.onReady)) return false;
|
||||
if (visual.kind === 'char') return drawTopdownChar(ctx, visual, entity, viewport, opts);
|
||||
if (visual.kind === 'skin') return drawTopdownSkin(ctx, visual, entity, viewport, opts);
|
||||
return false;
|
||||
}
|
||||
|
||||
RK.avatarVisuals = {
|
||||
DEFAULT_APPEARANCE: DEFAULT_APPEARANCE,
|
||||
normalize: normalize,
|
||||
kindFor: kindFor,
|
||||
resolve: resolve,
|
||||
ensureCanvasReady: ensureCanvasReady,
|
||||
drawIcon: drawIcon,
|
||||
drawTopdownWorld: drawTopdownWorld
|
||||
};
|
||||
RK.resolveAvatarVisual = resolve;
|
||||
})(window);
|
||||
@@ -13,6 +13,7 @@
|
||||
<script src="js/render/atlas.js"></script>
|
||||
<script src="js/render/charSprites.js"></script>
|
||||
<script src="js/render/assetPacks.js"></script>
|
||||
<script src="js/render/avatarVisuals.js"></script>
|
||||
<script src="js/render/designedRoom.js"></script>
|
||||
<script src="js/render/asciiRenderer.js"></script>
|
||||
<script src="js/render/tileRenderer.js"></script>
|
||||
|
||||
@@ -5,14 +5,16 @@ describe('single-player character identity wiring', () => {
|
||||
const indexHtml = fs.readFileSync(path.join(__dirname, '../html/index.html'), 'utf8');
|
||||
const socketHandlers = fs.readFileSync(path.join(__dirname, '../html/js/network/socketHandlers.js'), 'utf8');
|
||||
const renderEngine = fs.readFileSync(path.join(__dirname, '../html/js/display/renderEngine.js'), 'utf8');
|
||||
const screenManager = fs.readFileSync(path.join(__dirname, '../html/js/display/screenManager.js'), 'utf8');
|
||||
|
||||
test('main page loads shared character renderer dependencies before the overlay bridge', () => {
|
||||
test('main page loads the avatar visual resolver before the overlay bridge', () => {
|
||||
const order = [
|
||||
'js/render/atlas.js',
|
||||
'js/render/skins.js',
|
||||
'js/render/charSprites.js',
|
||||
'js/render/assetPacks.js',
|
||||
'js/render/charCustomize.js',
|
||||
'js/render/avatarVisuals.js',
|
||||
'js/core/singlePlayerAvatar.js'
|
||||
].map(src => indexHtml.indexOf(src));
|
||||
|
||||
@@ -32,4 +34,10 @@ describe('single-player character identity wiring', () => {
|
||||
expect(renderEngine).toContain('if (!avatarOverlayReady)');
|
||||
expect(renderEngine).toContain('SinglePlayerAvatar.drawPlayer');
|
||||
});
|
||||
|
||||
test('welcome legend resolves the player icon through the avatar bridge', () => {
|
||||
expect(screenManager).toContain('role: "player"');
|
||||
expect(screenManager).toContain('SinglePlayerAvatar.clearOverlay');
|
||||
expect(screenManager).toContain('SinglePlayerAvatar.drawLegendIcon');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user