@@ -21,10 +21,11 @@
. card-title { font-size : 1 rem ; font-weight : 700 ; color : var ( - - gold ) ; margin-bottom : 1 rem ; padding-bottom : .75 rem ; border-bottom : 1 px solid var ( - - border ) }
. card-title { font-size : 1 rem ; font-weight : 700 ; color : var ( - - gold ) ; margin-bottom : 1 rem ; padding-bottom : .75 rem ; border-bottom : 1 px solid var ( - - border ) }
label { display : block ; font-size : .8 rem ; color : var ( - - muted ) ; margin-bottom : .3 rem ; margin-top : .75 rem }
label { display : block ; font-size : .8 rem ; color : var ( - - muted ) ; margin-bottom : .3 rem ; margin-top : .75 rem }
label : first-of-type { margin-top : 0 }
label : first-of-type { margin-top : 0 }
input [ type = text ] , input [ type = number ] , input [ type = datetime-local ] {
input [ type = text ] , input [ type = number ] , input [ type = datetime-local ] , select {
width : 100 % ; padding : 9 px 12 px ; background : #0f0f1a ; border : 1 px solid var ( - - border ) ;
width : 100 % ; padding : 9 px 12 px ; background : #0f0f1a ; border : 1 px solid var ( - - border ) ;
border-radius : 8 px ; color : var ( - - text ) ; font-size : .875 rem ; font-family : inherit ; transition : border-color .2 s }
border-radius : 8 px ; color : var ( - - text ) ; font-size : .875 rem ; font-family : inherit ; transition : border-color .2 s }
input : focus { outline : none ; border-color : var ( - - gold ) }
input : focus , select : focus { outline : none ; border-color : var ( - - gold ) }
select option { background : #181824 }
. btn { padding : 9 px 18 px ; border : none ; border-radius : 8 px ; font-family : inherit ; font-weight : 600 ;
. btn { padding : 9 px 18 px ; border : none ; border-radius : 8 px ; font-family : inherit ; font-weight : 600 ;
font-size : .85 rem ; cursor : pointer ; transition : all .2 s }
font-size : .85 rem ; cursor : pointer ; transition : all .2 s }
. btn-gold { background : var ( - - gold ) ; color : #0f0f1a } . btn-gold : hover { background : #e0b85a }
. btn-gold { background : var ( - - gold ) ; color : #0f0f1a } . btn-gold : hover { background : #e0b85a }
@@ -56,9 +57,8 @@
padding : 10 px 14 px ; border-radius : 8 px ; font-size : .83 rem ; margin-top : .75 rem }
padding : 10 px 14 px ; border-radius : 8 px ; font-size : .83 rem ; margin-top : .75 rem }
. warn-box { background : rgba ( 240 , 192 , 64 , .08 ) ; border : 1 px solid rgba ( 240 , 192 , 64 , .25 ) ; color : var ( - - yellow ) ;
. warn-box { background : rgba ( 240 , 192 , 64 , .08 ) ; border : 1 px solid rgba ( 240 , 192 , 64 , .25 ) ; color : var ( - - yellow ) ;
padding : 10 px 14 px ; border-radius : 8 px ; font-size : .83 rem ; margin-top : .5 rem }
padding : 10 px 14 px ; border-radius : 8 px ; font-size : .83 rem ; margin-top : .5 rem }
# msg { display : none ; margin-top : .75 rem }
. town-meta { font-size : .75 rem ; color : var ( - - muted ) ; margin-top : 4 px }
. countdown { font-family : monospace ; font-w eight: 700 ; color : var ( - - yellow ) }
. section-sep { h eight: 1 px ; background : var ( - - border ) ; margin : 1 rem 0 }
. section-sep { height : 1 px ; background : var ( - - border ) ; margin : 1.5 rem 0 }
. plan-row { cursor : pointer } . plan-row : hover td { background : rgba ( 200 , 164 , 74 , .05 ) }
. plan-row { cursor : pointer } . plan-row : hover td { background : rgba ( 200 , 164 , 74 , .05 ) }
. detail-panel { display : none }
. detail-panel { display : none }
. detail-panel . open { display : block }
. detail-panel . open { display : block }
@@ -68,25 +68,32 @@
. unit-input input { padding : 6 px 8 px ; font-size : .82 rem }
. unit-input input { padding : 6 px 8 px ; font-size : .82 rem }
. feasible-ok { color : var ( - - green ) ; font-size : .78 rem }
. feasible-ok { color : var ( - - green ) ; font-size : .78 rem }
. feasible-err { color : var ( - - red ) ; font-size : .78 rem }
. feasible-err { color : var ( - - red ) ; font-size : .78 rem }
# msg { display : none ; margin-top : .75 rem }
< / style >
< / style >
< / head >
< / head >
< body >
< 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" >
< div class = "page-header" >
< a href = "/player/{{ player_id }}/{{ world_id }}/hub " class = "back-link" > ← Hub< / a >
< a href = "/player/{{ player_id }}/{{ world_id }}" class = "back-link" > ← Hub< / a >
< h1 > ⚔️ Attack Planner — {{ world_id }}< / h1 >
< h1 > ⚔️ Attack Planner — {{ world_id }}< / h1 >
< / div >
< / div >
< div class = "grid" >
< div class = "grid" >
<!-- LEFT: Plans list -->
<!-- LEFT: Plans list + detail -->
< div >
< div >
< div class = "card" >
< div class = "card" >
< div class = "card-title" > 📋 Ενεργά Πλάνα< / div >
< div class = "card-title" > 📋 Πλάνα Επίθεσης — {{ world_id }} < / div >
< div id = "plans-list" > < div class = "empty" > Φόρτωση...< / div > < / div >
< div id = "plans-list" > < div class = "empty" > Φόρτωση...< / div > < / div >
< / div >
< / div >
<!-- Plan detail panel (shown when a plan is clicked) -->
< div class = "card detail-panel" id = "detail-panel" >
< div class = "card detail-panel" id = "detail-panel" >
< div class = "card-title" id = "detail-title" > Λεπτομέρειες Πλάνου< / div >
< div class = "card-title" id = "detail-title" > Λεπτομέρειες Πλάνου< / div >
< div id = "detail-body" > < / div >
< div id = "detail-body" > < / div >
@@ -95,27 +102,29 @@
<!-- RIGHT: Create plan + add participant -->
<!-- RIGHT: Create plan + add participant -->
< div >
< div >
<!-- Create plan -->
< div class = "card" >
< div class = "card" >
< div class = "card-title" > ➕ Νέο Πλάνο Επίθεσης < / div >
< div class = "card-title" > ➕ Νέο Πλάνο< / div >
< label > Όνομα Πλάνου< / label >
< label > Όνομα Πλάνου< / label >
< input type = "text" id = "plan-name" placeholder = "π.χ. Επίθεση στον Leonidas" >
< 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 >
< label > Όνομα Στόχου< / label >
< input type = "text" id = "target-name" placeholder = "π.χ. Sparta Colony" >
< input type = "text" id = "target-name" placeholder = "π.χ. Sparta Colony" >
< label > Συντεταγμένες Στόχου (X) < / label >
< label > Συντεταγμένες Στόχου X < / label >
< input type = "number" id = "target-x" placeholder = "π.χ. 394 " min = "0" max = "999" >
< input type = "number" id = "target-x" placeholder = "π.χ. 503 " min = "0" max = "999" >
< label > Συντεταγμένες Στόχου (Y) < / label >
< label > Συντεταγμένες Στόχου Y < / label >
< input type = "number" id = "target-y" placeholder = "π.χ. 512 " min = "0" max = "999" >
< input type = "number" id = "target-y" placeholder = "π.χ. 474 " min = "0" max = "999" >
< label > Ώρα Άφιξης (τοπική ώρα)< / label >
< label > Ώρα Άφιξης (τοπική ώρα)< / label >
< input type = "datetime-local" id = "arrival-time" >
< input type = "datetime-local" id = "arrival-time" >
< div class = "info -box" >
< div class = "warn -box mt " >
⏱ Η ώρα άφιξης πρέπει ν α είναι τουλάχιστον 2 λεπτά στο μέλλον. Όλες ο ι ώρες αποθηκεύονται σε UTC.
⏱ Η ώρα άφιξης πρέπει ν α είναι τουλάχιστον 2 λεπτά στο μέλλον.
< / div >
< / div >
< div class = "mt" >
< div class = "mt" >
@@ -124,30 +133,21 @@
< div id = "msg" > < / div >
< div id = "msg" > < / div >
< / div >
< / div >
<!-- Add participant ( shown after plan selected) -->
<!-- Add participant — only shown after a plan is selected -->
< div class = "card" id = "add-participant-card" style = "display:none" >
< div class = "card" id = "add-participant-card" style = "display:none" >
< div class = "card-title" > 👤 Προσθήκη Συμμετέχοντα < / div >
< div class = "card-title" > 👤 Προσθήκη Πόλης στο Πλάνο < / div >
< p style = "font-size:.82rem;color:var(--muted);margin-bottom:.75rem" >
< p style = "font-size:.82rem;color:var(--muted);margin-bottom:.75rem" >
Πλάνο: < strong id = "selected-plan-name" style = "color:var(--gold)" > < / strong >
Πλάνο: < strong id = "selected-plan-name" style = "color:var(--gold)" > < / strong >
< / p >
< / p >
< label > Origin Town ID < / label >
< label > Επιλογή Πόλης ({{ world_id }}) < / label >
< input type = "text" id = "p-town-id" placeholder = "Από town_state ">
< select id = "p-town-select" onchange = "onTownSelected() ">
< option value = "" > — Επίλεξε πόλη —< / option >
< label > Όνομα Πόλης< / label >
< / select >
< input type = "text" id = "p-town-name" placeholder = "Προαιρετικό" >
< div class = "town-meta" id = "p-town-meta" > < / div >
< label > Συντεταγμένες Πόλης X< / label >
< input type = "number" id = "p-x" min = "0" max = "999" >
< label > Συντεταγμένες Πόλης Y< / label >
< input type = "number" id = "p-y" min = "0" max = "999" >
< label > Θαλάσσια Ζώνη (sea)< / label >
< input type = "number" id = "p-sea" placeholder = "π.χ. 45" >
< div class = "section-sep" > < / div >
< div class = "section-sep" > < / div >
< div class = "card-title" style = "border:none;padding:0 ;margin-bottom:.5rem"> 🗡 Μονάδες< / 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 = "unit-grid" id = "unit-inputs" > < / div >
< div class = "mt" >
< div class = "mt" >
@@ -161,14 +161,18 @@
< script >
< script >
( function ( ) {
( function ( ) {
const PLAYER _ID = '{{ player_id }}' ;
const PLAYER _ID = window . PLAYER _ID ;
const WORLD _ID = '{{ world_id }}' ;
const WORLD _ID = window . WORLD _ID ;
const CLAN _KEY = window . _ _GRC _CLAN _KEY ;
let selectedPlanId = null ;
let selectedPlanId = null ;
// ---- Unit list for inputs (common Grepolis land units) ----
// ---- Town data loaded from DB ----
let townData = [ ] ; // array of town objects with x, y, sea, town_id, town_name
// ---- Unit list ----
const UNITS = [
const UNITS = [
'swordsman' , 'slinger' , 'archer' , 'hoplite' , 'horseman' ,
'swordsman' , 'slinger' , 'archer' , 'hoplite' , 'horseman' ,
'chariot' , 'catapult' , 'godsent' ,
'chariot' , 'catapult' ,
'bireme' , 'attack_ship' , 'demolition_ship' , 'transport_ship' , 'colonize_ship'
'bireme' , 'attack_ship' , 'demolition_ship' , 'transport_ship' , 'colonize_ship'
] ;
] ;
@@ -177,10 +181,51 @@
UNITS . forEach ( u => {
UNITS . forEach ( u => {
const div = document . createElement ( 'div' ) ;
const div = document . createElement ( 'div' ) ;
div . className = 'unit-input' ;
div . className = 'unit-input' ;
div . innerHTML = ` <label> ${ u } </label><input type="number" id="unit_ ${ u } " min="0" value="0" placeholder="0" > ` ;
div . innerHTML = ` <label> ${ u } </label><input type="number" id="unit_ ${ u } " min="0" value="0"> ` ;
grid . appendChild ( div ) ;
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> Y: <strong> ${ town . y } </strong> Sea: <strong> ${ town . sea } </strong> World: <strong> ${ town . world _id } </strong> ` ;
}
} ;
// ---- Helpers ----
// ---- Helpers ----
function showMsg ( el , text , isError ) {
function showMsg ( el , text , isError ) {
el . style . display = 'block' ;
el . style . display = 'block' ;
@@ -197,37 +242,39 @@
return new Date ( unix * 1000 ) . toLocaleString ( 'el-GR' ) ;
return new Date ( unix * 1000 ) . toLocaleString ( 'el-GR' ) ;
}
}
function countdown ( unix ) {
function apiHeaders ( ) {
if ( ! unix ) return '– ' ;
return { 'Content-Type' : 'application/json' , 'X-Clan-Key' : CLAN _KEY } ;
const s = Math . max ( 0 , Math . floor ( unix - Date . now ( ) / 1000 ) ) ;
const h = Math . floor ( s / 3600 ) , m = Math . floor ( ( s % 3600 ) / 60 ) , ss = s % 60 ;
return ` <span class="countdown"> ${ String ( h ) . padStart ( 2 , '0' ) } : ${ String ( m ) . padStart ( 2 , '0' ) } : ${ String ( ss ) . padStart ( 2 , '0' ) } </span> ` ;
}
}
// ---- Load plans ----
// ---- Load plans list ----
async function loadPlans ( ) {
async function loadPlans ( ) {
const res = await fetch ( ` /api/ ${ WORLD _ID } /attack_plans ` ) ;
const res = await fetch ( ` /api/ ${ WORLD _ID } /attack_plans ` ) ;
const data = await res . json ( ) ;
const data = await res . json ( ) ;
const el = document . getElementById ( 'plans-list' ) ;
const el = document . getElementById ( 'plans-list' ) ;
if ( ! data . length ) {
if ( ! Array . isArray ( data ) || ! data . length ) {
el . innerHTML = '<div class="empty">Δεν υπάρχουν πλάνα ακόμη.</div>' ;
el . innerHTML = '<div class="empty">Δεν υπάρχουν πλάνα ακόμη.</div>' ;
return ;
return ;
}
}
let html = ` <table>
let html = ` <table>
<thead><tr><th>Πλάνο</th><th>Στόχος</th><th>Άφιξη</th><th>Κατάσταση</th><th>Συμμ.</th><th></th></tr></thead>
<thead><tr>
<tbody> ` ;
<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 } ')">
html += ` <tr class="plan-row" onclick="selectPlan( ${ p . id } ,' ${ p . plan _name } ')">
<td><strong> ${ p . plan _name } </strong></td>
<td><strong> ${ p . plan _name } </strong></td>
<td> ${ p . target _town _name || '– ' } </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 style="font-size:.78rem"> ${ formatTs ( p . target _arrival _time ) } </td>
<td> ${ statusBadge ( p . status ) } </td>
<td> ${ statusBadge ( p . status ) } </td>
<td> ${ p . participant _count } </td>
<td> ${ p . participant _count } </td>
<td>
<td>
${ p . status === 'draft' ? ` <button class="btn btn-green btn-sm" onclick="event.stopPropagation();armPlan( ${ p . id } )">ARM</button> ` : '' }
${ p . status === 'draft'
<button class="btn btn-red btn-sm" onclick="event.stopPropagation();cancel Plan(${ p . id } )" style="margin-left:4px">✕ </button>
? ` <button class="btn btn-g reen btn-sm" onclick="event.stopPropagation();arm Plan(${ p . id } )">ARM </button>`
: '' }
<button class="btn btn-red btn-sm" style="margin-left:4px"
onclick="event.stopPropagation();cancelPlan( ${ p . id } )">✕</button>
</td>
</td>
</tr> ` ;
</tr> ` ;
}
}
@@ -235,7 +282,7 @@
el . innerHTML = html ;
el . innerHTML = html ;
}
}
// ---- Select plan → show detail + add participant panel ----
// ---- Select plan → show detail + participant panel ----
window . selectPlan = async function ( planId , planName ) {
window . selectPlan = async function ( planId , planName ) {
selectedPlanId = planId ;
selectedPlanId = planId ;
document . getElementById ( 'selected-plan-name' ) . textContent = planName ;
document . getElementById ( 'selected-plan-name' ) . textContent = planName ;
@@ -249,7 +296,7 @@
const panel = document . getElementById ( 'detail-panel' ) ;
const panel = document . getElementById ( 'detail-panel' ) ;
const body = document . getElementById ( 'detail-body' ) ;
const body = document . getElementById ( 'detail-body' ) ;
document . getElementById ( 'detail-title' ) . textContent =
document . getElementById ( 'detail-title' ) . textContent =
` 📌 ${ plan . plan _name } — ${ plan . target _town _name || 'Άγνωστος Στόχος' } ` ;
` 📌 ${ plan . plan _name } — ${ plan . target _town _name || 'Άγνωστος Στόχος' } ( ${ plan . target _x || '?' } , ${ plan . target _y || '?' } ) `;
if ( ! plan . participants || ! plan . participants . length ) {
if ( ! plan . participants || ! plan . participants . length ) {
body . innerHTML = '<div class="empty">Χωρίς συμμετέχοντες ακόμη.</div>' ;
body . innerHTML = '<div class="empty">Χωρίς συμμετέχοντες ακόμη.</div>' ;
@@ -260,20 +307,20 @@
let html = ` <table>
let html = ` <table>
<thead><tr>
<thead><tr>
<th>Πόλη</th><th>Τύπος</th><th>Πλοία</th>
<th>Πόλη</th><th>Τύπος</th><th>Πλοία</th>
<th>Αποστολή</th><th>Επιστροφή</th><th>Κατάσταση </th><th>Feasible </th><th></th>
<th>Αποστολή</th><th>Επιστροφή</th><th>Status </th><th>OK </th><th></th>
</tr></thead><tbody> ` ;
</tr></thead><tbody> ` ;
for ( const p of plan . participants ) {
for ( const p of plan . participants ) {
const feasHtml = p . is _feasible
const f = p . is _feasible
? '<span class="feasible-ok">✅</span>'
? '<span class="feasible-ok">✅</span>'
: ` <span class="feasible-err" title=" ${ p . error _msg } ">❌</span> ` ;
: ` <span class="feasible-err" title=" ${ p . error _msg || '' } ">❌</span> ` ;
html += ` <tr>
html += ` <tr>
<td><strong> ${ p . origin _town _name || p . origin _town _id } </strong></td>
<td><strong> ${ p . origin _town _name || p . origin _town _id } </strong></td>
<td> ${ p . attack _type || '– ' } </td>
<td style="font-size:.75rem" > ${ p . attack _type || '– ' } </td>
<td> ${ p . transport _needed ? p . transport _count : '– ' } </td>
<td> ${ p . transport _needed ? p . transport _count : '– ' } </td>
<td style="font-size:.78 rem"> ${ formatTs ( p . send _time ) } </td>
<td style="font-size:.75 rem"> ${ formatTs ( p . send _time ) } </td>
<td style="font-size:.78 rem"> ${ formatTs ( p . return _time ) } </td>
<td style="font-size:.75 rem"> ${ formatTs ( p . return _time ) } </td>
<td> ${ statusBadge ( p . status ) } </td>
<td> ${ statusBadge ( p . status ) } </td>
<td> ${ feasHtml } </td>
<td> ${ f } </td>
<td>
<td>
<button class="btn btn-red btn-sm"
<button class="btn btn-red btn-sm"
onclick="removeParticipant( ${ planId } ,' ${ p . origin _town _id } ')">✕</button>
onclick="removeParticipant( ${ planId } ,' ${ p . origin _town _id } ')">✕</button>
@@ -282,11 +329,10 @@
}
}
html += '</tbody></table>' ;
html += '</tbody></table>' ;
// Return time summary
const latest = plan . participants . reduce ( ( m , p ) => Math . max ( m , p . return _time || 0 ) , 0 ) ;
const latest = plan . participants . reduce ( ( mx , p ) => Math . max ( mx , p . return _time || 0 ) , 0 ) ;
if ( latest ) {
if ( latest ) {
html += ` <div class="info-box" style="margin-top:.75rem">
html += ` <div class="info-box" style="margin-top:.75rem">
🏠 Τελευταία επιστροφή στρατού : <strong> ${ formatTs ( latest ) } </strong>
🏠 Τελευταία επιστροφή: <strong> ${ formatTs ( latest ) } </strong>
</div> ` ;
</div> ` ;
}
}
@@ -298,22 +344,22 @@
window . createPlan = async function ( ) {
window . createPlan = async function ( ) {
const msg = document . getElementById ( 'msg' ) ;
const msg = document . getElementById ( 'msg' ) ;
const name = document . getElementById ( 'plan-name' ) . value . trim ( ) ;
const name = document . getElementById ( 'plan-name' ) . value . trim ( ) ;
const tName = document . getElementById ( 'target-name' ) . value . trim ( ) ;
const tName = document . getElementById ( 'target-name' ) . value . trim ( ) ;
const tx = parseFloat ( document . getElementById ( 'target-x' ) . value ) ;
const tx = parseFloat ( document . getElementById ( 'target-x' ) . value ) || null ;
const ty = parseFloat ( document . getElementById ( 'target-y' ) . value ) ;
const ty = parseFloat ( document . getElementById ( 'target-y' ) . value ) || null ;
const dtLocal = document . getElementById ( 'arrival-time' ) . value ;
const dtLocal = document . getElementById ( 'arrival-time' ) . value ;
if ( ! dtLocal ) { showMsg ( msg , 'Επίλεξε ώρα άφιξης' , true ) ; return ; }
if ( ! dtLocal ) { showMsg ( msg , 'Επίλεξε ώρα άφιξης' , true ) ; return ; }
const arrivalUnix = Math . floor ( new Date ( dtLocal ) . getTime ( ) / 1000 ) ;
const arrivalUnix = Math . floor ( new Date ( dtLocal ) . getTime ( ) / 1000 ) ;
const res = await fetch ( ` /api/ ${ WORLD _ID } /attack_plans ` , {
const res = await fetch ( ` /api/ ${ WORLD _ID } /attack_plans ` , {
method : 'POST' ,
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' ,
headers : apiHeaders ( ) ,
'X-Clan-Key' : window . _ _GRC _CLAN _KEY || '' } ,
body : JSON . stringify ( {
body : JSON . stringify ( {
player _id : PLAYER _ID , plan _name : name || 'Επίθεση' ,
player _id : PLAYER _ID ,
target _tow n_name : tN ame, target _x : tx || null , target _y : ty || null ,
pla n_name : n ame || 'Επίθεση' ,
target _town _name : tName ,
target _x : tx , target _y : ty ,
target _arrival _time : arrivalUnix
target _arrival _time : arrivalUnix
} )
} )
} ) ;
} ) ;
@@ -326,41 +372,58 @@
}
}
} ;
} ;
// ---- Add participant ----
// ---- Add participant (uses selected town from dropdown) ----
window . addParticipant = async function ( ) {
window . addParticipant = async function ( ) {
if ( ! selectedPlanId ) return ;
if ( ! selectedPlanId ) return ;
const result = document . getElementById ( 'participant-result' ) ;
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 = { } ;
const units = { } ;
UNITS . forEach ( u => {
UNITS . forEach ( u => {
const v = parseInt ( document . getElementById ( ` unit_ ${ u } ` ) . value ) || 0 ;
const v = parseInt ( document . getElementById ( ` unit_ ${ u } ` ) ? . value ) || 0 ;
if ( v > 0 ) units [ u ] = v ;
if ( v > 0 ) units [ u ] = v ;
} ) ;
} ) ;
const body = {
const body = {
requester _player _id : PLAYER _ID ,
requester _player _id : PLAYER _ID ,
player _id : document . getElementById ( 'p-town-id' ) . value . split ( '_' ) [ 0 ] || PLAYER _ID ,
player _id : PLAYER _ID ,
origin _town _id : document . getElementById ( 'p-town-id' ) . value . trim ( ) ,
origin _town _id : town . town _id ,
origin _town _name : document . getElementById ( 'p-town-name' ) . value . trim ( ) ,
origin _town _name : town . town _name ,
origin _x : parseFloat ( document . getElementById ( 'p-x' ) . value ) || null ,
origin _x : town . x ,
origin _y : parseFloat ( document . getElementById ( 'p-y' ) . value ) || null ,
origin _y : town . y ,
origin _sea : parseInt ( document . getElementById ( 'p-sea' ) . value ) || null ,
origin _sea : town . sea ,
units
units
} ;
} ;
const res = await fetch ( ` /api/ ${ WORLD _ID } /attack_plans/ ${ selectedPlanId } /participants ` , {
const res = await fetch ( ` /api/ ${ WORLD _ID } /attack_plans/ ${ selectedPlanId } /participants ` , {
method : 'POST' ,
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' ,
headers : apiHeaders ( ) ,
'X-Clan-Key' : window . _ _GRC _CLAN _KEY || '' } ,
body : JSON . stringify ( body )
body : JSON . stringify ( body )
} ) ;
} ) ;
const data = await res . json ( ) ;
const data = await res . json ( ) ;
if ( data . ok || data . is_feasible !== undefined ) {
if ( data . is _feasible !== undefined ) {
let html = data . is _feasible
if ( data . is _feasible ) {
? ` <div class="info-box">✅ Feasible — Travel: ${ Math . floor ( data . travel _time _secs / 60 ) } m, Ships: ${ data . transport _count || 0 } , Send: ${ new Date ( data . send _time * 1000 ) . toLocaleString ( 'el-GR' ) } </div> `
result . innerHTML = ` <div class="info-box">
: ` <div class="error-box">❌ ${ data . error _msg } </div> ` ;
✅ Feasible — Χρόνος: ${ Math . floor ( data . travel _time _secs / 60 ) } m
result . innerHTML = html ;
| Πλοία: ${ data . transport _count || 0 }
| Αποστολή: ${ new Date ( data . send _time * 1000 ) . toLocaleString ( 'el-GR' ) }
</div> ` ;
} else {
result . innerHTML = ` <div class="error-box">❌ ${ data . error _msg } </div> ` ;
}
loadPlanDetail ( selectedPlanId ) ;
loadPlanDetail ( selectedPlanId ) ;
} else {
} else {
result . innerHTML = ` <div class="error-box">❌ ${ data . error || 'Unknown error' } </div> ` ;
result . innerHTML = ` <div class="error-box">❌ ${ data . error || 'Unknown error' } </div> ` ;
@@ -372,8 +435,7 @@
if ( ! confirm ( 'Αφαίρεση συμμετέχοντα;' ) ) return ;
if ( ! confirm ( 'Αφαίρεση συμμετέχοντα;' ) ) return ;
await fetch ( ` /api/ ${ WORLD _ID } /attack_plans/ ${ planId } /participants/ ${ townId } ` , {
await fetch ( ` /api/ ${ WORLD _ID } /attack_plans/ ${ planId } /participants/ ${ townId } ` , {
method : 'DELETE' ,
method : 'DELETE' ,
headers : { 'Content-Type' : 'application/json' ,
headers : apiHeaders ( ) ,
'X-Clan-Key' : window . _ _GRC _CLAN _KEY || '' } ,
body : JSON . stringify ( { requester _player _id : PLAYER _ID } )
body : JSON . stringify ( { requester _player _id : PLAYER _ID } )
} ) ;
} ) ;
loadPlanDetail ( planId ) ;
loadPlanDetail ( planId ) ;
@@ -384,8 +446,7 @@
window . armPlan = async function ( planId ) {
window . armPlan = async function ( planId ) {
const res = await fetch ( ` /api/ ${ WORLD _ID } /attack_plans/ ${ planId } /arm ` , {
const res = await fetch ( ` /api/ ${ WORLD _ID } /attack_plans/ ${ planId } /arm ` , {
method : 'POST' ,
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' ,
headers : apiHeaders ( ) ,
'X-Clan-Key' : window . _ _GRC _CLAN _KEY || '' } ,
body : JSON . stringify ( { player _id : PLAYER _ID } )
body : JSON . stringify ( { player _id : PLAYER _ID } )
} ) ;
} ) ;
const data = await res . json ( ) ;
const data = await res . json ( ) ;
@@ -399,8 +460,7 @@
if ( ! confirm ( 'Ακύρωση πλάνου;' ) ) return ;
if ( ! confirm ( 'Ακύρωση πλάνου;' ) ) return ;
await fetch ( ` /api/ ${ WORLD _ID } /attack_plans/ ${ planId } /cancel ` , {
await fetch ( ` /api/ ${ WORLD _ID } /attack_plans/ ${ planId } /cancel ` , {
method : 'POST' ,
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' ,
headers : apiHeaders ( ) ,
'X-Clan-Key' : window . _ _GRC _CLAN _KEY || '' } ,
body : JSON . stringify ( { player _id : PLAYER _ID } )
body : JSON . stringify ( { player _id : PLAYER _ID } )
} ) ;
} ) ;
loadPlans ( ) ;
loadPlans ( ) ;
@@ -410,8 +470,9 @@
}
}
} ;
} ;
// ---- Auto-refresh every 15s ----
// ---- Init ----
loadPlans ( ) ;
loadPlans ( ) ;
loadTowns ( ) ;
setInterval ( loadPlans , 15000 ) ;
setInterval ( loadPlans , 15000 ) ;
setInterval ( ( ) => { if ( selectedPlanId ) loadPlanDetail ( selectedPlanId ) ; } , 15000 ) ;
setInterval ( ( ) => { if ( selectedPlanId ) loadPlanDetail ( selectedPlanId ) ; } , 15000 ) ;