feat: enhance puzzle rendering with clean textures and improved UI/UX
- Add clean texture variants (`piece_clean_N`) for merged pieces to remove outlines after grouping - Improve piece renderer progress tracking to distinguish main vs. clean textures - Support 140-piece puzzles (14×10 grid) across generator, menu, and selection scenes - Auto-open join dialog when `?room=` URL parameter present in main menu - Add shareable room link with copy-to-clipboard button in puzzle scene UI - Introduce `_updatePieceEffects()` to manage visual states: clean textures for merged groups, drop shadows for singletons/groups - Enhance music playback with fallback completion detection and robust event handling
This commit is contained in:
parent
8b30269b56
commit
5b1c8214a6
|
|
@ -26,17 +26,27 @@ class PieceRenderer {
|
||||||
const dims = { pieceW, pieceH, tabSize, canvasW, canvasH };
|
const dims = { pieceW, pieceH, tabSize, canvasW, canvasH };
|
||||||
|
|
||||||
// Build a set of expected keys so the listener ignores unrelated textures
|
// Build a set of expected keys so the listener ignores unrelated textures
|
||||||
const pending = new Set(pieceDataArray.map(p => `piece_${p.id}`));
|
// Track both outlined (piece_N) and clean (piece_clean_N) textures
|
||||||
|
const pending = new Set();
|
||||||
|
pieceDataArray.forEach(p => {
|
||||||
|
pending.add(`piece_${p.id}`);
|
||||||
|
pending.add(`piece_clean_${p.id}`);
|
||||||
|
});
|
||||||
|
const totalTextures = pending.size;
|
||||||
|
let texDone = 0;
|
||||||
let done = 0;
|
let done = 0;
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
// Single listener for all pieces — Phaser 3.9 emits 'addtexture' with key arg
|
|
||||||
const onAdded = (key) => {
|
const onAdded = (key) => {
|
||||||
if (!pending.has(key)) return;
|
if (!pending.has(key)) return;
|
||||||
pending.delete(key);
|
pending.delete(key);
|
||||||
done++;
|
texDone++;
|
||||||
if (onProgress) onProgress(done, total);
|
// Only count progress for main piece textures (not clean variants)
|
||||||
if (done === total) {
|
if (key.startsWith('piece_') && !key.startsWith('piece_clean_')) {
|
||||||
|
done++;
|
||||||
|
if (onProgress) onProgress(done, total);
|
||||||
|
}
|
||||||
|
if (texDone === totalTextures) {
|
||||||
scene.textures.off('addtexture', onAdded);
|
scene.textures.off('addtexture', onAdded);
|
||||||
resolve(dims);
|
resolve(dims);
|
||||||
}
|
}
|
||||||
|
|
@ -78,6 +88,33 @@ class PieceRenderer {
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
|
// Save clean version (used after pieces merge)
|
||||||
|
const cleanKey = `piece_clean_${piece.id}`;
|
||||||
|
if (scene.textures.exists(cleanKey)) scene.textures.remove(cleanKey);
|
||||||
|
scene.textures.addBase64(cleanKey, canvas.toDataURL('image/png'));
|
||||||
|
|
||||||
|
// Dark outer glow (drawn behind the stroke, outside the clip)
|
||||||
|
ctx.save();
|
||||||
|
buildPiecePath(ctx, piece.gridCol, piece.gridRow, cols, rows,
|
||||||
|
pieceW, pieceH, piece.edges, tabSize);
|
||||||
|
ctx.shadowColor = 'rgba(0, 0, 0, 0.7)';
|
||||||
|
ctx.shadowBlur = 8;
|
||||||
|
ctx.shadowOffsetX = 0;
|
||||||
|
ctx.shadowOffsetY = 0;
|
||||||
|
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// Bright off-white stroke along the piece outline
|
||||||
|
ctx.save();
|
||||||
|
buildPiecePath(ctx, piece.gridCol, piece.gridRow, cols, rows,
|
||||||
|
pieceW, pieceH, piece.edges, tabSize);
|
||||||
|
ctx.strokeStyle = 'rgba(240, 240, 255, 0.6)';
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
const key = `piece_${piece.id}`;
|
const key = `piece_${piece.id}`;
|
||||||
if (scene.textures.exists(key)) scene.textures.remove(key);
|
if (scene.textures.exists(key)) scene.textures.remove(key);
|
||||||
scene.textures.addBase64(key, canvas.toDataURL('image/png'));
|
scene.textures.addBase64(key, canvas.toDataURL('image/png'));
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ const GRID_SIZES = {
|
||||||
20: { cols: 5, rows: 4 },
|
20: { cols: 5, rows: 4 },
|
||||||
40: { cols: 8, rows: 5 },
|
40: { cols: 8, rows: 5 },
|
||||||
60: { cols: 10, rows: 6 },
|
60: { cols: 10, rows: 6 },
|
||||||
100: { cols: 10, rows: 10 }
|
100: { cols: 10, rows: 10 },
|
||||||
|
140: { cols: 14, rows: 10 }
|
||||||
};
|
};
|
||||||
|
|
||||||
class PuzzleGenerator {
|
class PuzzleGenerator {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* global Phaser, NetworkManager, StorageManager */
|
/* global Phaser, NetworkManager, StorageManager, getRoomCodeFromURL */
|
||||||
|
|
||||||
class MainMenuScene extends Phaser.Scene {
|
class MainMenuScene extends Phaser.Scene {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -36,6 +36,12 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
|
|
||||||
// Run animated intro sequence
|
// Run animated intro sequence
|
||||||
this._animateIntro();
|
this._animateIntro();
|
||||||
|
|
||||||
|
// Auto-open join dialog if URL has ?room= parameter
|
||||||
|
const urlRoom = getRoomCodeFromURL();
|
||||||
|
if (urlRoom) {
|
||||||
|
this.time.delayedCall(1600, () => this._showJoinDialog(urlRoom));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Animated Intro ─────────────────────────────────────────────────
|
// ─── Animated Intro ─────────────────────────────────────────────────
|
||||||
|
|
@ -240,7 +246,7 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_showJoinDialog() {
|
_showJoinDialog(prefillCode) {
|
||||||
if (this._joinDialogEl) return;
|
if (this._joinDialogEl) return;
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
|
|
@ -319,6 +325,9 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
input.maxLength = 4;
|
input.maxLength = 4;
|
||||||
input.placeholder = 'ABCD';
|
input.placeholder = 'ABCD';
|
||||||
input.autocomplete = 'off';
|
input.autocomplete = 'off';
|
||||||
|
if (prefillCode) {
|
||||||
|
input.value = prefillCode.toUpperCase().replace(/[^A-Z0-9]/g, '').substring(0, 4);
|
||||||
|
}
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener('input', () => {
|
||||||
input.value = input.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
input.value = input.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||||
updateJoinBtn();
|
updateJoinBtn();
|
||||||
|
|
@ -382,10 +391,15 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
this._uiLayer.appendChild(overlay);
|
this._uiLayer.appendChild(overlay);
|
||||||
this._joinDialogEl = overlay;
|
this._joinDialogEl = overlay;
|
||||||
|
|
||||||
// Focus the name input (or code input if name already filled)
|
// Focus the name input (or code input if name already filled and no prefill)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (nameInput.value.trim().length > 0) input.focus();
|
if (prefillCode) {
|
||||||
else nameInput.focus();
|
nameInput.focus();
|
||||||
|
} else if (nameInput.value.trim().length > 0) {
|
||||||
|
input.focus();
|
||||||
|
} else {
|
||||||
|
nameInput.focus();
|
||||||
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
// Initial button state
|
// Initial button state
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ const DIFFICULTIES = [
|
||||||
{ pieces: 20, label: '20 Pieces' },
|
{ pieces: 20, label: '20 Pieces' },
|
||||||
{ pieces: 40, label: '40 Pieces' },
|
{ pieces: 40, label: '40 Pieces' },
|
||||||
{ pieces: 60, label: '60 Pieces' },
|
{ pieces: 60, label: '60 Pieces' },
|
||||||
{ pieces: 100, label: '100 Pieces' }
|
{ pieces: 100, label: '100 Pieces' },
|
||||||
|
{ pieces: 140, label: '140 Pieces' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const BACKGROUNDS = [
|
const BACKGROUNDS = [
|
||||||
|
|
@ -25,7 +26,7 @@ class NewPuzzleScene extends Phaser.Scene {
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
this.selectedImageIdx = null;
|
this.selectedImageIdx = null;
|
||||||
this.selectedPieces = null;
|
this.selectedPieces = 60;
|
||||||
this.selectedBg = BACKGROUNDS[0]; // default to dark wood
|
this.selectedBg = BACKGROUNDS[0]; // default to dark wood
|
||||||
|
|
||||||
const W = this.sys.game.config.width;
|
const W = this.sys.game.config.width;
|
||||||
|
|
@ -273,11 +274,12 @@ class NewPuzzleScene extends Phaser.Scene {
|
||||||
this._diffBtnEls = DIFFICULTIES.map((diff, i) => {
|
this._diffBtnEls = DIFFICULTIES.map((diff, i) => {
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.textContent = diff.label;
|
btn.textContent = diff.label;
|
||||||
|
const isDefault = diff.pieces === this.selectedPieces;
|
||||||
Object.assign(btn.style, {
|
Object.assign(btn.style, {
|
||||||
padding: '0.7vmin 2.5vmin',
|
padding: '0.7vmin 2.5vmin',
|
||||||
background: 'rgba(10, 10, 30, 0.8)',
|
background: isDefault ? '#1565c0' : 'rgba(10, 10, 30, 0.8)',
|
||||||
color: '#e0e0e0',
|
color: '#e0e0e0',
|
||||||
border: '1px solid rgba(100, 181, 246, 0.3)',
|
border: '1px solid ' + (isDefault ? '#64b5f6' : 'rgba(100, 181, 246, 0.3)'),
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
fontSize: '1.6vmin',
|
fontSize: '1.6vmin',
|
||||||
fontFamily: 'Arial, sans-serif',
|
fontFamily: 'Arial, sans-serif',
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,11 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
update() {
|
update() {
|
||||||
if (!this._ready) return;
|
if (!this._ready) return;
|
||||||
|
|
||||||
|
// Fallback: detect when music track has finished (Phaser 3.9 'complete' event can be unreliable)
|
||||||
|
if (this._currentMusic && this._musicStarted && !this._currentMusic.isPlaying && !this._currentMusic.isPaused) {
|
||||||
|
this._playNextTrack();
|
||||||
|
}
|
||||||
|
|
||||||
// Camera pan (right-click drag, or left-click drag on empty space)
|
// Camera pan (right-click drag, or left-click drag on empty space)
|
||||||
const ptr = this.input.activePointer;
|
const ptr = this.input.activePointer;
|
||||||
if (this._isPanning && ptr.isDown && !this._heldPiece) {
|
if (this._isPanning && ptr.isDown && !this._heldPiece) {
|
||||||
|
|
@ -330,6 +335,9 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
// DOM UI (immune to camera zoom)
|
// DOM UI (immune to camera zoom)
|
||||||
this._buildDomUI();
|
this._buildDomUI();
|
||||||
|
|
||||||
|
// Apply piece visual effects (outlines on singletons, shadows on groups)
|
||||||
|
this._updatePieceEffects();
|
||||||
|
|
||||||
// Check already complete on restore
|
// Check already complete on restore
|
||||||
if (saved && saved.completed) {
|
if (saved && saved.completed) {
|
||||||
this._showCompletion(false);
|
this._showCompletion(false);
|
||||||
|
|
@ -469,6 +477,7 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
if (this._groupManager.groupCount < prevCount) {
|
if (this._groupManager.groupCount < prevCount) {
|
||||||
this.sound.play('sfx_click');
|
this.sound.play('sfx_click');
|
||||||
}
|
}
|
||||||
|
this._updatePieceEffects();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove floating label
|
// Remove floating label
|
||||||
|
|
@ -796,6 +805,9 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
this._glowGraphics.clear();
|
this._glowGraphics.clear();
|
||||||
this._heldPiece = null;
|
this._heldPiece = null;
|
||||||
|
|
||||||
|
// Refresh shadows after piece positions changed
|
||||||
|
this._updatePieceEffects();
|
||||||
|
|
||||||
this._saveState();
|
this._saveState();
|
||||||
this._checkCompletion();
|
this._checkCompletion();
|
||||||
}
|
}
|
||||||
|
|
@ -843,6 +855,29 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
// Update heldPiece reference so getPeersOf returns the enlarged group
|
// Update heldPiece reference so getPeersOf returns the enlarged group
|
||||||
// (heldPiece pointer stays the same object, groupManager now returns merged group)
|
// (heldPiece pointer stays the same object, groupManager now returns merged group)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._updatePieceEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Piece visual effects ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swap merged pieces to clean textures (no outline) and
|
||||||
|
* redraw drop shadows behind merged groups.
|
||||||
|
*/
|
||||||
|
_updatePieceEffects() {
|
||||||
|
this._groupManager.groups.forEach((members) => {
|
||||||
|
const isMerged = members.size > 1;
|
||||||
|
|
||||||
|
members.forEach(id => {
|
||||||
|
const po = this._pieceObjects.get(id);
|
||||||
|
if (!po) return;
|
||||||
|
const expectedKey = isMerged ? `piece_clean_${id}` : `piece_${id}`;
|
||||||
|
if (po.image.texture.key !== expectedKey && this.textures.exists(expectedKey)) {
|
||||||
|
po.image.setTexture(expectedKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Glow ────────────────────────────────────────────────────────────
|
// ─── Glow ────────────────────────────────────────────────────────────
|
||||||
|
|
@ -859,8 +894,13 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
);
|
);
|
||||||
|
|
||||||
segments.forEach(({ x0, y0, x1, y1 }) => {
|
segments.forEach(({ x0, y0, x1, y1 }) => {
|
||||||
// Three passes: outer, mid, inner glow
|
// Four passes: wide outer, outer, mid, inner glow
|
||||||
[[10, 0x00ccff, 0.10], [6, 0x00eeff, 0.28], [3, 0xaaffff, 0.65]].forEach(([lw, color, alpha]) => {
|
[
|
||||||
|
[20, 0x00ccff, 0.06],
|
||||||
|
[14, 0x00ccff, 0.12],
|
||||||
|
[8, 0x00eeff, 0.30],
|
||||||
|
[4, 0xaaffff, 0.70],
|
||||||
|
].forEach(([lw, color, alpha]) => {
|
||||||
this._glowGraphics.lineStyle(lw, color, alpha);
|
this._glowGraphics.lineStyle(lw, color, alpha);
|
||||||
this._glowGraphics.beginPath();
|
this._glowGraphics.beginPath();
|
||||||
this._glowGraphics.moveTo(x0, y0);
|
this._glowGraphics.moveTo(x0, y0);
|
||||||
|
|
@ -1000,9 +1040,11 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
|
|
||||||
const idx = this._musicQueue.shift();
|
const idx = this._musicQueue.shift();
|
||||||
const key = `music_${idx}`;
|
const key = `music_${idx}`;
|
||||||
|
this._musicStarted = false;
|
||||||
this._currentMusic = this.sound.add(key, { volume: 0.3 });
|
this._currentMusic = this.sound.add(key, { volume: 0.3 });
|
||||||
this._currentMusic.setMute(this._musicMuted);
|
this._currentMusic.setMute(this._musicMuted);
|
||||||
this._currentMusic.play();
|
this._currentMusic.play();
|
||||||
|
this._musicStarted = true;
|
||||||
|
|
||||||
// Update track info display
|
// Update track info display
|
||||||
const track = this._musicTracks[idx];
|
const track = this._musicTracks[idx];
|
||||||
|
|
@ -1010,8 +1052,12 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
this._trackInfoEl.textContent = `${track.title} — ${track.artist}`;
|
this._trackInfoEl.textContent = `${track.title} — ${track.artist}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When track ends, play next
|
// When track ends, play next (both event and polling fallback)
|
||||||
this._currentMusic.once('complete', () => this._playNextTrack());
|
this._currentMusic.once('complete', () => {
|
||||||
|
if (this._currentMusic && this._currentMusic.key === key) {
|
||||||
|
this._playNextTrack();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_toggleMute() {
|
_toggleMute() {
|
||||||
|
|
@ -1048,24 +1094,105 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
document.body.appendChild(this._uiLayer);
|
document.body.appendChild(this._uiLayer);
|
||||||
|
|
||||||
// Room code — bottom-right
|
// Room code + share link — bottom-right
|
||||||
|
const roomCode = this.cfg.roomCode;
|
||||||
|
const shareUrl = `${window.location.origin}${window.location.pathname}?room=${roomCode}`;
|
||||||
|
|
||||||
const roomEl = document.createElement('div');
|
const roomEl = document.createElement('div');
|
||||||
Object.assign(roomEl.style, {
|
Object.assign(roomEl.style, {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: '1.5%',
|
bottom: '48px',
|
||||||
right: '1%',
|
right: '20px',
|
||||||
color: '#ddeeff',
|
color: '#ddeeff',
|
||||||
fontSize: '2.4vmin',
|
fontSize: '26px',
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
letterSpacing: '0.15em',
|
letterSpacing: '0.15em',
|
||||||
background: 'rgba(0, 0, 0, 0.55)',
|
background: 'rgba(0, 0, 0, 0.55)',
|
||||||
padding: '0.6vmin 1.2vmin',
|
padding: '6px 14px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px 4px 0 0',
|
||||||
});
|
});
|
||||||
roomEl.textContent = `Room: ${this.cfg.roomCode}`;
|
roomEl.textContent = `Room: ${roomCode}`;
|
||||||
this._uiLayer.appendChild(roomEl);
|
this._uiLayer.appendChild(roomEl);
|
||||||
|
|
||||||
|
const linkRow = document.createElement('div');
|
||||||
|
Object.assign(linkRow.style, {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '16px',
|
||||||
|
right: '20px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
background: 'rgba(0, 0, 0, 0.55)',
|
||||||
|
padding: '5px 14px',
|
||||||
|
borderRadius: '0 0 4px 4px',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
const linkText = document.createElement('span');
|
||||||
|
Object.assign(linkText.style, {
|
||||||
|
color: '#8899bb',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
userSelect: 'all',
|
||||||
|
});
|
||||||
|
linkText.textContent = shareUrl;
|
||||||
|
|
||||||
|
const copyBtn = document.createElement('button');
|
||||||
|
Object.assign(copyBtn.style, {
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
color: '#8899bb',
|
||||||
|
fontSize: '16px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '2px 4px',
|
||||||
|
lineHeight: '1',
|
||||||
|
});
|
||||||
|
copyBtn.textContent = '\uD83D\uDCCB';
|
||||||
|
copyBtn.title = 'Copy link';
|
||||||
|
copyBtn.addEventListener('mouseenter', () => { copyBtn.style.color = '#ddeeff'; });
|
||||||
|
copyBtn.addEventListener('mouseleave', () => { copyBtn.style.color = '#8899bb'; });
|
||||||
|
copyBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const showCopied = () => {
|
||||||
|
copyBtn.textContent = '\u2713 Copied!';
|
||||||
|
copyBtn.style.color = '#44cc66';
|
||||||
|
setTimeout(() => {
|
||||||
|
copyBtn.textContent = '\uD83D\uDCCB';
|
||||||
|
copyBtn.style.color = '#8899bb';
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
// Try modern clipboard API first, fall back to execCommand
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(shareUrl).then(showCopied).catch(() => {
|
||||||
|
// Fallback for non-secure contexts
|
||||||
|
const temp = document.createElement('textarea');
|
||||||
|
temp.value = shareUrl;
|
||||||
|
temp.style.position = 'fixed';
|
||||||
|
temp.style.opacity = '0';
|
||||||
|
document.body.appendChild(temp);
|
||||||
|
temp.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(temp);
|
||||||
|
showCopied();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const temp = document.createElement('textarea');
|
||||||
|
temp.value = shareUrl;
|
||||||
|
temp.style.position = 'fixed';
|
||||||
|
temp.style.opacity = '0';
|
||||||
|
document.body.appendChild(temp);
|
||||||
|
temp.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(temp);
|
||||||
|
showCopied();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
linkRow.appendChild(linkText);
|
||||||
|
linkRow.appendChild(copyBtn);
|
||||||
|
this._uiLayer.appendChild(linkRow);
|
||||||
|
|
||||||
// Connection status indicator (only for networked games)
|
// Connection status indicator (only for networked games)
|
||||||
if (this._isNetworked) {
|
if (this._isNetworked) {
|
||||||
this._connStatusEl = document.createElement('div');
|
this._connStatusEl = document.createElement('div');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue