LifeSimulator/js/ui.js

685 lines
25 KiB
JavaScript

/**
* User Interface & Interactive Controls Module
* Binds expanded sliders, preset actions, live HUD badges, population history chart,
* entity inspector, and the post-extinction analysis modal.
*/
export class UIManager {
constructor(sim, configManager) {
this.sim = sim;
this.configManager = configManager;
this.cacheDOMElements();
this.bindEvents();
this.syncSlidersFromConfig();
this.updateAudioButtons();
this.applyURLParams();
}
cacheDOMElements() {
// Primary Controls
this.btnPlayPause = document.getElementById('btnPlayPause');
this.btnRestart = document.getElementById('btnRestart');
this.btnSound = document.getElementById('btnSound');
this.soundIcon = document.getElementById('soundIcon');
this.soundText = document.getElementById('soundText');
this.btnMusic = document.getElementById('btnMusic');
this.musicIcon = document.getElementById('musicIcon');
this.musicText = document.getElementById('musicText');
this.btnFullscreen = document.getElementById('btnFullscreen');
this.btnExitFullscreen = document.getElementById('btnExitFullscreen');
this.btnSetDefaults = document.getElementById('btnSetDefaults');
this.btnDefaults = document.getElementById('btnDefaults');
this.btnCopyJson = document.getElementById('btnCopyJson');
this.btnRain = document.getElementById('btnRain');
this.toastMsg = document.getElementById('toastMsg');
// Continuous Screen Saver Mode
this.checkContinuous = document.getElementById('checkContinuous');
// Speed buttons
this.speedBtns = document.querySelectorAll('.speed-btn');
// Tool buttons
this.toolBtns = document.querySelectorAll('.tool-btn');
// Sliders & value displays
this.sliders = {
initialSheep: { input: document.getElementById('sliderSheep'), label: document.getElementById('valSheep'), format: (v) => v },
initialWolves: { input: document.getElementById('sliderWolves'), label: document.getElementById('valWolves'), format: (v) => v },
initialPlants: { input: document.getElementById('sliderPlants'), label: document.getElementById('valPlants'), format: (v) => v },
maxPlantCap: { input: document.getElementById('sliderMaxPlantCap'), label: document.getElementById('valMaxPlantCap'), format: (v) => v },
sheepHungerDepletionRate: { input: document.getElementById('sliderSheepHunger'), label: document.getElementById('valSheepHunger'), format: (v) => `${Number(v).toFixed(1)}/s` },
sheepGrazingReach: { input: document.getElementById('sliderSheepReach'), label: document.getElementById('valSheepReach'), format: (v) => `${v}px` },
sheepMatingCooldown: { input: document.getElementById('sliderSheepMating'), label: document.getElementById('valSheepMating'), format: (v) => `${v}s` },
wolfLungeCost: { input: document.getElementById('sliderWolfLunge'), label: document.getElementById('valWolfLunge'), format: (v) => `${v} HP` },
wolfHungerDepletionRate: { input: document.getElementById('sliderWolfHunger'), label: document.getElementById('valWolfHunger'), format: (v) => `${Number(v).toFixed(1)}/s` },
wolfAttackDamage: { input: document.getElementById('sliderWolfDamage'), label: document.getElementById('valWolfDamage'), format: (v) => v },
wolfMatingCooldown: { input: document.getElementById('sliderWolfMating'), label: document.getElementById('valWolfMating'), format: (v) => `${v}s` },
plantSeedRate: { input: document.getElementById('sliderSeedRate'), label: document.getElementById('valSeedRate'), format: (v) => `${v}%` },
plantDryRate: { input: document.getElementById('sliderDryRate'), label: document.getElementById('valDryRate'), format: (v) => `${Number(v).toFixed(1)}/s` },
rainFrequency: { input: document.getElementById('sliderRain'), label: document.getElementById('valRain'), format: (v) => `${v}%` },
decaySpeed: { input: document.getElementById('sliderDecay'), label: document.getElementById('valDecay'), format: (v) => `${Number(v).toFixed(1)}x` },
// Continuous Mode Influx intervals & wave size
continuousSheepInterval: { input: document.getElementById('sliderContinuousSheep'), label: document.getElementById('valContinuousSheep'), format: (v) => `${v}s` },
continuousWolfInterval: { input: document.getElementById('sliderContinuousWolf'), label: document.getElementById('valContinuousWolf'), format: (v) => `${v}s` },
continuousMaxGroupSize: { input: document.getElementById('sliderContinuousGroup'), label: document.getElementById('valContinuousGroup'), format: (v) => `${v}` },
};
// Live HUD badges
this.hudSheep = document.getElementById('hudSheep');
this.hudWolves = document.getElementById('hudWolves');
this.hudPlants = document.getElementById('hudPlants');
this.hudTime = document.getElementById('hudTime');
this.hudWeather = document.getElementById('hudWeather');
// Mini chart canvas
this.miniChartCanvas = document.getElementById('miniChart');
if (this.miniChartCanvas) {
this.miniChartCtx = this.miniChartCanvas.getContext('2d');
}
// Extinction modal elements
this.extinctionModal = document.getElementById('extinctionModal');
this.extinctionChartCanvas = document.getElementById('extinctionChart');
if (this.extinctionChartCanvas) {
this.extinctionChartCtx = this.extinctionChartCanvas.getContext('2d');
}
this.extTimeSurv = document.getElementById('extTimeSurv');
this.extPeakSheep = document.getElementById('extPeakSheep');
this.extPeakWolves = document.getElementById('extPeakWolves');
this.extPeakPlants = document.getElementById('extPeakPlants');
this.extTotalBorn = document.getElementById('extTotalBorn');
this.extWolfKills = document.getElementById('extWolfKills');
this.extDecayBlooms = document.getElementById('extDecayBlooms');
this.extSeeds = document.getElementById('extSeeds');
this.btnModalRestart = document.getElementById('btnModalRestart');
this.btnModalReview = document.getElementById('btnModalReview');
// Entity Inspector
this.inspectorPanel = document.getElementById('inspectorPanel');
this.inspectorDetails = document.getElementById('inspectorDetails');
this.btnCloseInspector = document.getElementById('btnCloseInspector');
}
showToast(text) {
if (!this.toastMsg) return;
this.toastMsg.textContent = text;
this.toastMsg.classList.remove('hidden');
clearTimeout(this._toastTimeout);
this._toastTimeout = setTimeout(() => {
this.toastMsg.classList.add('hidden');
}, 2400);
}
syncSlidersFromConfig() {
const cfg = this.configManager.current;
if (this.checkContinuous && cfg.continuousMode !== undefined) {
this.checkContinuous.checked = !!cfg.continuousMode;
}
for (const [key, item] of Object.entries(this.sliders)) {
if (item.input && cfg[key] !== undefined) {
item.input.value = cfg[key];
if (item.label) {
item.label.textContent = item.format(cfg[key]);
}
}
}
}
readSliders() {
const config = {};
if (this.checkContinuous) {
config.continuousMode = this.checkContinuous.checked;
}
for (const [key, item] of Object.entries(this.sliders)) {
if (item.input) {
config[key] = parseFloat(item.input.value);
}
}
return config;
}
bindEvents() {
// Audio gesture unlock for browser autoplay compliance
const resumeAudioOnGesture = () => {
if (this.sim && this.sim.audio) {
this.sim.audio.ensureContext();
if (this.sim.isRunning && this.sim.audio.musicEnabled && !this.sim.audio.isMusicPlaying) {
this.sim.audio.startMusic();
}
}
};
window.addEventListener('pointerdown', resumeAudioOnGesture, { once: true });
window.addEventListener('keydown', resumeAudioOnGesture, { once: true });
// 1. Play / Pause
this.btnPlayPause.addEventListener('click', () => {
if (this.sim.audio) {
this.sim.audio.ensureContext();
}
this.sim.isRunning = !this.sim.isRunning;
if (this.sim.isRunning && this.sim.audio && this.sim.audio.musicEnabled) {
this.sim.audio.startMusic();
} else if (!this.sim.isRunning && this.sim.audio) {
this.sim.audio.stopMusic();
}
this.updatePlayPauseButton();
});
// 2. Restart Simulation
this.btnRestart.addEventListener('click', () => {
this.extinctionModal.classList.add('hidden');
this.sim.restart();
this.sim.isRunning = true;
this.updatePlayPauseButton();
});
// 2b. Sound Effects Toggle (Midi Chomps, Bites, Chimes)
if (this.btnSound) {
this.btnSound.addEventListener('click', () => {
const isMuted = this.sim.audio.toggleMute();
this.updateAudioButtons();
this.showToast(isMuted ? '🔇 Sound FX Muted' : '🔊 Sound FX Enabled');
});
}
// 2c. Elevator Music BGM Toggle
if (this.btnMusic) {
this.btnMusic.addEventListener('click', () => {
const isMusicOn = this.sim.audio.toggleMusic();
this.updateAudioButtons();
this.showToast(isMusicOn ? '🎵 Elevator Music Playing' : '🎶 Elevator Music Paused');
});
}
// 3. Save current settings as custom defaults
if (this.btnSetDefaults) {
this.btnSetDefaults.addEventListener('click', () => {
const currentVals = this.readSliders();
this.configManager.saveAsNewDefaults(currentVals);
this.sim.config = { ...this.configManager.current };
this.showToast('✓ Saved As Custom Defaults!');
});
}
// 4. Restore Defaults
this.btnDefaults.addEventListener('click', () => {
this.configManager.resetToDefaults();
this.syncSlidersFromConfig();
this.sim.config = { ...this.configManager.current };
this.showToast('↺ Restored Defaults');
});
// 5. Copy JSON Config
if (this.btnCopyJson) {
this.btnCopyJson.addEventListener('click', () => {
const jsonStr = JSON.stringify(this.sim.config, null, 2);
navigator.clipboard.writeText(jsonStr).then(() => {
this.showToast('📋 JSON Copied to Clipboard!');
}).catch(() => {
this.showToast('Failed to copy to clipboard');
});
});
}
// 6. Summon Rain button
if (this.btnRain) {
this.btnRain.addEventListener('click', () => {
this.sim.weather.forceRain();
});
}
// 7. Speed Buttons (1x, 2x, 4x)
this.speedBtns.forEach((btn) => {
btn.addEventListener('click', () => {
this.speedBtns.forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
const speed = parseFloat(btn.dataset.speed || '1');
this.sim.speedMultiplier = speed;
});
});
// 8. Spawn / Inspect Tools
this.toolBtns.forEach((btn) => {
btn.addEventListener('click', () => {
this.toolBtns.forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
this.sim.activeTool = btn.dataset.tool || 'inspect';
});
});
// 9. Real-time Slider input listeners
for (const [key, item] of Object.entries(this.sliders)) {
if (item.input) {
item.input.addEventListener('input', () => {
const val = parseFloat(item.input.value);
if (item.label) {
item.label.textContent = item.format(val);
}
// Live update simulation config & persist
const updated = this.readSliders();
this.configManager.save(updated);
this.sim.config = { ...this.sim.config, ...updated };
});
}
}
// 10. Canvas click interactions (Spawn or Inspect)
this.sim.canvas.addEventListener('click', (e) => {
const rect = this.sim.canvas.getBoundingClientRect();
const scaleX = this.sim.canvas.width / rect.width;
const scaleY = this.sim.canvas.height / rect.height;
const clickX = (e.clientX - rect.left) * scaleX;
const clickY = (e.clientY - rect.top) * scaleY;
if (this.sim.activeTool === 'inspect') {
this.handleInspectClick(clickX, clickY);
} else {
this.sim.spawnEntity(this.sim.activeTool, clickX, clickY);
}
});
// 11. Extinction modal actions
if (this.btnModalRestart) {
this.btnModalRestart.addEventListener('click', () => {
this.extinctionModal.classList.add('hidden');
this.sim.restart();
this.sim.isRunning = true;
this.updatePlayPauseButton();
});
}
if (this.btnModalReview) {
this.btnModalReview.addEventListener('click', () => {
this.extinctionModal.classList.add('hidden');
});
}
if (this.btnCloseInspector) {
this.btnCloseInspector.addEventListener('click', () => {
this.sim.selectedEntity = null;
this.inspectorPanel.classList.add('hidden');
});
}
// 12. Fullscreen Screen Saver Mode
if (this.btnFullscreen) {
this.btnFullscreen.addEventListener('click', () => {
this.enterFullscreen();
});
}
if (this.btnExitFullscreen) {
this.btnExitFullscreen.addEventListener('click', () => {
this.exitFullscreen();
});
}
// Keyboard ESC & browser fullscreenchange listener
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement && this.hadDOMFullscreen) {
this.hadDOMFullscreen = false;
this.exitFullscreen(false);
}
});
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isFullscreen) {
this.exitFullscreen();
}
});
// 13. Continuous Mode Toggle
if (this.checkContinuous) {
this.checkContinuous.addEventListener('change', () => {
const isChecked = this.checkContinuous.checked;
this.sim.config.continuousMode = isChecked;
this.configManager.save({ continuousMode: isChecked });
if (isChecked) {
this.extinctionModal.classList.add('hidden');
this.showToast('📺 Continuous Screen Saver Mode Active');
} else {
this.showToast('Continuous Mode Disabled');
}
});
}
}
updateAudioButtons() {
if (this.sim && this.sim.audio) {
const isMuted = this.sim.audio.isMuted;
if (this.soundIcon) this.soundIcon.textContent = isMuted ? '🔇' : '🔊';
if (this.soundText) this.soundText.textContent = isMuted ? 'Muted' : 'Sound';
if (this.btnSound) {
this.btnSound.classList.toggle('btn-muted', isMuted);
}
const isMusic = this.sim.audio.musicEnabled;
if (this.musicIcon) this.musicIcon.textContent = isMusic ? '🎵' : '🎶';
if (this.musicText) this.musicText.textContent = isMusic ? 'Music' : 'Music Off';
if (this.btnMusic) {
this.btnMusic.classList.toggle('btn-muted', !isMusic);
}
}
}
applyURLParams() {
try {
const params = new URLSearchParams(window.location.search);
const isScreensaver = params.get('screensaver') === '1' || params.get('screensaver') === 'true' ||
params.get('wallpaper') === '1' || params.get('wallpaper') === 'true';
const isFullscreen = isScreensaver || params.get('fullscreen') === '1' || params.get('fullscreen') === 'true';
const isContinuous = isScreensaver || params.get('continuous') === '1' || params.get('continuous') === 'true';
if (isContinuous) {
if (this.checkContinuous) {
this.checkContinuous.checked = true;
}
this.sim.config.continuousMode = true;
this.configManager.save({ continuousMode: true });
}
if (isFullscreen) {
setTimeout(() => {
this.enterFullscreen(false);
}, 120);
}
if (params.get('sound') === '0' || params.get('mute') === '1') {
if (this.sim.audio && this.sim.audio.sfxEnabled) {
this.sim.audio.toggleSfx();
this.updateAudioButtons();
}
}
if (params.get('music') === '1') {
if (this.sim.audio && !this.sim.audio.musicEnabled) {
this.sim.audio.toggleMusic();
this.updateAudioButtons();
}
}
} catch (e) {
console.warn('URL params check skipped:', e);
}
}
enterFullscreen(requestDOMFullscreen = true) {
this.isFullscreen = true;
const appLayout = document.querySelector('.app-layout');
if (appLayout) {
appLayout.classList.add('fullscreen-mode');
}
if (this.btnExitFullscreen) {
this.btnExitFullscreen.classList.remove('hidden');
}
if (requestDOMFullscreen && document.documentElement.requestFullscreen && !document.fullscreenElement) {
document.documentElement.requestFullscreen().then(() => {
this.hadDOMFullscreen = true;
}).catch(() => {
this.hadDOMFullscreen = false;
});
}
setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 60);
}
exitFullscreen(requestExit = true) {
this.isFullscreen = false;
this.hadDOMFullscreen = false;
const appLayout = document.querySelector('.app-layout');
if (appLayout) {
appLayout.classList.remove('fullscreen-mode');
}
if (this.btnExitFullscreen) {
this.btnExitFullscreen.classList.add('hidden');
}
if (requestExit && document.fullscreenElement && document.exitFullscreen) {
document.exitFullscreen().catch(() => {});
}
setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 60);
}
handleInspectClick(x, y) {
let bestEntity = null;
let minD = 28;
const checkList = (list) => {
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (item.isDead) continue;
const d = Math.hypot(item.x - x, item.y - y);
if (d < minD) {
minD = d;
bestEntity = item;
}
}
};
checkList(this.sim.wolves);
checkList(this.sim.sheep);
checkList(this.sim.plants);
this.sim.selectedEntity = bestEntity;
this.updateInspectorPanel();
}
updateInspectorPanel() {
const e = this.sim.selectedEntity;
if (!e || e.isDead) {
this.inspectorPanel.classList.add('hidden');
return;
}
this.inspectorPanel.classList.remove('hidden');
let html = '';
if (e.species === 'sheep' || e.species === 'wolf') {
const icon = e.species === 'sheep' ? '🐑' : '🐺';
const name = e.isBaby ? `Baby ${e.species}` : `Adult ${e.species}`;
html = `
<div class="insp-title">${icon} ${name}</div>
<div class="insp-row"><span>Health:</span> <span>${Math.round(e.health)}/100</span></div>
<div class="insp-row"><span>Hunger:</span> <span>${Math.round(e.hunger)}/100</span></div>
<div class="insp-row"><span>State:</span> <span class="capitalize">${e.actionState}</span></div>
<div class="insp-row"><span>Age:</span> <span>${Math.round(e.age)}s</span></div>
${e.isBaby ? `<div class="insp-row"><span>Food for Adult:</span> <span>${e.foodEatenAsBaby}/3 meals</span></div>` : ''}
`;
} else {
const isFlower = e.type === 'flower';
html = `
<div class="insp-title">${isFlower ? '🌸 Dense Flower Tuft' : '🌿 Grass Tuft'}</div>
<div class="insp-row"><span>Moisture:</span> <span>${Math.round(e.moisture)}%</span></div>
<div class="insp-row"><span>Growth:</span> <span>${Math.round(e.growth * 100)}%</span></div>
<div class="insp-row"><span>Age:</span> <span>${Math.round(e.age)}s</span></div>
`;
}
this.inspectorDetails.innerHTML = html;
}
updatePlayPauseButton() {
if (this.sim.isRunning) {
this.btnPlayPause.innerHTML = `
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
<span>Pause</span>
`;
this.btnPlayPause.classList.add('btn-active');
} else {
this.btnPlayPause.innerHTML = `
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
<span>Play</span>
`;
this.btnPlayPause.classList.remove('btn-active');
}
}
updateHUD(stats, sheepCount, wolfCount, plantCount) {
if (this.hudSheep) this.hudSheep.textContent = sheepCount;
if (this.hudWolves) this.hudWolves.textContent = wolfCount;
if (this.hudPlants) this.hudPlants.textContent = plantCount;
if (this.hudTime) {
const totalSec = Math.floor(stats.elapsedSimTime);
const mins = Math.floor(totalSec / 60);
const secs = totalSec % 60;
this.hudTime.textContent = `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
if (this.hudWeather) {
const wState = this.sim.weather.state;
let label = '☀️ Sunny';
if (wState === 'raining') label = '🌧️ Raining';
else if (wState === 'cloudy') label = '☁️ Overcast';
else if (wState === 'clearing') label = '⛅ Clearing';
this.hudWeather.textContent = label;
}
// Refresh inspector if entity is active
if (this.sim.selectedEntity) {
this.updateInspectorPanel();
}
// Draw Mini Chart
if (this.miniChartCtx && stats.history.length > 1) {
this.drawChart(this.miniChartCtx, this.miniChartCanvas.width, this.miniChartCanvas.height, stats.history, false);
}
}
showExtinctionModal(stats) {
this.extinctionModal.classList.remove('hidden');
const totalSec = Math.floor(stats.extinctionTime || stats.elapsedSimTime);
const mins = Math.floor(totalSec / 60);
const secs = totalSec % 60;
this.extTimeSurv.textContent = `${mins}m ${secs}s`;
this.extPeakSheep.textContent = stats.peakSheep;
this.extPeakWolves.textContent = stats.peakWolves;
this.extPeakPlants.textContent = stats.peakPlants;
this.extTotalBorn.textContent = `${stats.totalSheepBorn} sheep / ${stats.totalWolvesBorn} wolves`;
this.extWolfKills.textContent = `${stats.sheepKilledByWolves} sheep hunted (${stats.sheepStarved} starved)`;
this.extDecayBlooms.textContent = `${stats.totalPlantsFromDecay} flower tufts`;
this.extSeeds.textContent = `${stats.totalSeedsSprouted} seed puffs sprouted`;
// Render large extinction history chart
if (this.extinctionChartCtx) {
this.drawChart(
this.extinctionChartCtx,
this.extinctionChartCanvas.width,
this.extinctionChartCanvas.height,
stats.history,
true
);
}
}
drawChart(ctx, width, height, history, showAxes = true) {
ctx.clearRect(0, 0, width, height);
if (!history || history.length < 2) return;
const padLeft = showAxes ? 45 : 4;
const padRight = showAxes ? 20 : 4;
const padTop = showAxes ? 25 : 4;
const padBottom = showAxes ? 25 : 4;
const chartW = width - padLeft - padRight;
const chartH = height - padTop - padBottom;
// Determine max value for Y-scaling
let maxY = 10;
for (let i = 0; i < history.length; i++) {
const h = history[i];
if (h.sheep > maxY) maxY = h.sheep;
if (h.wolves > maxY) maxY = h.wolves;
if (h.plants > maxY) maxY = h.plants;
}
maxY = Math.ceil(maxY * 1.15);
// Background grid
if (showAxes) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
ctx.lineWidth = 1;
// Horizontal grid lines
for (let g = 0; g <= 4; g++) {
const yVal = Math.round((maxY / 4) * g);
const yPos = padTop + chartH - (g / 4) * chartH;
ctx.beginPath();
ctx.moveTo(padLeft, yPos);
ctx.lineTo(padLeft + chartW, yPos);
ctx.stroke();
ctx.fillStyle = 'rgba(255, 255, 255, 0.45)';
ctx.font = '10px monospace';
ctx.textAlign = 'right';
ctx.fillText(yVal.toString(), padLeft - 6, yPos + 3);
}
}
const mapX = (idx) => padLeft + (idx / (history.length - 1)) * chartW;
const mapY = (val) => padTop + chartH - (val / maxY) * chartH;
// Draw Line Series helper
const drawSeries = (key, color, lineWidth = 2) => {
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.beginPath();
for (let i = 0; i < history.length; i++) {
const x = mapX(i);
const y = mapY(history[i][key]);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
};
// Draw series: Plants (green), Sheep (sky blue/white), Wolves (coral red)
drawSeries('plants', '#38D39F', showAxes ? 2.5 : 1.5);
drawSeries('sheep', '#4CC9F0', showAxes ? 2.5 : 1.5);
drawSeries('wolves', '#FF4D6D', showAxes ? 2.5 : 1.5);
// Chart Legend on full chart
if (showAxes) {
ctx.font = '11px sans-serif';
ctx.textAlign = 'left';
// Sheep
ctx.fillStyle = '#4CC9F0';
ctx.fillRect(padLeft + 10, 10, 10, 10);
ctx.fillStyle = '#E0E0E0';
ctx.fillText('Sheep', padLeft + 25, 19);
// Wolves
ctx.fillStyle = '#FF4D6D';
ctx.fillRect(padLeft + 85, 10, 10, 10);
ctx.fillStyle = '#E0E0E0';
ctx.fillText('Wolves', padLeft + 100, 19);
// Plants
ctx.fillStyle = '#38D39F';
ctx.fillRect(padLeft + 165, 10, 10, 10);
ctx.fillStyle = '#E0E0E0';
ctx.fillText('Plants / Flowers', padLeft + 180, 19);
}
}
}