fix planner

This commit is contained in:
2026-05-03 14:33:09 +03:00
parent cf23f38a6e
commit 51485c0048
2 changed files with 449 additions and 425 deletions

View File

@@ -291,6 +291,50 @@ def get_towns():
return jsonify(towns) return jsonify(towns)
# ------------------------------------------------------------------
# GET /dashboard/clan-towns?world_id=...
# Returns all towns for ALL clan members in a given world.
# Used by attack planner to let admin pick any player's town.
# ------------------------------------------------------------------
@dashboard.route('/dashboard/clan-towns', methods=['GET'])
@login_required
def get_clan_towns():
world_id = request.args.get('world_id', '').strip()
clan_id = current_user.clan_id
if not clan_id or not world_id:
return jsonify([])
conn = get_db()
rows = conn.execute('''
SELECT ts.town_id, ts.town_name, ts.player, ts.player_id,
ts.x, ts.y, ts.sea, ts.world_id, ts.updated_at, ts.data
FROM town_state ts
INNER JOIN clan_members cm
ON cm.player_id = ts.player_id
AND cm.world_id = ts.world_id
AND cm.clan_id = ?
WHERE ts.world_id = ?
ORDER BY ts.player ASC, ts.town_name ASC
''', (clan_id, world_id)).fetchall()
conn.close()
towns = []
for r in rows:
d = json.loads(r['data']) if r['data'] else {}
towns.append({
'town_id': r['town_id'],
'town_name': r['town_name'],
'player': r['player'],
'player_id': r['player_id'],
'world_id': r['world_id'],
'x': r['x'],
'y': r['y'],
'sea': r['sea'],
'units': d.get('units', {}),
})
return jsonify(towns)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# POST /dashboard/blueprints # POST /dashboard/blueprints
# Toggle a blueprint for a specific town # Toggle a blueprint for a specific town

View File

