From 3d1dcf0f7e20337ad2ee3838c13852f4b5d9fb4a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 12:27:44 +0000 Subject: [PATCH] Set the defense formation by dragging or tapping regiments onto the battlefield The Defense Formation window used one dropdown per slot and row. It now shows the same terrain slots as the attack picker, each with a front place and (when there are more regiments than slots) a reserve place. Regiments are dragged onto a place, tapped and then a place (touch screens), or finger-dragged; dropping on a taken place swaps. The form still posts slot__, so saving and the battle rules are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WpWbfCGmL2XfCVKDE9frai --- public/dolumar/css/windows.css | 210 ++++++++++++++++++ public/dolumar/javascript/dolumar.js | 208 ++++++++++++++++- resources/language/de/battle.lng | 6 + resources/language/en/battle.lng | 6 + resources/language/fr/battle.lng | 6 + resources/language/nl/battle.lng | 6 + resources/language/pl/battle.lng | 6 + .../templates/default/battle/formation.phpt | 69 ++++-- src/Dolumar/Windows/Formation.php | 76 +++++-- 9 files changed, 555 insertions(+), 38 deletions(-) diff --git a/public/dolumar/css/windows.css b/public/dolumar/css/windows.css index 4e602b47..3b086f4d 100755 --- a/public/dolumar/css/windows.css +++ b/public/dolumar/css/windows.css @@ -805,6 +805,7 @@ h3 label { background: red; } + /* Server settings */ @@ -831,3 +832,212 @@ h3 label { margin-top: 16px; } + +/* + Defense formation: the attack picker's terrain slots, each with a front place and + (with enough regiments) a reserve place below it. Regiments are dragged or tapped in. +*/ +div.formation p.formation-help +{ + margin: 0 0 8px; +} + +div.formation p.formation-note +{ + margin: 6px 0 0; + font-size: 0.9em; +} + +div.formation div.formation-scroll +{ + overflow-x: auto; + overflow-y: hidden; + position: relative; + width: 100%; + -webkit-overflow-scrolling: touch; +} + +div.formation div.formation-field +{ + overflow: hidden; + margin: 0 auto; +} + +div.formation div.formation-slot +{ + float: left; + width: 75px; + height: 150px; + position: relative; +} + +div.formation span.formation-terrain +{ + position: absolute; + left: 2px; + right: 2px; + top: 3px; + font-size: 10px; + line-height: 14px; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: #40331f; + background: rgba(251, 243, 220, 0.85); + border-radius: 3px; +} + +div.formation div.formation-cell +{ + position: absolute; + left: 9px; + width: 55px; + height: 55px; + border: 1px dashed rgba(251, 243, 220, 0.9); + border-radius: 3px; +} + +div.formation div.formation-cell.front +{ + top: 22px; +} + +div.formation div.formation-slot.rows-1 div.formation-cell.front +{ + top: 52px; +} + +div.formation div.formation-cell.reserve +{ + top: 86px; +} + +div.formation div.formation-cell.filled +{ + border-color: transparent; +} + +div.formation span.formation-cell-label +{ + display: block; + margin-top: 19px; + font-size: 10px; + text-align: center; + color: #40331f; + background: rgba(251, 243, 220, 0.85); +} + +div.formation div.formation-cell.filled span.formation-cell-label +{ + display: none; +} + +div.formation div.formation-cell.reserve img.unit-selected +{ + opacity: 0.85; +} + +div.formation img.unit-selected +{ + position: absolute; + left: 1px; + top: 1px; + width: 50px; + height: 50px; + border: 1px solid black; +} + +div.formation div.clear-button +{ + position: absolute; + left: 2px; + right: 2px; + bottom: 3px; + text-align: center; + font-size: 10px; + line-height: 14px; +} + +div.formation div.clear-button span +{ + display: block; + color: #000097; + text-decoration: underline; + background: rgba(251, 243, 220, 0.9); + cursor: pointer; +} + +div.formation div.clear-button span:hover +{ + color: #b3261e; +} + +.tap-placing div.formation-cell +{ + cursor: pointer; + box-shadow: inset 0 0 0 2px rgba(240, 192, 64, 0.8); +} + +div.formation div.formation-cell.touch-over, +div.formation div.formation-cell.hover +{ + box-shadow: inset 0 0 0 3px #f0c040; +} + +div.formation div.formation-pool +{ + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +div.formation div.formation-unit +{ + width: 92px; + text-align: center; + font-size: 11px; + line-height: 14px; +} + +div.formation div.formation-unit img +{ + display: block; + width: 50px; + height: 50px; + margin: 0 auto 2px; + border: 1px solid black; + position: relative; + z-index: 1; +} + +div.formation div.formation-unit span +{ + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +div.formation div.formation-unit span.formation-unit-name +{ + font-weight: bold; +} + +/* Placed regiments stay in the list (drag them again to move them), dimmed. */ +div.formation div.formation-unit.placed img, +div.formation div.formation-unit.placed span +{ + opacity: 0.45; +} + +div.formation div.ui-btn-row +{ + margin-top: 10px; +} + +/* Tap-to-place marks the places, not the whole terrain column. */ +.tap-placing div.formation-slot +{ + box-shadow: none; +} diff --git a/public/dolumar/javascript/dolumar.js b/public/dolumar/javascript/dolumar.js index 0f355957..c3e9c429 100755 --- a/public/dolumar/javascript/dolumar.js +++ b/public/dolumar/javascript/dolumar.js @@ -499,9 +499,215 @@ Game.dolumar = Game.dolumar.enableTapToPlace (div, tapImages, tapSlots); }, + /* + Defense formation: the same terrain slots as the attack picker. A regiment is dragged + (or tapped, then a slot tapped) onto the front or reserve place of a slot. Each place + holds a hidden input slot__ that the window saves. A regiment stands in one + place at a time; dropping it on a taken place swaps the two. + */ + 'initFormationWindow' : function (oWindow) + { + var div = $(oWindow.div); + var form = div.select ('form.formation-form')[0]; + + if (!form) + { + return; + } + + // Content is replaced after saving: forget drop targets that left the page. + Droppables.drops.slice (0).each (function (drop) + { + if (!document.body.contains (drop.element)) + { + Droppables.remove (drop.element); + } + }); + + var scrolldiv = div.select ('div.formation-scroll')[0]; + var field = div.select ('div.formation-field')[0]; + var lng_clear = div.select ('span.formation-clear-text')[0].innerHTML; + + var units = {}; + var cells = div.select ('div.formation-cell'); + var tapImages = []; + var tapSlots = []; + + div.select ('div.formation-unit').each (function (box) + { + var img = box.select ('img')[0]; + var key = box.getAttribute ('data-unit'); + + img.id = oWindow.sDialogId + 'fimg' + key; + img.unitKey = key; + units[key] = { 'box' : box, 'img' : img }; + tapImages.push (img); + + img.dragable = new Draggable + ( + img.id, + { + 'revert' : true, + 'reverteffect' : function (element, top_offset, left_offset) + { + element.style.left = (parseInt (element.style.left) - left_offset) + 'px'; + element.style.top = (parseInt (element.style.top) - top_offset) + 'px'; + }, + // No fade effects: they leave an inline opacity over the "placed" dimming. + 'starteffect' : function (element) + { + element.style.opacity = 0.7; + }, + 'endeffect' : function (element) + { + element.style.opacity = ''; + } + } + ); + }); + + var inputOf = function (cell) + { + return cell.select ('input')[0]; + }; + + var cellOf = function (key) + { + for (var i = 0; i < cells.length; i ++) + { + if (inputOf (cells[i]).value == key) + { + return cells[i]; + } + } + + return null; + }; + + // Draw a place from its input: the regiment's image and a clear link, or empty. + var render = function (cell) + { + cell.select ('img.unit-selected, div.clear-button').each (function (el) + { + el.remove (); + }); + + var key = inputOf (cell).value; + var unit = units[key]; + + if (!unit) + { + cell.removeClassName ('filled'); + return; + } + + cell.addClassName ('filled'); + + var img = document.createElement ('img'); + img.src = unit.img.src; + img.title = unit.img.title; + img.alt = ''; + img.className = 'unit-selected'; + cell.appendChild (img); + + var clear = document.createElement ('div'); + clear.className = 'clear-button'; + clear.innerHTML = '' + lng_clear + ''; + clear.onclick = function () + { + inputOf (cell).value = '0'; + refresh (); + }; + cell.appendChild (clear); + }; + + var refresh = function () + { + // A reserve only counts behind a regiment in front of it: move it up. + div.select ('div.formation-slot').each (function (slot) + { + var places = slot.select ('div.formation-cell'); + + for (var i = 0; i < places.length - 1; i ++) + { + if (inputOf (places[i]).value == '0' && inputOf (places[i + 1]).value != '0') + { + inputOf (places[i]).value = inputOf (places[i + 1]).value; + inputOf (places[i + 1]).value = '0'; + } + } + }); + + cells.each (render); + + for (var key in units) + { + if (cellOf (key)) + { + units[key].box.addClassName ('placed'); + } + else + { + units[key].box.removeClassName ('placed'); + } + } + }; + + var place = function (key, cell) + { + var from = cellOf (key); + var current = inputOf (cell).value; + + if (from === cell) + { + return; + } + + // Swap: the regiment that stood here takes the place the new one came from. + if (from) + { + inputOf (from).value = current; + } + + inputOf (cell).value = key; + refresh (); + }; + + cells.each (function (cell, i) + { + cell.id = oWindow.sDialogId + 'fcell' + i; + + var dropOptions = + { + 'onDrop' : function (element, dropel) + { + place (element.unitKey, dropel); + return true; + }, + 'hoverclass' : 'hover', + 'scrollid' : scrolldiv + }; + + Droppables.add (cell.id, dropOptions); + tapSlots.push ({ 'slot' : cell, 'options' : dropOptions }); + }); + + // The slot row is as wide as its slots; on a narrow screen it scrolls. + var width = 0; + field.childElements ().each (function (el) + { + width += el.getWidth (); + }); + field.style.width = (width + 1) + 'px'; + + refresh (); + + Game.dolumar.enableTapToPlace (div, tapImages, tapSlots); + }, + /* Battle window - + TODO: Clean all dragables on destruction */ 'initBattleWindow' : function (oWindow) diff --git a/resources/language/de/battle.lng b/resources/language/de/battle.lng index a2f1762e..86bfda60 100755 --- a/resources/language/de/battle.lng +++ b/resources/language/de/battle.lng @@ -97,6 +97,12 @@ row="Reihe" saved="Deine Formation wurde gespeichert." noSquads="Du hast noch keine Mannschaften." slot="Gebiet" +help="Ziehe eine Mannschaft auf ein Gebiet, oder tippe auf eine Mannschaft und dann auf ein Gebiet. Diese Formation wird verwendet, wenn dein Dorf angegriffen wird." +reserve="Reserve" +reserveHelp="Eine Reserve übernimmt das Gebiet, wenn die Mannschaft davor nicht zu Hause ist." +regiments="Deine Mannschaften" +unplaced="Mannschaften, die du nicht aufstellst, kämpfen auf einem zufälligen freien Gebiet." +clear="Leeren" [support] title="Sende Verstärkungstruppen nach @@target." diff --git a/resources/language/en/battle.lng b/resources/language/en/battle.lng index b5139467..ef11d825 100755 --- a/resources/language/en/battle.lng +++ b/resources/language/en/battle.lng @@ -103,6 +103,12 @@ save = "Save formation" row = "Row" saved = "Your formation has been saved." noSquads = "You don't have any squads yet." +help = "Drag a regiment onto a slot, or tap a regiment and then a slot. This formation is used whenever your village is attacked." +reserve = "Reserve" +reserveHelp = "A reserve takes the slot when the regiment in front of it is away from home." +regiments = "Your regiments" +unplaced = "Regiments you leave out fight in a random free slot." +clear = "Clear" [support] title = "Send support units to @@target" diff --git a/resources/language/fr/battle.lng b/resources/language/fr/battle.lng index 3d03393d..b18fa8ab 100644 --- a/resources/language/fr/battle.lng +++ b/resources/language/fr/battle.lng @@ -30,6 +30,12 @@ row="Ligne" save="Sauver la formation" saved="Votre formation a été sauvegardée." slot="Position" +help="Faites glisser un peloton sur une position, ou touchez un peloton puis une position. Cette formation est utilisée chaque fois que votre village est attaqué." +reserve="Réserve" +reserveHelp="Une réserve prend la position quand le peloton devant elle n'est pas au village." +regiments="Vos pelotons" +unplaced="Les pelotons non placés combattent sur une position libre au hasard." +clear="Retirer" [overview] actions="Actions" diff --git a/resources/language/nl/battle.lng b/resources/language/nl/battle.lng index 0bf62714..13556dd6 100755 --- a/resources/language/nl/battle.lng +++ b/resources/language/nl/battle.lng @@ -99,6 +99,12 @@ save="Formatie opslaan" row="Rij" saved="Je aanvalsformatie is opgeslagen." noSquads="Je hebt nog geen regimenten." +help="Sleep een regiment naar een plaats, of tik op een regiment en dan op een plaats. Deze formatie wordt gebruikt wanneer je dorp aangevallen wordt." +reserve="Reserve" +reserveHelp="Een reserve neemt de plaats in wanneer het regiment ervoor niet thuis is." +regiments="Jouw regimenten" +unplaced="Regimenten die je niet plaatst, vechten op een willekeurige vrije plaats." +clear="Wissen" [support] title="Stuur hulptroepen naar @@target" diff --git a/resources/language/pl/battle.lng b/resources/language/pl/battle.lng index a192a595..be4cba5a 100755 --- a/resources/language/pl/battle.lng +++ b/resources/language/pl/battle.lng @@ -28,6 +28,12 @@ row="Rząd" save="Zapisz formację" saved="Twoja formacja została zapisana" slot="Otwór" +help="Przeciągnij oddział na pole albo dotknij oddziału, a potem pola. Ta formacja jest używana zawsze, gdy twoja wioska zostanie zaatakowana." +reserve="Rezerwa" +reserveHelp="Rezerwa zajmuje pole, gdy oddział przed nią jest poza domem." +regiments="Twoje oddziały" +unplaced="Oddziały, których nie ustawisz, walczą na losowym wolnym polu." +clear="Usuń" [overview] actions="Akcje" diff --git a/resources/templates/default/battle/formation.phpt b/resources/templates/default/battle/formation.phpt index b435d5d7..312db851 100755 --- a/resources/templates/default/battle/formation.phpt +++ b/resources/templates/default/battle/formation.phpt @@ -2,36 +2,61 @@ -

getText ($message)?>

+

getText ($message)?>

-

getText ($message)?>

+

getText ($message)?>

- 0) { ?> -
-
- $slot) { ?> -
- - - - + 0) { ?> + getText ('reserve')?> - - - +
+ + + + +
 
+ + + + 1) { ?> +

getText ('reserveHelp')?>

+ + +

getText ('regiments')?>

+
+ +
+ + +
+
+ +

getText ('unplaced')?>

+ +
+ +
- -
+

getText ('noSquads')?>

diff --git a/src/Dolumar/Windows/Formation.php b/src/Dolumar/Windows/Formation.php index de516df5..43d4fbda 100755 --- a/src/Dolumar/Windows/Formation.php +++ b/src/Dolumar/Windows/Formation.php @@ -51,8 +51,9 @@ public function setSettings () $this->setTitle ($text->get ('formation', 'menu', 'main')); } - $this->setSize (300, 300); + $this->setSize (460, 540); $this->setClassName ('formation'); + $this->setOnload ('Game.dolumar.initFormationWindow'); $this->setAllowOnlyOnce (); } @@ -79,32 +80,77 @@ public function getContent ($msg = null, $failed = false) // See how much rows that is $rows = $this->countRows ($squads, $slots); - $page->set ('rows', $rows); - $page->set ('slots', $slots); - + $text = Neuron_Core_Text::__getInstance (); + if (!empty ($msg)) { $page->set ('message', $msg); $page->set ('error', $failed); } - + + // The regiments that can be placed, and where each one stands now. + $placed = array (); + foreach ($squads as $v) { if ($v->getUnitsAmount () > 0) { - $page->addListValue - ( - 'squads', - array + foreach ($v->getUnits () as $unit) + { + $key = $v->getId () . '_' . $unit->getUnitId (); + + if ($unit->getDefaultSlot () > 0 && $unit->getSlotPriority () < $rows) + { + $placed[$unit->getDefaultSlot ()][$unit->getSlotPriority ()] = $key; + } + + $page->addListValue ( - 'sName' => Neuron_Core_Tools::output_varchar ($v->getName ()), - 'oUnits' => $v->getUnits (), - 'id' => $v->getId () - ) - ); + 'units', + array + ( + 'key' => $key, + 'sName' => Neuron_Core_Tools::output_varchar ($v->getName ()), + 'sUnit' => Neuron_Core_Tools::output_varchar ($unit->getName ()), + 'amount' => $unit->getAvailableAmount (), + 'image' => $unit->getImageUrl () + ) + ); + } } } - + + foreach ($slots as $slotId => $slot) + { + $cells = array (); + + for ($i = 0; $i < $rows; $i ++) + { + $cells[$i] = isset ($placed[$slotId][$i]) ? $placed[$slotId][$i] : '0'; + } + + // A reserve only counts behind a regiment in front of it (see processInput). + if ($rows > 1 && $cells[0] === '0' && $cells[1] !== '0') + { + $cells[0] = $cells[1]; + $cells[1] = '0'; + } + + $page->addListValue + ( + 'slots', + array + ( + 'id' => $slotId, + 'sName' => $slot->getName (), + 'sType' => $text->get ($slot->getName (), 'slots', 'battle'), + 'cells' => $cells + ) + ); + } + + $page->set ('rows', $rows); + return $page->parse ('battle/formation.phpt'); }