Files
grepo-remote/templates/attack_planner.html
2026-05-03 13:50:37 +03:00

483 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="el">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Attack Planner — Grepolis Remote</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0f0f1a;--surface:#181824;--border:#2a2a40;--text:#e0e0e0;--muted:#666;
--gold:#c8a44a;--red:#e05555;--green:#4acc64;--blue:#6fcfcf;--yellow:#f0c040;--orange:#e07830}
body{min-height:100vh;background:var(--bg);font-family:'Inter',sans-serif;color:var(--text);padding:1.5rem 2rem}
.page-header{display:flex;align-items:center;gap:1rem;margin-bottom:1.5rem;flex-wrap:wrap}
.page-header h1{font-size:1.7rem;font-weight:800;background:linear-gradient(135deg,#c8a44a,#f0c96e,#c8a44a);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;flex:1}
.back-link{color:var(--muted);text-decoration:none;font-size:.85rem;transition:color .2s}
.back-link:hover{color:var(--text)}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:start}
@media(max-width:900px){.grid{grid-template-columns:1fr}}
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:1.5rem;margin-bottom:1rem}
.card-title{font-size:1rem;font-weight:700;color:var(--gold);margin-bottom:1rem;padding-bottom:.75rem;border-bottom:1px solid var(--border)}
label{display:block;font-size:.8rem;color:var(--muted);margin-bottom:.3rem;margin-top:.75rem}
label:first-of-type{margin-top:0}
input[type=text],input[type=number],input[type=datetime-local],select{
width:100%;padding:9px 12px;background:#0f0f1a;border:1px solid var(--border);
border-radius:8px;color:var(--text);font-size:.875rem;font-family:inherit;transition:border-color .2s}
input:focus,select:focus{outline:none;border-color:var(--gold)}
select option{background:#181824}
.btn{padding:9px 18px;border:none;border-radius:8px;font-family:inherit;font-weight:600;
font-size:.85rem;cursor:pointer;transition:all .2s}
.btn-gold{background:var(--gold);color:#0f0f1a}.btn-gold:hover{background:#e0b85a}
.btn-red{background:rgba(224,85,85,.15);color:var(--red);border:1px solid rgba(224,85,85,.35)}
.btn-red:hover{background:rgba(224,85,85,.25)}
.btn-green{background:rgba(74,204,100,.15);color:var(--green);border:1px solid rgba(74,204,100,.35)}
.btn-green:hover{background:rgba(74,204,100,.25)}
.btn-sm{padding:5px 12px;font-size:.78rem}
.mt{margin-top:.75rem}
table{width:100%;border-collapse:collapse;font-size:.83rem}
th{padding:8px 12px;text-align:left;color:var(--muted);font-size:.72rem;text-transform:uppercase;
letter-spacing:.05em;border-bottom:1px solid var(--border)}
td{padding:9px 12px;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:middle}
tr:last-child td{border-bottom:none}
tr:hover td{background:rgba(255,255,255,.02)}
.badge{display:inline-block;padding:2px 8px;border-radius:6px;font-size:.72rem;font-weight:700}
.badge-draft{background:rgba(102,102,102,.2);color:#999;border:1px solid #444}
.badge-active{background:rgba(74,204,100,.15);color:var(--green);border:1px solid rgba(74,204,100,.35)}
.badge-completed{background:rgba(111,207,207,.15);color:var(--blue);border:1px solid rgba(111,207,207,.35)}
.badge-cancelled{background:rgba(224,85,85,.1);color:var(--red);border:1px solid rgba(224,85,85,.2)}
.badge-pending{background:rgba(240,192,64,.12);color:var(--yellow);border:1px solid rgba(240,192,64,.3)}
.badge-armed{background:rgba(224,120,48,.15);color:var(--orange);border:1px solid rgba(224,120,48,.35)}
.badge-sent{background:rgba(74,204,100,.15);color:var(--green);border:1px solid rgba(74,204,100,.35)}
.badge-missed{background:rgba(224,85,85,.15);color:var(--red);border:1px solid rgba(224,85,85,.35)}
.empty{text-align:center;padding:2rem;color:var(--muted);font-size:.875rem}
.error-box{background:rgba(224,85,85,.1);border:1px solid rgba(224,85,85,.3);color:var(--red);
padding:10px 14px;border-radius:8px;font-size:.83rem;margin-top:.75rem}
.info-box{background:rgba(111,207,207,.08);border:1px solid rgba(111,207,207,.25);color:var(--blue);
padding:10px 14px;border-radius:8px;font-size:.83rem;margin-top:.75rem}
.warn-box{background:rgba(240,192,64,.08);border:1px solid rgba(240,192,64,.25);color:var(--yellow);
padding:10px 14px;border-radius:8px;font-size:.83rem;margin-top:.5rem}
.town-meta{font-size:.75rem;color:var(--muted);margin-top:4px}
.section-sep{height:1px;background:var(--border);margin:1rem 0}
.plan-row{cursor:pointer}.plan-row:hover td{background:rgba(200,164,74,.05)}
.detail-panel{display:none}
.detail-panel.open{display:block}
.unit-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:.5rem;margin-top:.5rem}
.unit-input{display:flex;flex-direction:column;gap:3px}
.unit-input label{font-size:.72rem;color:var(--muted);margin:0}
.unit-input input{padding:6px 8px;font-size:.82rem}
.feasible-ok{color:var(--green);font-size:.78rem}
.feasible-err{color:var(--red);font-size:.78rem}
#msg{display:none;margin-top:.75rem}
</style>
</head>
<body>
<!-- Inject credentials for API calls from the web browser -->
<script>
window.__GRC_CLAN_KEY = "{{ clan_key }}";
window.PLAYER_ID = "{{ player_id }}";
window.WORLD_ID = "{{ world_id }}";
</script>
<div class="page-header">
<a href="/player/{{ player_id }}/{{ world_id }}" class="back-link">← Hub</a>
<h1>⚔️ Attack Planner — {{ world_id }}</h1>
</div>
<div class="grid">
<!-- LEFT: Plans list + detail -->
<div>
<div class="card">
<div class="card-title">📋 Πλάνα Επίθεσης — {{ world_id }}</div>
<div id="plans-list"><div class="empty">Φόρτωση...</div></div>
</div>
<div class="card detail-panel" id="detail-panel">
<div class="card-title" id="detail-title">Λεπτομέρειες Πλάνου</div>
<div id="detail-body"></div>
</div>
</div>
<!-- RIGHT: Create plan + add participant -->
<div>
<div class="card">
<div class="card-title"> Νέο Πλάνο</div>
<label>Όνομα Πλάνου</label>
<input type="text" id="plan-name" placeholder="π.χ. Επίθεση στον Leonidas">
<label>Κόσμος (World)</label>
<input type="text" id="plan-world" value="{{ world_id }}" readonly style="opacity:.6;cursor:not-allowed">
<label>Όνομα Στόχου</label>
<input type="text" id="target-name" placeholder="π.χ. Sparta Colony">
<label>Συντεταγμένες Στόχου X</label>
<input type="number" id="target-x" placeholder="π.χ. 503" min="0" max="999">
<label>Συντεταγμένες Στόχου Y</label>
<input type="number" id="target-y" placeholder="π.χ. 474" min="0" max="999">
<label>Ώρα Άφιξης (τοπική ώρα)</label>
<input type="datetime-local" id="arrival-time">
<div class="warn-box mt">
Η ώρα άφιξης πρέπει να είναι τουλάχιστον 2 λεπτά στο μέλλον.
</div>
<div class="mt">
<button class="btn btn-gold" onclick="createPlan()">Δημιουργία Πλάνου →</button>
</div>
<div id="msg"></div>
</div>
<!-- Add participant — only shown after a plan is selected -->
<div class="card" id="add-participant-card" style="display:none">
<div class="card-title">👤 Προσθήκη Πόλης στο Πλάνο</div>
<p style="font-size:.82rem;color:var(--muted);margin-bottom:.75rem">
Πλάνο: <strong id="selected-plan-name" style="color:var(--gold)"></strong>
</p>
<label>Επιλογή Πόλης ({{ world_id }})</label>
<select id="p-town-select" onchange="onTownSelected()">
<option value="">— Επίλεξε πόλη —</option>
</select>
<div class="town-meta" id="p-town-meta"></div>
<div class="section-sep"></div>
<div style="font-size:.85rem;font-weight:700;color:var(--gold);margin-bottom:.5rem">🗡 Μονάδες</div>
<div class="unit-grid" id="unit-inputs"></div>
<div class="mt">
<button class="btn btn-gold" onclick="addParticipant()">Υπολογισμός & Προσθήκη</button>
</div>
<div id="participant-result"></div>
</div>
</div>
</div>
<script>
(function() {
const PLAYER_ID = window.PLAYER_ID;
const WORLD_ID = window.WORLD_ID;
const CLAN_KEY = window.__GRC_CLAN_KEY;
let selectedPlanId = null;
// ---- Town data loaded from DB ----
let townData = []; // array of town objects with x, y, sea, town_id, town_name
// ---- Unit list ----
const UNITS = [
'swordsman','slinger','archer','hoplite','horseman',
'chariot','catapult',
'bireme','attack_ship','demolition_ship','transport_ship','colonize_ship'
];
// Build unit input grid
const grid = document.getElementById('unit-inputs');
UNITS.forEach(u => {
const div = document.createElement('div');
div.className = 'unit-input';
div.innerHTML = `<label>${u}</label><input type="number" id="unit_${u}" min="0" value="0">`;
grid.appendChild(div);
});
// ---- Load player's towns for this world ----
async function loadTowns() {
try {
const res = await fetch(`/dashboard/towns?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`);
const towns = await res.json();
townData = towns;
const sel = document.getElementById('p-town-select');
// Clear old options (keep first placeholder)
while (sel.options.length > 1) sel.remove(1);
if (!towns.length) {
sel.options[0].text = '— Δεν βρέθηκαν πόλεις (script offline?) —';
return;
}
towns.forEach(t => {
const opt = document.createElement('option');
opt.value = t.town_id;
opt.textContent = `${t.town_name} (${t.x}, ${t.y})`;
sel.appendChild(opt);
});
} catch(e) {
console.error('Failed to load towns:', e);
}
}
// ---- When a town is selected from dropdown ----
window.onTownSelected = function() {
const sel = document.getElementById('p-town-select');
const tid = sel.value;
const meta = document.getElementById('p-town-meta');
if (!tid) { meta.textContent = ''; return; }
const town = townData.find(t => String(t.town_id) === String(tid));
if (town) {
meta.innerHTML =
`🗺 X: <strong>${town.x}</strong> &nbsp; Y: <strong>${town.y}</strong> &nbsp; Sea: <strong>${town.sea}</strong> &nbsp; World: <strong>${town.world_id}</strong>`;
}
};
// ---- Helpers ----
function showMsg(el, text, isError) {
el.style.display = 'block';
el.className = isError ? 'error-box' : 'info-box';
el.textContent = text;
}
function statusBadge(s) {
return `<span class="badge badge-${s}">${s}</span>`;
}
function formatTs(unix) {
if (!unix) return '';
return new Date(unix * 1000).toLocaleString('el-GR');
}
function apiHeaders() {
return { 'Content-Type': 'application/json', 'X-Clan-Key': CLAN_KEY };
}
// ---- Load plans list ----
async function loadPlans() {
const res = await fetch(`/api/${WORLD_ID}/attack_plans`);
const data = await res.json();
const el = document.getElementById('plans-list');
if (!Array.isArray(data) || !data.length) {
el.innerHTML = '<div class="empty">Δεν υπάρχουν πλάνα ακόμη.</div>';
return;
}
let html = `<table>
<thead><tr>
<th>Πλάνο</th><th>Στόχος</th><th>Άφιξη</th><th>Status</th><th>Συμμ.</th><th></th>
</tr></thead><tbody>`;
for (const p of data) {
html += `<tr class="plan-row" onclick="selectPlan(${p.id},'${p.plan_name}')">
<td><strong>${p.plan_name}</strong></td>
<td>${p.target_town_name||''} ${p.target_x?`(${p.target_x},${p.target_y})`:''}
</td>
<td style="font-size:.78rem">${formatTs(p.target_arrival_time)}</td>
<td>${statusBadge(p.status)}</td>
<td>${p.participant_count}</td>
<td>
${p.status==='draft'
? `<button class="btn btn-green btn-sm" onclick="event.stopPropagation();armPlan(${p.id})">ARM</button>`
: ''}
<button class="btn btn-red btn-sm" style="margin-left:4px"
onclick="event.stopPropagation();cancelPlan(${p.id})">✕</button>
</td>
</tr>`;
}
html += '</tbody></table>';
el.innerHTML = html;
}
// ---- Select plan → show detail + participant panel ----
window.selectPlan = async function(planId, planName) {
selectedPlanId = planId;
document.getElementById('selected-plan-name').textContent = planName;
document.getElementById('add-participant-card').style.display = 'block';
await loadPlanDetail(planId);
};
async function loadPlanDetail(planId) {
const res = await fetch(`/api/${WORLD_ID}/attack_plans/${planId}`);
const plan = await res.json();
const panel = document.getElementById('detail-panel');
const body = document.getElementById('detail-body');
document.getElementById('detail-title').textContent =
`📌 ${plan.plan_name}${plan.target_town_name||'Άγνωστος Στόχος'} (${plan.target_x||'?'}, ${plan.target_y||'?'})`;
if (!plan.participants || !plan.participants.length) {
body.innerHTML = '<div class="empty">Χωρίς συμμετέχοντες ακόμη.</div>';
panel.classList.add('open');
return;
}
let html = `<table>
<thead><tr>
<th>Πόλη</th><th>Τύπος</th><th>Πλοία</th>
<th>Αποστολή</th><th>Επιστροφή</th><th>Status</th><th>OK</th><th></th>
</tr></thead><tbody>`;
for (const p of plan.participants) {
const f = p.is_feasible
? '<span class="feasible-ok">✅</span>'
: `<span class="feasible-err" title="${p.error_msg||''}">❌</span>`;
html += `<tr>
<td><strong>${p.origin_town_name||p.origin_town_id}</strong></td>
<td style="font-size:.75rem">${p.attack_type||''}</td>
<td>${p.transport_needed ? p.transport_count : ''}</td>
<td style="font-size:.75rem">${formatTs(p.send_time)}</td>
<td style="font-size:.75rem">${formatTs(p.return_time)}</td>
<td>${statusBadge(p.status)}</td>
<td>${f}</td>
<td>
<button class="btn btn-red btn-sm"
onclick="removeParticipant(${planId},'${p.origin_town_id}')">✕</button>
</td>
</tr>`;
}
html += '</tbody></table>';
const latest = plan.participants.reduce((m, p) => Math.max(m, p.return_time||0), 0);
if (latest) {
html += `<div class="info-box" style="margin-top:.75rem">
🏠 Τελευταία επιστροφή: <strong>${formatTs(latest)}</strong>
</div>`;
}
body.innerHTML = html;
panel.classList.add('open');
}
// ---- Create plan ----
window.createPlan = async function() {
const msg = document.getElementById('msg');
const name = document.getElementById('plan-name').value.trim();
const tName= document.getElementById('target-name').value.trim();
const tx = parseFloat(document.getElementById('target-x').value) || null;
const ty = parseFloat(document.getElementById('target-y').value) || null;
const dtLocal = document.getElementById('arrival-time').value;
if (!dtLocal) { showMsg(msg, 'Επίλεξε ώρα άφιξης', true); return; }
const arrivalUnix = Math.floor(new Date(dtLocal).getTime() / 1000);
const res = await fetch(`/api/${WORLD_ID}/attack_plans`, {
method: 'POST',
headers: apiHeaders(),
body: JSON.stringify({
player_id: PLAYER_ID,
plan_name: name || 'Επίθεση',
target_town_name: tName,
target_x: tx, target_y: ty,
target_arrival_time: arrivalUnix
})
});
const data = await res.json();
if (data.ok) {
showMsg(msg, `✅ Πλάνο δημιουργήθηκε (ID: ${data.plan_id})`, false);
loadPlans();
} else {
showMsg(msg, `${data.error}`, true);
}
};
// ---- Add participant (uses selected town from dropdown) ----
window.addParticipant = async function() {
if (!selectedPlanId) return;
const result = document.getElementById('participant-result');
const sel = document.getElementById('p-town-select');
const tid = sel.value;
if (!tid) {
result.innerHTML = '<div class="error-box">❌ Επίλεξε πόλη πρώτα</div>';
return;
}
const town = townData.find(t => String(t.town_id) === String(tid));
if (!town) {
result.innerHTML = '<div class="error-box">❌ Πόλη δεν βρέθηκε</div>';
return;
}
const units = {};
UNITS.forEach(u => {
const v = parseInt(document.getElementById(`unit_${u}`)?.value) || 0;
if (v > 0) units[u] = v;
});
const body = {
requester_player_id: PLAYER_ID,
player_id: PLAYER_ID,
origin_town_id: town.town_id,
origin_town_name: town.town_name,
origin_x: town.x,
origin_y: town.y,
origin_sea: town.sea,
units
};
const res = await fetch(`/api/${WORLD_ID}/attack_plans/${selectedPlanId}/participants`, {
method: 'POST',
headers: apiHeaders(),
body: JSON.stringify(body)
});
const data = await res.json();
if (data.is_feasible !== undefined) {
if (data.is_feasible) {
result.innerHTML = `<div class="info-box">
✅ Feasible — Χρόνος: ${Math.floor(data.travel_time_secs/60)}m
&nbsp;|&nbsp; Πλοία: ${data.transport_count||0}
&nbsp;|&nbsp; Αποστολή: ${new Date(data.send_time*1000).toLocaleString('el-GR')}
</div>`;
} else {
result.innerHTML = `<div class="error-box">❌ ${data.error_msg}</div>`;
}
loadPlanDetail(selectedPlanId);
} else {
result.innerHTML = `<div class="error-box">❌ ${data.error||'Unknown error'}</div>`;
}
};
// ---- Remove participant ----
window.removeParticipant = async function(planId, townId) {
if (!confirm('Αφαίρεση συμμετέχοντα;')) return;
await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/participants/${townId}`, {
method: 'DELETE',
headers: apiHeaders(),
body: JSON.stringify({ requester_player_id: PLAYER_ID })
});
loadPlanDetail(planId);
loadPlans();
};
// ---- Arm plan ----
window.armPlan = async function(planId) {
const res = await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/arm`, {
method: 'POST',
headers: apiHeaders(),
body: JSON.stringify({ player_id: PLAYER_ID })
});
const data = await res.json();
alert(data.ok ? '✅ Πλάνο ενεργοποιήθηκε!' : `${data.error}`);
loadPlans();
if (selectedPlanId === planId) loadPlanDetail(planId);
};
// ---- Cancel plan ----
window.cancelPlan = async function(planId) {
if (!confirm('Ακύρωση πλάνου;')) return;
await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/cancel`, {
method: 'POST',
headers: apiHeaders(),
body: JSON.stringify({ player_id: PLAYER_ID })
});
loadPlans();
if (selectedPlanId === planId) {
document.getElementById('detail-panel').classList.remove('open');
document.getElementById('add-participant-card').style.display = 'none';
}
};
// ---- Init ----
loadPlans();
loadTowns();
setInterval(loadPlans, 15000);
setInterval(() => { if (selectedPlanId) loadPlanDetail(selectedPlanId); }, 15000);
})();
</script>
</body>
</html>