Files
wownerogue/html/js/display/screenManager.js
T
jwinterm d8abe89cb7 fix(home): align splash legend + make the character creator discoverable
- Splash legend (screenManager.js): every row now uses a uniform 2-cell icon slot at
  a shared left edge and a single fixed text column, so the four icons + descriptions
  line up regardless of glyph width (was: per-row span math + leading-space hacks →
  ragged columns, player sprite offset).
- Character creator was already on the homepage but read like an admin control: the
  button showed the DEFAULT character label "Operator". Renamed the default to
  "Adventurer" (charCustomize.js), switched the icon 🧍→🎭, and gave it a clear
  "Create / customize your character" tooltip so it announces itself.
2026-07-12 08:55:24 -04:00

456 lines
17 KiB
JavaScript

// ScreenManager - Handles different screen states and UI drawing
var ScreenManager = {
defaultFg: "#FFF",
_currentBlockHeight: 0,
_blockTimer: null,
_updateInterval: null, // Track the update interval separately
_waitingForBlock: false,
_isShowingWaitingScreen: false, // Track if waiting screen is active
_cryptoType: 'WOW', // Default to WOW, updated on game mode info
_ambientFXOn: false, // GFX3: FX overlay ambient state (guarded, no-op unless window.FX)
// Get the game title based on crypto type
getGameTitle: function() {
return this._cryptoType === 'XMR' ? 'MONEROGUE' : 'WOWNEROGUE';
},
// Set crypto type (called from socket handlers)
setCryptoType: function(cryptoType) {
this._cryptoType = cryptoType || 'WOW';
},
// Check if waiting screen is currently being shown
isShowingWaitingScreen: function() {
return this._isShowingWaitingScreen;
},
init: function(screenWidth, screenHeight) {
this._screenWidth = screenWidth;
this._screenHeight = screenHeight;
this._currentBlockHeight = 0;
this._waitingForBlock = false;
this._isShowingWaitingScreen = false;
this._updateInterval = null; // Initialize update interval tracker
this._cryptoType = 'WOW';
this._ambientFXOn = false; // GFX3: reset FX overlay ambient state
// Initialize the waiting screen animator
if (typeof WaitingScreenAnimator !== 'undefined') {
WaitingScreenAnimator.init();
}
},
// ===== GFX3: FX overlay helpers (additive, always guarded) =====
_fxBaseCanvas: function() {
try {
var d = (typeof DisplayManager !== 'undefined' && DisplayManager.getDisplay)
? DisplayManager.getDisplay() : null;
return (d && d.getContainer) ? d.getContainer() : null;
} catch (e) { return null; }
},
// Subtle vignette + slow ambient embers behind the teletype title.
_startAmbientFX: function() {
if (!window.FX) return;
if (this._ambientFXOn) return;
var canvas = this._fxBaseCanvas();
if (!canvas) return;
try {
if (window.FX.attach) window.FX.attach(canvas);
if (window.FX.syncTo) window.FX.syncTo(canvas);
if (window.FX.setAmbient) window.FX.setAmbient(true);
if (window.FX.start) window.FX.start();
this._ambientFXOn = true;
} catch (e) { /* additive: never break the screen */ }
},
_stopAmbientFX: function() {
this._ambientFXOn = false;
if (!window.FX) return;
try {
if (window.FX.setAmbient) window.FX.setAmbient(false);
if (window.FX.stop) window.FX.stop();
if (window.FX.clear) window.FX.clear();
} catch (e) { /* ignore */ }
},
// One-shot celebratory/somber flash as an end screen appears.
_screenFlash: function(color, alpha, ms) {
if (!window.FX) return;
var canvas = this._fxBaseCanvas();
if (!canvas) return;
try {
if (window.FX.attach) window.FX.attach(canvas);
if (window.FX.syncTo) window.FX.syncTo(canvas);
if (window.FX.start) window.FX.start();
if (window.FX.flash) window.FX.flash(color, alpha, ms);
} catch (e) { /* ignore */ }
},
drawText: function(x, y, text) {
if (!DisplayManager.ensureDisplay()) return;
const display = DisplayManager.getDisplay();
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (window.options?.tileMap?.hasOwnProperty(char) || char === ' ') {
display.draw(x + i, y, char, "transparent", "transparent");
} else {
display.draw(x + i, y, "?", "transparent", "transparent");
}
}
},
drawCenteredText: function(y, text) {
const x = Math.floor((this._screenWidth - text.length) / 2);
this.drawText(x, y, text);
},
drawBorder: function() {
if (!DisplayManager.ensureDisplay()) return;
const display = DisplayManager.getDisplay();
// Use wall tiles (#) for borders - these are guaranteed to exist in tileMap
const borderChar = "#"; // Wall tile at [64, 32]
// Draw top and bottom borders
for (var i = 0; i < this._screenWidth; i++) {
display.draw(i, 0, borderChar, "transparent", "transparent");
display.draw(i, this._screenHeight - 1, borderChar, "transparent", "transparent");
}
// Draw left and right borders
for (var j = 1; j < this._screenHeight - 1; j++) {
display.draw(0, j, borderChar, "transparent", "transparent");
display.draw(this._screenWidth - 1, j, borderChar, "transparent", "transparent");
}
},
drawWelcomeScreen: function() {
if (!DisplayManager.ensureDisplay()) return;
// Mark that we're NOT showing the waiting screen
this._isShowingWaitingScreen = false;
DisplayManager.clearDisplay();
// Draw the border
this.drawBorder();
// GFX3: subtle vignette + slow ambient embers behind the teletype title
this._startAmbientFX();
// Draw content - use dynamic title based on crypto type
let y = 3;
this.drawCenteredText(y, this.getGameTitle());
y += 3;
// Draw instructions
this.drawCenteredText(y, "Type enter to queue!");
this.drawCenteredText(y + 1, "Or use START GAME");
y += 3;
// Draw legend with special tile handling
const display = DisplayManager.getDisplay();
// Use dynamic treasure tile based on crypto type
const treasureTile = (typeof GameTiles !== 'undefined' && GameTiles.getTreasureTile)
? GameTiles.getTreasureTile()
: (this._cryptoType === 'XMR' ? '$M' : '$W');
// Every row uses a uniform icon slot at `centerX` and a single fixed text column, so the
// icons share a left edge and all four descriptions line up — regardless of whether the
// icon is a 1-cell glyph (>, ~, $) or the 2-cell player avatar sprite.
const legendItems = [
{ tile: "@2", text: "This is you", role: "player" },
{ 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 ICON_COLS = 2; // uniform icon slot width
const centerX = Math.floor(this._screenWidth / 2) - 10;
const textX = centerX + ICON_COLS + 1; // one fixed text column for every row
for (let item of legendItems) {
let iconDrawn = false;
if (item.role === "player" && window.SinglePlayerAvatar && window.SinglePlayerAvatar.drawLegendIcon) {
iconDrawn = window.SinglePlayerAvatar.drawLegendIcon(centerX, y, {
fallbackTile: item.tile,
cols: ICON_COLS
});
}
// Grid-tile icons: centre the (1-cell) glyph within the 2-cell slot so it aligns under
// the player sprite above it.
if (!iconDrawn) {
const glyph = window.options?.tileMap?.hasOwnProperty(item.tile) ? item.tile : "?";
const gx = centerX + Math.floor((ICON_COLS - (glyph.length || 1)) / 2);
display.draw(gx, y, glyph, "transparent", "transparent");
}
this.drawText(textX, y, item.text);
y++;
}
// Draw current block info on two lines
y += 2;
this.drawCenteredText(y, "Current Block:");
y += 1;
this.drawCenteredText(y, `${this._currentBlockHeight}`);
// Control HTML button visibility
const startButton = document.getElementById('startButton');
if (startButton) {
startButton.style.display = 'block';
}
// Hide animation button on welcome screen
this.hideAnimationButton();
// Debug mode detection (for future use)
const isDebugMode = window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' ||
window.location.protocol === 'file:';
},
toggleAnimation: function() {
if (typeof WaitingScreenAnimator !== 'undefined') {
WaitingScreenAnimator.toggleAnimation();
}
},
// Start simulated block mining (30 second intervals)
startBlockSimulation: function() {
this._currentBlockHeight = Math.floor(Math.random() * 1000000) + 3200000; // Realistic Wownero block height
this._waitingForBlock = true;
// Update welcome screen every second during mining
this._updateInterval = setInterval(() => {
if (this._waitingForBlock && !GameState.isGameActive()) {
this.drawWelcomeScreen();
} else {
if (this._updateInterval) {
clearInterval(this._updateInterval);
this._updateInterval = null;
}
}
}, 1000);
// Simulate new block every 30 seconds
this._blockTimer = setInterval(() => {
if (!GameState.isGameActive()) { // Only continue if game is not active
this._currentBlockHeight++;
this._waitingForBlock = false;
this.drawWelcomeScreen();
// Show block found message briefly
setTimeout(() => {
if (!DisplayManager.ensureDisplay() || GameState.isGameActive()) return;
let y = Math.floor(this._screenHeight / 2);
this.drawCenteredText(y, "*** NEW BLOCK FOUND! ***");
this.drawCenteredText(y + 1, "You may now enter the dungeon!");
// Allow entry for 5 seconds
setTimeout(() => {
if (!GameState.isGameActive()) {
this._waitingForBlock = true;
this.drawWelcomeScreen();
}
}, 5000);
}, 500);
}
}, 30000); // 30 second intervals
},
// Stop block simulation
stopBlockSimulation: function() {
if (this._blockTimer) {
clearInterval(this._blockTimer);
this._blockTimer = null;
}
if (this._updateInterval) {
clearInterval(this._updateInterval);
this._updateInterval = null;
}
this._waitingForBlock = false;
},
// Check if player can enter (not waiting for block or debug mode)
canEnterGame: function() {
const isDebugMode = window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' ||
window.location.protocol === 'file:';
return !this._waitingForBlock || isDebugMode;
},
drawWinScreen: function(hasTreasure) {
if (!DisplayManager.ensureDisplay()) return;
DisplayManager.clearDisplay();
// GFX3: brief celebratory gold flash + burst as the win screen appears
if (window.FX) {
this._stopAmbientFX();
this._screenFlash('#ffd700', 0.45, 700);
var winCanvas = this._fxBaseCanvas();
if (winCanvas && window.FX.burst) {
try {
window.FX.burst(winCanvas.width / 2, winCanvas.height / 2, '#ffd700', 24);
} catch (e) { /* ignore */ }
}
}
let y = Math.floor(this._screenHeight / 3);
this.drawCenteredText(y, "CONGRATULATIONS!");
if (hasTreasure) {
this.drawCenteredText(y + 2, "You got the treasure!");
} else {
this.drawCenteredText(y + 2, "You escaped alive!");
}
this.drawCenteredText(y + 3, "Hero of Wownero!");
},
drawLoseScreen: function(reason) {
if (!DisplayManager.ensureDisplay()) return;
DisplayManager.clearDisplay();
// GFX3: brief somber red flash as the lose screen appears
if (window.FX) {
this._stopAmbientFX();
this._screenFlash('#ff2a2a', 0.4, 700);
}
let y = Math.floor(this._screenHeight / 3);
this.drawCenteredText(y, "YOU HAVE PERISHED");
if (reason === 'monster') {
this.drawCenteredText(y + 2, "Slain by a beast.");
} else if (reason === 'timeout') {
this.drawCenteredText(y + 2, "Time ran out.");
} else {
this.drawCenteredText(y + 2, "Your adventure ends.");
}
this.drawCenteredText(y + 3, "Better luck next time.");
},
drawWaitingScreen: function(_internalLoopCall = false) {
if (!DisplayManager.ensureDisplay()) return;
// Mark that we're showing the waiting screen
this._isShowingWaitingScreen = true;
DisplayManager.clearDisplay();
const animationEnabled = (typeof WaitingScreenAnimator !== 'undefined') ?
WaitingScreenAnimator.isAnimationEnabled() : false;
// Delegate to WaitingScreenAnimator with proper parameters
if (typeof WaitingScreenAnimator !== 'undefined') {
if (animationEnabled) {
WaitingScreenAnimator.drawAnimatedWaitingScreen(
this._screenWidth,
this._screenHeight,
this.drawBorder.bind(this),
this.drawCenteredText.bind(this)
);
} else {
WaitingScreenAnimator.drawStaticWaitingScreen(
this._screenWidth,
this._screenHeight,
this.drawBorder.bind(this),
this.drawCenteredText.bind(this)
);
}
} else {
// Fallback if WaitingScreenAnimator is not available
this.drawFallbackWaitingScreen();
}
// Show the animation toggle button
this.showAnimationButton();
// Start animation loop if not already running
// Only start the animator if this wasn't invoked from inside the animator loop
if (! _internalLoopCall && animationEnabled && typeof WaitingScreenAnimator !== 'undefined') {
WaitingScreenAnimator.startAnimation();
}
},
showAnimationButton: function() {
// First ensure the button exists
if (!this.ensureAnimationButton()) {
console.error("❌ Cannot show animation button - element missing from DOM");
return;
}
const animButton = document.getElementById('animationToggleButton');
if (animButton) {
animButton.style.display = 'inline-block';
animButton.style.visibility = 'visible';
const animationEnabled = (typeof WaitingScreenAnimator !== 'undefined') ?
WaitingScreenAnimator.isAnimationEnabled() : false;
animButton.textContent = animationEnabled ? '🎬 DISABLE ANIMATION' : '🎬 ENABLE ANIMATION';
} else {
// Animation button element not found - non-critical warning
// console.warn("❌ Animation button element not found!");
}
},
hideAnimationButton: function() {
const animButton = document.getElementById('animationToggleButton');
if (animButton) {
animButton.style.display = 'none';
}
},
ensureAnimationButton: function() {
let animButton = document.getElementById('animationToggleButton');
if (!animButton) {
// Animation button not found - create or handle missing button
const statusDiv = document.querySelector('.status');
if (statusDiv) {
const existingButtons = statusDiv.querySelectorAll('button');
// Check existing buttons for debugging
for (let btn of existingButtons) {
// Button discovery for debugging
}
}
return false;
}
return true;
},
stopWaitingScreen: function() {
if (typeof WaitingScreenAnimator !== 'undefined') {
WaitingScreenAnimator.stopAnimation();
}
// GFX3: waiting animator tears down the shared FX overlay; clear our flag
// so ambient FX can restart when the welcome screen is shown again.
this._ambientFXOn = false;
this.hideAnimationButton();
if (!DisplayManager.ensureDisplay()) return;
DisplayManager.clearDisplay();
},
drawFallbackWaitingScreen: function() {
this.drawBorder();
let y = Math.floor(this._screenHeight / 2) - 1;
this.drawCenteredText(y, "Waiting for next block...");
this.drawCenteredText(y + 1, "Loading animation module...");
}
};
// Make it available globally
if (typeof window !== 'undefined') {
window.ScreenManager = ScreenManager;
}