@@ -7,153 +7,108 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style> <style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0} *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0f0f1a;--surface:#181824;--border:#2a2a40;--text:#e0e0e0;--muted:#666; :root{--bg:#0f0f1a;--surf:#181824;--border:#2a2a40;--text:#e0e0e0;--muted:#666;
--gold:#c8a44a;--red:#e05555;--green:#4acc64;--blue:#6fcfcf;--yellow:#f0c040;--orange:#e07830} --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} 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} .topbar{display:flex;align-items:center;gap:1rem;margin-bottom:1.5rem}
.page-header h1{font-size:1.7rem;font-weight:800;background:linear-gradient(135deg,#c8a44a,#f0c96e,#c8a44a); .topbar h1{font-size:1.6rem;font-weight:800;background:linear-gradient(135deg,#c8a44a,#f0c96e);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;flex:1} -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{color:var(--muted);text-decoration:none;font-size:.85rem}
.back-link:hover{color:var(--text)} .back:hover{color:var(--text)}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:start} .layout{display:grid;grid-template-columns:340px 1fr;gap:1.5rem;align-items:start}
@media(max-width:900px){.grid{grid-template-columns:1fr}} @media(max-width:900px){.layout{grid-template-columns:1fr}}
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:1.5rem;margin-bottom:1rem} .card{background:var(--surf);border:1px solid var(--border);border-radius:14px;padding:1.25rem;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)} .ct{font-size:.9rem;font-weight:700;color:var(--gold);margin-bottom:.85rem;padding-bottom:.6rem;border-bottom:1px solid var(--border)}
label{display:block;font-size:.8rem;color:var(--muted);margin-bottom:.3rem;margin-top:.75rem} label{display:block;font-size:.78rem;color:var(--muted);margin-bottom:.25rem;margin-top:.65rem}
label:first-of-type{margin-top:0} label:first-of-type{margin-top:0}
input[type=text],input[type=number],input[type=datetime-local],select{ input,select{width:100%;padding:8px 11px;background:#0f0f1a;border:1px solid var(--border);
width:100%;padding:9px 12px;background:#0f0f1a;border:1px solid var(--border); border-radius:7px;color:var(--text);font-size:.85rem;font-family:inherit}
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)} input:focus,select:focus{outline:none;border-color:var(--gold)}
select option{background:#181824} select option{background:#181824}
.btn{padding:9px 18px;border:none;border-radius:8px;font-family:inherit;font-weight:600; .btn{padding:8px 16px;border:none;border-radius:7px;font-family:inherit;font-weight:600;font-size:.82rem;cursor:pointer;transition:all .2s}
font-size:.85rem;cursor:pointer;transition:all .2s}
.btn-gold{background:var(--gold);color:#0f0f1a}.btn-gold:hover{background:#e0b85a} .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{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-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-green{background:rgba(74,204,100,.15);color:var(--green);border:1px solid rgba(74,204,100,.35)} .btn-blue{background:rgba(111,207,207,.12);color:var(--blue);border:1px solid rgba(111,207,207,.3)}.btn-blue:hover{background:rgba(111,207,207,.22)}
.btn-green:hover{background:rgba(74,204,100,.25)} .btn-sm{padding:4px 11px;font-size:.75rem}
.btn-sm{padding:5px 12px;font-size:.78rem} .mt{margin-top:.65rem}
.mt{margin-top:.75rem} .sep{height:1px;background:var(--border);margin:.85rem 0}
table{width:100%;border-collapse:collapse;font-size:.83rem} table{width:100%;border-collapse:collapse;font-size:.8rem}
th{padding:8px 12px;text-align:left;color:var(--muted);font-size:.72rem;text-transform:uppercase; th{padding:7px 10px;text-align:left;color:var(--muted);font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--border)}
letter-spacing:.05em;border-bottom:1px solid var(--border)} td{padding:8px 10px;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:middle}
td{padding:9px 12px;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:middle}
tr:last-child td{border-bottom:none} tr:last-child td{border-bottom:none}
tr:hover td{background:rgba(255,255,255,.02)} .plan-row{cursor:pointer}.plan-row:hover td{background:rgba(200,164,74,.04)}
.badge{display:inline-block;padding:2px 8px;border-radius:6px;font-size:.72rem;font-weight:700} .plan-row.selected td{background:rgba(200,164,74,.08)}
.badge-draft{background:rgba(102,102,102,.2);color:#999;border:1px solid #444} .badge{display:inline-block;padding:2px 7px;border-radius:5px;font-size:.7rem;font-weight:700}
.badge-active{background:rgba(74,204,100,.15);color:var(--green);border:1px solid rgba(74,204,100,.35)} .badge-draft{background:rgba(102,102,102,.2);color:#888;border:1px solid #444}
.badge-completed{background:rgba(111,207,207,.15);color:var(--blue);border:1px solid rgba(111,207,207,.35)} .badge-active{background:rgba(74,204,100,.15);color:var(--green);border:1px solid rgba(74,204,100,.3)}
.badge-cancelled{background:rgba(224,85,85,.1);color:var(--red);border:1px solid rgba(224,85,85,.2)} .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-completed{background:rgba(111,207,207,.12);color:var(--blue);border:1px solid rgba(111,207,207,.3)}
.badge-armed{background:rgba(224,120,48,.15);color:var(--orange);border:1px solid rgba(224,120,48,.35)} .badge-pending{background:rgba(240,192,64,.1);color:var(--yellow);border:1px solid rgba(240,192,64,.3)}
.badge-sent{background:rgba(74,204,100,.15);color:var(--green);border:1px solid rgba(74,204,100,.35)} .badge-armed{background:rgba(224,120,48,.12);color:var(--orange);border:1px solid rgba(224,120,48,.3)}
.badge-missed{background:rgba(224,85,85,.15);color:var(--red);border:1px solid rgba(224,85,85,.35)} .badge-sent{background:rgba(74,204,100,.15);color:var(--green);border:1px solid rgba(74,204,100,.3)}
.empty{text-align:center;padding:2rem;color:var(--muted);font-size:.875rem} .badge-missed{background:rgba(224,85,85,.15);color:var(--red);border:1px solid rgba(224,85,85,.3)}
.error-box{background:rgba(224,85,85,.1);border:1px solid rgba(224,85,85,.3);color:var(--red); .empty{text-align:center;padding:1.5rem;color:var(--muted);font-size:.85rem}
padding:10px 14px;border-radius:8px;font-size:.83rem;margin-top:.75rem} .err{background:rgba(224,85,85,.1);border:1px solid rgba(224,85,85,.3);color:var(--red);padding:9px 13px;border-radius:7px;font-size:.8rem;margin-top:.65rem}
.info-box{background:rgba(111,207,207,.08);border:1px solid rgba(111,207,207,.25);color:var(--blue); .ok{background:rgba(111,207,207,.08);border:1px solid rgba(111,207,207,.25);color:var(--blue);padding:9px 13px;border-radius:7px;font-size:.8rem;margin-top:.65rem}
padding:10px 14px;border-radius:8px;font-size:.83rem;margin-top:.75rem} .warn{background:rgba(240,192,64,.07);border:1px solid rgba(240,192,64,.25);color:var(--yellow);padding:9px 13px;border-radius:7px;font-size:.8rem;margin-top:.5rem}
.warn-box{background:rgba(240,192,64,.08);border:1px solid rgba(240,192,64,.25);color:var(--yellow); .unit-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(115px,1fr));gap:.4rem;margin-top:.5rem}
padding:10px 14px;border-radius:8px;font-size:.83rem;margin-top:.5rem} .ui{display:flex;flex-direction:column;gap:2px}
.town-meta{font-size:.75rem;color:var(--muted);margin-top:4px} .ui label{font-size:.7rem;margin:0}
.section-sep{height:1px;background:var(--border);margin:1rem 0} .ui input{padding:5px 7px;font-size:.8rem}
.plan-row{cursor:pointer}.plan-row:hover td{background:rgba(200,164,74,.05)} .town-meta{font-size:.73rem;color:var(--muted);margin-top:3px}
.detail-panel{display:none} .plan-header{display:flex;gap:.5rem;align-items:flex-start;flex-wrap:wrap;margin-bottom:.85rem}
.detail-panel.open{display:block} .plan-stat{background:#0f0f1a;border:1px solid var(--border);border-radius:8px;padding:8px 13px;font-size:.78rem;flex:1;min-width:120px}
.unit-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:.5rem;margin-top:.5rem} .plan-stat strong{display:block;font-size:1rem;color:var(--gold);font-weight:700}
.unit-input{display:flex;flex-direction:column;gap:3px} .cd{font-family:monospace;font-weight:700;color:var(--yellow)}
.unit-input label{font-size:.72rem;color:var(--muted);margin:0} .fok{color:var(--green)}.ferr{color:var(--red)}
.unit-input input{padding:6px 8px;font-size:.82rem} .section-label{font-size:.78rem;font-weight:700;color:var(--gold);margin-bottom:.4rem}
.feasible-ok{color:var(--green);font-size:.78rem} #msg{display:none;margin-top:.65rem}
.feasible-err{color:var(--red);font-size:.78rem}
#msg{display:none;margin-top:.75rem}
</style> </style>
</head> </head>
<body> <body>
<!-- Inject credentials for API calls from the web browser -->
<script> <script>
window.__GRC_CLAN_KEY = "{{ clan_key }}"; window.__GRC_CLAN_KEY = "{{ clan_key }}";
window.PLAYER_ID = "{{ player_id }}"; window.PLAYER_ID = "{{ player_id }}";
window.WORLD_ID = "{{ world_id }}"; window.WORLD_ID = "{{ world_id }}";
</script> </script>
<div class="page-header"> <div class="topbar">
<a href="/player/{{ player_id }}/{{ world_id }}" class="back-link">← Hub</a> <a href="/player/{{ player_id }}/{{ world_id }}" class="back">← Hub</a>
<h1>⚔️ Attack Planner — {{ world_id }}</h1> <h1>⚔️ Attack Planner — {{ world_id }}</h1>
</div> </div>
<div class="grid"> <div class="layout">
<!-- LEFT: Plans list + detail --> <!-- ═══════════════ LEFT: Plan list + Create ═══════════════ -->
<div> <div>
<div class="card"> <div class="card">
<div class="card-title">📋 Πλάνα Επίθεσης — {{ world_id }}</div> <div class="ct"> Νέο Πλάνο</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> <label>Όνομα Πλάνου</label>
<input type="text" id="plan-name" placeholder="π.χ. Επίθεση στον Leonidas"> <input id="p-name" placeholder="π.χ. Επίθεση στον Leonidas">
<label>Στόχος (όνομα)</label>
<label>Κόσμος (World)</label> <input id="p-tname" placeholder="π.χ. Athens Colony">
<input type="text" id="plan-world" value="{{ world_id }}" readonly style="opacity:.6;cursor:not-allowed"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem">
<div><label>Στόχος X</label><input id="p-tx" type="number" min="0" max="999"></div>
<label>Όνομα Στόχου</label> <div><label>Στόχος Y</label><input id="p-ty" type="number" min="0" max="999"></div>
<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>
<label>Ώρα Άφιξης (τοπική)</label>
<input id="p-arr" type="datetime-local">
<div class="warn">⏱ Τουλάχιστον 2 λεπτά στο μέλλον</div>
<div class="mt"><button class="btn btn-gold" onclick="createPlan()">Δημιουργία →</button></div>
<div id="msg"></div> <div id="msg"></div>
</div> </div>
<!-- Add participant — only shown after a plan is selected --> <div class="card">
<div class="card" id="add-participant-card" style="display:none"> <div class="ct">📋 Πλάνα — {{ world_id }}</div>
<div class="card-title">👤 Προσθήκη Πόλης στο Πλάνο</div> <div id="plans-list"><div class="empty">Φόρτωση…</div></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>
<div id="participant-result"></div> </div>
<!-- ═══════════════ RIGHT: Plan detail + Add participant ═══════════════ -->
<div id="right-panel">
<div class="card" style="border-color:#2a3060">
<div class="empty" style="color:#444">← Επίλεξε πλάνο για λεπτομέρειες</div>
</div> </div>
</div> </div>
@@ -161,244 +116,264 @@
<script> <script>
(function(){ (function(){
const PLAYER_ID = window.PLAYER_ID; const PID = window.PLAYER_ID;
const WORLD_ID = window.WORLD_ID; const WID = window.WORLD_ID;
const CLAN_KEY = window.__GRC_CLAN_KEY; const KEY = window.__GRC_CLAN_KEY;
let selectedPlanId = null; let selPlan = null;
let allTowns = []; // all clan towns for this world
// ---- Town data loaded from DB ---- function hdrs(){ return {'Content-Type':'application/json','X-Clan-Key':KEY}; }
let townData = []; // array of town objects with x, y, sea, town_id, town_name function ts(u){ return u ? new Date(u*1000).toLocaleString('el-GR') : ''; }
function badge(s){ return `<span class="badge badge-${s}">${s}</span>`; }
function showMsg(el,txt,err){el.style.display='block';el.className=err?'err':'ok';el.textContent=txt;}
// ---- Unit list ---- // ─── unit groups ───
const UNITS = [ const LAND = ['swordsman','slinger','archer','hoplite','horseman','chariot','catapult'];
'swordsman','slinger','archer','hoplite','horseman', const NAVAL = ['bireme','attack_ship','demolition_ship','transport_ship','colonize_ship'];
'chariot','catapult',
'bireme','attack_ship','demolition_ship','transport_ship','colonize_ship'
];
// Build unit input grid // ─── Load all clan towns for this world ───
const grid = document.getElementById('unit-inputs'); async function loadClanTowns(){
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{ try{
const res = await fetch(`/dashboard/towns?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`); const r = await fetch(`/dashboard/clan-towns?world_id=${WID}`);
const towns = await res.json(); allTowns = await r.json();
townData = towns; }catch(e){ allTowns=[]; }
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 => { // ─── Load plans list ───
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(){ async function loadPlans(){
const res = await fetch(`/api/${WORLD_ID}/attack_plans`); const r = await fetch(`/api/${WID}/attack_plans`);
const data = await res.json(); const data = await r.json();
const el = document.getElementById('plans-list'); const el = document.getElementById('plans-list');
if(!Array.isArray(data)||!data.length){ if(!Array.isArray(data)||!data.length){
el.innerHTML = '<div class="empty">Δεν υπάρχουν πλάνα ακόμη.</div>'; el.innerHTML='<div class="empty">Δεν υπάρχουν πλάνα.</div>'; return;
return;
} }
let h=`<table><thead><tr><th>Πλάνο</th><th>Στόχος</th><th>Άφιξη</th><th>Status</th><th>Συμμ.</th></tr></thead><tbody>`;
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){ for(const p of data){
html += `<tr class="plan-row" onclick="selectPlan(${p.id},'${p.plan_name}')"> const sel = selPlan===p.id ? ' selected':'';
h+=`<tr class="plan-row${sel}" onclick="selectPlan(${p.id})">
<td><strong>${p.plan_name}</strong></td> <td><strong>${p.plan_name}</strong></td>
<td>${p.target_town_name||''} ${p.target_x?`(${p.target_x},${p.target_y})`:''} <td style="font-size:.75rem">${p.target_town_name||''} ${p.target_x?`(${p.target_x},${p.target_y})`:''}</td>
</td> <td style="font-size:.72rem">${ts(p.target_arrival_time)}</td>
<td style="font-size:.78rem">${formatTs(p.target_arrival_time)}</td> <td>${badge(p.status)}</td>
<td>${statusBadge(p.status)}</td> <td style="text-align:center">${p.participant_count}</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>`; </tr>`;
} }
html += '</tbody></table>'; h+='</tbody></table>';
el.innerHTML = html; el.innerHTML=h;
} }
// ---- Select plan → show detail + participant panel ---- // ─── Select plan ───
window.selectPlan = async function(planId, planName) { window.selectPlan = async function(id){
selectedPlanId = planId; selPlan = id;
document.getElementById('selected-plan-name').textContent = planName; await loadPlans();
document.getElementById('add-participant-card').style.display = 'block'; await renderPlanDetail(id);
await loadPlanDetail(planId);
}; };
async function loadPlanDetail(planId) { async function renderPlanDetail(id){
const res = await fetch(`/api/${WORLD_ID}/attack_plans/${planId}`); const r = await fetch(`/api/${WID}/attack_plans/${id}`);
const plan = await res.json(); const plan = await r.json();
const panel = document.getElementById('detail-panel'); if(plan.error){ return; }
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) { const parts = plan.participants||[];
body.innerHTML = '<div class="empty">Χωρίς συμμετέχοντες ακόμη.</div>'; const isDraft = plan.status==='draft';
panel.classList.add('open');
return; // Header stats
let hdr = `
<div class="plan-stat"><span style="color:var(--muted);font-size:.7rem">ΣΤΟΧΟΣ</span>
<strong>${plan.target_town_name||''}</strong>
<span style="font-size:.75rem;color:var(--muted)">(${plan.target_x||'?'}, ${plan.target_y||'?'})</span>
</div>
<div class="plan-stat"><span style="color:var(--muted);font-size:.7rem">ΑΦΙΞΗ</span>
<strong style="font-size:.85rem">${ts(plan.target_arrival_time)}</strong>
</div>
<div class="plan-stat"><span style="color:var(--muted);font-size:.7rem">STATUS</span>
<strong>${badge(plan.status)}</strong>
</div>
<div class="plan-stat"><span style="color:var(--muted);font-size:.7rem">ΣΥΜΜΕΤΕΧΟΝΤΕΣ</span>
<strong>${parts.length}</strong>
</div>`;
// Action buttons
let btns = '';
if(isDraft){
btns=`<button class="btn btn-green btn-sm" onclick="armPlan(${id})">▶ ARM</button>`;
} }
btns+=` <button class="btn btn-red btn-sm" onclick="cancelPlan(${id})">✕ Ακύρωση</button>`;
let html = `<table> // Participants table
<thead><tr> let ptable = '<div class="empty">Χωρίς συμμετέχοντες ακόμη.</div>';
<th>Πόλη</th><th>Τύπος</th><th>Πλοία</th> if(parts.length){
<th>Αποστολή</th><th>Επιστροφή</th><th>Status</th><th>OK</th><th></th> let latest = 0;
</tr></thead><tbody>`; let ptrows = '';
for (const p of plan.participants) { for(const p of parts){
const f = p.is_feasible if(p.return_time>latest) latest=p.return_time;
? '<span class="feasible-ok">✅</span>' const units = p.units ? Object.entries(p.units).filter(([,v])=>v>0).map(([k,v])=>`${k}:${v}`).join(', ') : '';
: `<span class="feasible-err" title="${p.error_msg||''}">❌</span>`; ptrows+=`<tr>
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> <td>
<button class="btn btn-red btn-sm" <div style="font-weight:600">${p.origin_town_name||p.origin_town_id}</div>
onclick="removeParticipant(${planId},'${p.origin_town_id}')">✕</button> <div style="font-size:.72rem;color:var(--muted)">${p.player_name||p.player_id||''}</div>
</td> </td>
<td style="font-size:.72rem;color:var(--blue)">${units||''}</td>
<td>${p.transport_needed?`🚢 ${p.transport_count}`:''}</td>
<td style="font-size:.72rem">${ts(p.send_time)}</td>
<td style="font-size:.72rem">${ts(p.return_time)}</td>
<td>${badge(p.status)}</td>
<td>${p.is_feasible?'<span class="fok">✅</span>':`<span class="ferr" title="${p.error_msg||''}">❌</span>`}</td>
<td><button class="btn btn-red btn-sm" onclick="removeParticipant(${id},'${p.origin_town_id}')">✕</button></td>
</tr>`; </tr>`;
} }
html += '</tbody></table>'; ptable=`
<table>
const latest = plan.participants.reduce((m, p) => Math.max(m, p.return_time||0), 0); <thead><tr>
<th>Πόλη / Παίκτης</th><th>Στρατός</th><th>Πλοία</th>
<th>Αποστολή</th><th>Επιστροφή</th><th>Status</th><th>OK</th><th></th>
</tr></thead>
<tbody>${ptrows}</tbody>
</table>`;
if(latest){ if(latest){
html += `<div class="info-box" style="margin-top:.75rem"> ptable+=`<div class="ok" style="margin-top:.65rem">🏠 Τελευταία επιστροφή: <strong>${ts(latest)}</strong></div>`;
🏠 Τελευταία επιστροφή: <strong>${formatTs(latest)}</strong> }
}
// Add participant form
const addForm = buildAddForm(id, isDraft);
document.getElementById('right-panel').innerHTML=`
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.85rem">
<div class="ct" style="margin:0;border:none;padding:0">📌 ${plan.plan_name}</div>
<div style="display:flex;gap:.4rem">${btns}</div>
</div>
<div class="plan-header">${hdr}</div>
<div class="sep"></div>
<div class="section-label">👥 Συμμετέχοντες</div>
${ptable}
</div>
${isDraft ? addForm : ''}
`;
// Populate player dropdown
if(isDraft) populatePlayerDropdown();
}
// ─── Build "Add Participant" form ───
function buildAddForm(planId, isDraft){
if(!isDraft) return '';
// Build unit inputs (land + naval groups)
let landInputs = LAND.map(u=>`
<div class="ui"><label>${u}</label>
<input type="number" id="u_${u}" min="0" value="0"></div>`).join('');
let navalInputs = NAVAL.map(u=>`
<div class="ui"><label>${u}</label>
<input type="number" id="u_${u}" min="0" value="0"></div>`).join('');
return `
<div class="card" id="add-form">
<div class="ct">👤 Προσθήκη Συμμετέχοντα</div>
<label>Παίκτης</label>
<select id="ap-player" onchange="onPlayerChange()">
<option value="">— Επίλεξε παίκτη —</option>
</select>
<label>Πόλη</label>
<select id="ap-town" onchange="onTownChange()">
<option value="">— Επίλεξε πόλη —</option>
</select>
<div class="town-meta" id="ap-meta"></div>
<div class="sep"></div>
<div class="section-label">🗡 Χερσαίος Στρατός</div>
<div class="unit-grid">${landInputs}</div>
<div class="section-label" style="margin-top:.75rem">⚓ Ναυτικός Στρατός</div>
<div class="unit-grid">${navalInputs}</div>
<div class="warn" style="margin-top:.65rem">
💡 Αφήσε 0 σε μονάδες που δεν συμμετέχουν. Τα πλοία μεταφοράς υπολογίζονται αυτόματα.
</div>
<div class="mt" style="display:flex;gap:.5rem">
<button class="btn btn-gold" onclick="addParticipant(${planId})">Υπολογισμός & Προσθήκη</button>
<button class="btn btn-blue" onclick="fillFromGame()">📥 Φόρτωση από game</button>
</div>
<div id="ap-result"></div>
</div>`; </div>`;
} }
body.innerHTML = html; // ─── Populate player dropdown from allTowns ───
panel.classList.add('open'); function populatePlayerDropdown(){
const sel = document.getElementById('ap-player');
if(!sel) return;
const seen = {};
allTowns.forEach(t=>{ seen[t.player_id]=t.player; });
while(sel.options.length>1) sel.remove(1);
for(const [pid,pname] of Object.entries(seen)){
const opt=document.createElement('option');
opt.value=pid; opt.textContent=pname||pid;
sel.appendChild(opt);
}
} }
// ---- Create plan ---- // ─── Player change → filter town dropdown ───
window.createPlan = async function() { window.onPlayerChange = function(){
const msg = document.getElementById('msg'); const pid = document.getElementById('ap-player').value;
const name = document.getElementById('plan-name').value.trim(); const tsel = document.getElementById('ap-town');
const tName= document.getElementById('target-name').value.trim(); while(tsel.options.length>1) tsel.remove(1);
const tx = parseFloat(document.getElementById('target-x').value) || null; document.getElementById('ap-meta').textContent='';
const ty = parseFloat(document.getElementById('target-y').value) || null; if(!pid) return;
const dtLocal = document.getElementById('arrival-time').value; allTowns.filter(t=>t.player_id===pid).forEach(t=>{
const opt=document.createElement('option');
if (!dtLocal) { showMsg(msg, 'Επίλεξε ώρα άφιξης', true); return; } opt.value=t.town_id;
const arrivalUnix = Math.floor(new Date(dtLocal).getTime() / 1000); opt.textContent=`${t.town_name} (${t.x}, ${t.y})`;
tsel.appendChild(opt);
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) ---- // ─── Town change → show meta + fill units from game data ───
window.addParticipant = async function() { window.onTownChange = function(){
if (!selectedPlanId) return; const tid = document.getElementById('ap-town').value;
const result = document.getElementById('participant-result'); const meta = document.getElementById('ap-meta');
if(!tid){ meta.textContent=''; return; }
const town = allTowns.find(t=>String(t.town_id)===String(tid));
if(!town){ return; }
meta.innerHTML=`🗺 X:<strong>${town.x}</strong> Y:<strong>${town.y}</strong> Sea:<strong>${town.sea}</strong>`;
};
const sel = document.getElementById('p-town-select'); // ─── Fill units from game data (if available) ───
const tid = sel.value; window.fillFromGame = function(){
if (!tid) { const tid = document.getElementById('ap-town').value;
result.innerHTML = '<div class="error-box">❌ Επίλεξε πόλη πρώτα</div>'; if(!tid){ return; }
return; const town = allTowns.find(t=>String(t.town_id)===String(tid));
} if(!town||!town.units) return;
const all=[...LAND,...NAVAL];
all.forEach(u=>{
const el=document.getElementById(`u_${u}`);
if(el && town.units[u]!==undefined) el.value=town.units[u]||0;
});
};
const town = townData.find(t => String(t.town_id) === String(tid)); // ─── Add participant ───
if (!town) { window.addParticipant = async function(planId){
result.innerHTML = '<div class="error-box">❌ Πόλη δεν βρέθηκε</div>'; const result=document.getElementById('ap-result');
return; const tid = document.getElementById('ap-town').value;
} if(!tid){ result.innerHTML='<div class="err">❌ Επίλεξε πόλη</div>'; return; }
const town = allTowns.find(t=>String(t.town_id)===String(tid));
if(!town){ result.innerHTML='<div class="err">❌ Πόλη δεν βρέθηκε</div>'; return; }
const units={}; const units={};
UNITS.forEach(u => { [...LAND,...NAVAL].forEach(u=>{
const v = parseInt(document.getElementById(`unit_${u}`)?.value) || 0; const v=parseInt(document.getElementById(`u_${u}`)?.value)||0;
if(v>0) units[u]=v; if(v>0) units[u]=v;
}); });
if(!Object.keys(units).length){ result.innerHTML='<div class="err">❌ Πρόσθεσε τουλάχιστον 1 μονάδα</div>'; return; }
const body={ const body={
requester_player_id: PLAYER_ID, requester_player_id: PID,
player_id: PLAYER_ID, player_id: town.player_id,
player_name: town.player,
origin_town_id: town.town_id, origin_town_id: town.town_id,
origin_town_name: town.town_name, origin_town_name: town.town_name,
origin_x: town.x, origin_x: town.x,
@@ -407,75 +382,80 @@
units units
}; };
const res = await fetch(`/api/${WORLD_ID}/attack_plans/${selectedPlanId}/participants`, { const r = await fetch(`/api/${WID}/attack_plans/${planId}/participants`,
method: 'POST', {method:'POST', headers:hdrs(), body:JSON.stringify(body)});
headers: apiHeaders(), const data = await r.json();
body: JSON.stringify(body)
});
const data = await res.json();
if(data.is_feasible!==undefined){ if(data.is_feasible!==undefined){
if(data.is_feasible){ if(data.is_feasible){
result.innerHTML = `<div class="info-box"> result.innerHTML=`<div class="ok">
✅ Feasible — Χρόνος: ${Math.floor(data.travel_time_secs/60)}m ✅ Feasible — Χρόνος: ${Math.floor(data.travel_time_secs/60)}λεπτά
&nbsp;|&nbsp; Πλοία: ${data.transport_count||0} &nbsp;|&nbsp; Πλοία: ${data.transport_count||0}
&nbsp;|&nbsp; Αποστολή: ${new Date(data.send_time*1000).toLocaleString('el-GR')} &nbsp;|&nbsp; Αποστολή: ${ts(data.send_time)}
&nbsp;|&nbsp; Επιστροφή: ${ts(data.return_time)}
</div>`; </div>`;
} else { } else {
result.innerHTML = `<div class="error-box">❌ ${data.error_msg}</div>`; result.innerHTML=`<div class="err">❌ ${data.error_msg}</div>`;
} }
loadPlanDetail(selectedPlanId); renderPlanDetail(planId);
} else { } else {
result.innerHTML = `<div class="error-box">❌ ${data.error||'Unknown error'}</div>`; result.innerHTML=`<div class="err">❌ ${data.error||'Σφάλμα'}</div>`;
} }
}; };
// ---- Remove participant ---- // ─── Remove participant ───
window.removeParticipant = async function(planId,townId){ window.removeParticipant = async function(planId,townId){
if (!confirm('Αφαίρεση συμμετέχοντα;')) return; if(!confirm('Αφαίρεση;')) return;
await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/participants/${townId}`, { await fetch(`/api/${WID}/attack_plans/${planId}/participants/${townId}`,
method: 'DELETE', {method:'DELETE', headers:hdrs(), body:JSON.stringify({requester_player_id:PID})});
headers: apiHeaders(), renderPlanDetail(planId); loadPlans();
body: JSON.stringify({ requester_player_id: PLAYER_ID })
});
loadPlanDetail(planId);
loadPlans();
}; };
// ---- Arm plan ---- // ─── Arm plan ───
window.armPlan = async function(planId) { window.armPlan = async function(id){
const res = await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/arm`, { const r=await fetch(`/api/${WID}/attack_plans/${id}/arm`,
method: 'POST', {method:'POST', headers:hdrs(), body:JSON.stringify({player_id:PID})});
headers: apiHeaders(), const d=await r.json();
body: JSON.stringify({ player_id: PLAYER_ID }) alert(d.ok?'✅ Πλάνο ενεργοποιήθηκε!':'❌ '+d.error);
}); renderPlanDetail(id); loadPlans();
const data = await res.json();
alert(data.ok ? '✅ Πλάνο ενεργοποιήθηκε!' : `${data.error}`);
loadPlans();
if (selectedPlanId === planId) loadPlanDetail(planId);
}; };
// ---- Cancel plan ---- // ─── Cancel plan ───
window.cancelPlan = async function(planId) { window.cancelPlan = async function(id){
if(!confirm('Ακύρωση πλάνου;')) return; if(!confirm('Ακύρωση πλάνου;')) return;
await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/cancel`, { await fetch(`/api/${WID}/attack_plans/${id}/cancel`,
method: 'POST', {method:'POST', headers:hdrs(), body:JSON.stringify({player_id:PID})});
headers: apiHeaders(), selPlan=null; loadPlans();
body: JSON.stringify({ player_id: PLAYER_ID }) document.getElementById('right-panel').innerHTML=
}); '<div class="card" style="border-color:#2a3060"><div class="empty" style="color:#444">← Επίλεξε πλάνο για λεπτομέρειες</div></div>';
loadPlans();
if (selectedPlanId === planId) {
document.getElementById('detail-panel').classList.remove('open');
document.getElementById('add-participant-card').style.display = 'none';
}
}; };
// ---- Init ---- // ─── Create plan ───
loadPlans(); window.createPlan = async function(){
loadTowns(); const msg=document.getElementById('msg');
setInterval(loadPlans, 15000); const dt=document.getElementById('p-arr').value;
setInterval(() => { if (selectedPlanId) loadPlanDetail(selectedPlanId); }, 15000); if(!dt){ showMsg(msg,'Επίλεξε ώρα άφιξης',true); return; }
const r=await fetch(`/api/${WID}/attack_plans`,{method:'POST',headers:hdrs(),body:JSON.stringify({
player_id:PID,
plan_name:document.getElementById('p-name').value.trim()||'Επίθεση',
target_town_name:document.getElementById('p-tname').value.trim(),
target_x:parseFloat(document.getElementById('p-tx').value)||null,
target_y:parseFloat(document.getElementById('p-ty').value)||null,
target_arrival_time:Math.floor(new Date(dt).getTime()/1000)
})});
const d=await r.json();
if(d.ok){ showMsg(msg,`✅ Πλάνο δημιουργήθηκε (ID:${d.plan_id})`,false); loadPlans(); }
else showMsg(msg,'❌ '+d.error,true);
};
// ─── Init ───
async function init(){
await loadClanTowns();
await loadPlans();
setInterval(loadPlans, 15000);
setInterval(()=>{ if(selPlan) renderPlanDetail(selPlan); }, 15000);
}
init();
})(); })();
</script> </script>
</body> </body>