diff --git a/.ai/superpowers/specs/2026-11-05-battle-target-picker.md b/.ai/superpowers/specs/2026-11-05-battle-target-picker.md new file mode 100644 index 0000000..e63ad1c --- /dev/null +++ b/.ai/superpowers/specs/2026-11-05-battle-target-picker.md @@ -0,0 +1,33 @@ +# Battle window: choosing a target + +Date: 2026-11-05. Options write-up and mockups: project files `battle-targets/01-target-picking-options.md`. +Thijs picked A + C, with Nearby and the honour cost premium only (with a call to upgrade), the 20 closest villages, +networth instead of runes, and distances in leagues. + +## Way back + +- Every step after Attack / Support starts with "‹ Choose another target · All battles" (`battle/backLinks.phpt`), + including the error pages (unreachable, own village, holiday, not found) and the "attack sent" message. +- The window keeps the picked `target` in its request data. `Dolumar_Windows_Battle::getContent()` now checks + `overview` (back to the list) and `action` without `target` (choose again) before that, and resets the request data. + +## Picker (`Dolumar_Battle_TargetList`, `battle/chooseTarget.phpt`) + +Replaces `Neuron_Structure_ChooseTarget` in the Battle window only (Thieves and Market still use the engine one). + +| Tab | Content | Free | +|---|---|---| +| Suggested | Attacks only, shown when there is something: strike back (attacked you in the last 7 days), avenge a clan mate (latest attack on a clan member by an outsider); premium adds the closest tribe and the closest player that costs no honour and you did not attack this week. Never suggests a village that costs honour or cannot be attacked. Max 4. | Yes (premium adds two kinds) | +| Nearby | The 20 closest active villages by straight line between town centres, filter All / Players / Tribes. Support: your villages and your clan first. | No: upgrade box | +| Recent | They attacked you (7 days), you attacked (with result), profiles you looked at (`villages_visits`, the old "latest targets") | Yes | +| Search | Name search, 10 results, exact match first | Yes | + +Each row: name, owner (or "Tribe"), race, networth, distance in leagues (`Dolumar_Map_Map::tile2league`), and for +premium attacks the honour cost (`Dolumar_Battle_Battle::getHonourPenalty`). Clan members and players on holiday stay +in the list, greyed, with the reason. Free players land on Recent rather than on the locked tab; other tabs carry a +one-line "premium shows the honour cost" link. + +Premium here means `isPremium()` (which includes the free first week). When credits are not configured on a server +(`Dolumar_Premium_Prices::creditsEnabled()` false), nobody could buy premium, so the picker is open to everyone. + +The path distance and travel time stay on the next step: the path search is too heavy for 20 rows. diff --git a/public/dolumar/css/battlereport.css b/public/dolumar/css/battlereport.css index fd4b6f9..92019f1 100644 --- a/public/dolumar/css/battlereport.css +++ b/public/dolumar/css/battlereport.css @@ -110,8 +110,24 @@ div.battle .battle-report .br-tok img, .battle-report .br-dot { display: inline-block; width: 10px; height: 10px; border: 2px solid #f3e7c4; background: #40331f; vertical-align: -2px; } .battle-report .br-dot.us { border-color: #000097; } +/* Choose a target (battle/chooseTarget.phpt, battle/backLinks.phpt) */ +.battle-report .bt-target img { width: 40px; height: 40px; } +.battle-report .bt-rt { flex: 1; min-width: 0; } +.battle-report .bt-rt .ui-card-title { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.battle-report .bt-line { display: block; margin-top: 2px; } +.battle-report .bt-note { font-style: italic; } +.battle-report .bt-target.off { opacity: 0.6; } +.battle-report .bt-target.off .ui-card-title { color: #40331f; text-decoration: none; } +.battle-report .bt-intro { margin-top: 0; } +.battle-report .bt-search .ui-field input { width: 100%; } +.battle-report .bt-premium p { margin: 0; } +.battle-report .bt-premium p.bt-premium-go { margin-top: 8px; } +.battle-report .bt-honour-hint { margin: 12px 0 0; } +.battle-report .ui-tab-panel h3:first-child { margin-top: 0; } + /* Mobile: the kit handles sizes; the transport keeps its buttons on one line. */ body.layout-mobile .sheet-content .battle-report .br-transport .ui-btn { padding: 0 12px; } body.layout-mobile .sheet-content .battle-report .br-back a { text-decoration: none; } +body.layout-mobile .sheet-content .battle-report .bt-search .ui-field input { font-size: 16px; } body.layout-mobile .sheet-content .battle-report h2, body.layout-mobile .sheet-content .battle-report h3 { font-family: Verdana, sans-serif; } diff --git a/resources/language/de/battle.lng b/resources/language/de/battle.lng index 86bfda6..04cc08d 100755 --- a/resources/language/de/battle.lng +++ b/resources/language/de/battle.lng @@ -210,3 +210,54 @@ killed = "@@0 getötet" loot = "@@0 Rohst." runes = "@@0 Runen" rune = "1 Rune" + +[targets] +title_attack = "Angriff: wähle ein Ziel" +title_support = "Unterstützung: wähle eine Siedlung" +tab_suggested = "Vorschläge" +tab_nearby = "In der Nähe" +tab_recent = "Zuletzt" +tab_search = "Suche" +suggestedIntro = "Siedlungen, die einen Blick wert sind. Du entscheidest." +filter_all = "Alle" +filter_players = "Spieler" +filter_tribes = "Stämme" +closest = "die @@amount nächsten" +village = "Name der Siedlung" +search = "Suchen" +tribe = "Stamm" +networth = "Punktzahl @@networth" +leagues = "@@distance League" +honourLoss = "du verlierst @@honour% Ehre" +noHonourLoss = "kein Ehrverlust" +status_own = "Deine eigene Siedlung." +status_clan = "Clanmitglied: du kannst diese Siedlung nicht angreifen." +status_holiday = "Im Urlaub: diese Siedlung ist nicht erreichbar." +section_ownVillages = "Deine Siedlungen" +section_clanVillages = "Dein Clan" +section_otherVillages = "Andere Siedlungen in der Nähe" +section_attackedYou = "Sie haben dich angegriffen" +section_fought = "Du hast angegriffen" +section_visited = "Profile, die du angesehen hast" +empty_suggested = "Im Moment gibt es nichts vorzuschlagen." +empty_nearby = "Keine Siedlungen in der Nähe gefunden." +empty_search = "Ich habe keine Siedlung mit diesem Namen gefunden." +empty_ownVillages = "Du hast keine anderen Siedlungen." +empty_clanVillages = "Du bist in keinem Clan, oder dein Clan hat keine anderen Siedlungen." +empty_otherVillages = "Keine anderen Siedlungen in der Nähe gefunden." +empty_attackedYou = "Diese Woche hat dich niemand angegriffen." +empty_fought = "Du hast noch niemanden angegriffen." +empty_visited = "Du hast noch keine Siedlungsprofile angesehen." +attackedYou = "hat dich am @@when angegriffen" +youWon = "gewonnen, @@when" +youLost = "zurückgeschlagen, @@when" +whyStrikeBack = "Schlag zurück: sie haben dich am @@when angegriffen" +whyClan = "Sie haben dein Clanmitglied @@village angegriffen (@@when)" +whyTribe = "Nächster Stamm: Überfälle kosten keine Ehre" +whyPlayer = "In der Nähe, kein Ehrverlust, diese Woche nicht von dir angegriffen" +premiumNearby = "Mit Premium siehst du die 20 Siedlungen, die dir am nächsten liegen, mit ihrer Entfernung und dem Ehrverlust eines Angriffs." +premiumHonour = "Mit Premium siehst du, wie viel Ehre jeder Angriff kostet." +premiumBenefit = "Die 20 nächsten Siedlungen und die Ehrkosten jedes Angriffs im Kampffenster." +upgrade = "Premium holen" +upgradeShort = "Premium holen" +otherTarget = "Anderes Ziel wählen" diff --git a/resources/language/en/battle.lng b/resources/language/en/battle.lng index ef11d82..e63e788 100755 --- a/resources/language/en/battle.lng +++ b/resources/language/en/battle.lng @@ -215,3 +215,54 @@ killed = "killed @@0" loot = "@@0 res" runes = "@@0 runes" rune = "1 rune" + +[targets] +title_attack = "Attack: choose a target" +title_support = "Support: choose a village" +tab_suggested = "Suggested" +tab_nearby = "Nearby" +tab_recent = "Recent" +tab_search = "Search" +suggestedIntro = "Villages worth a look. You decide." +filter_all = "All" +filter_players = "Players" +filter_tribes = "Tribes" +closest = "@@amount closest" +village = "Village name" +search = "Search" +tribe = "Tribe" +networth = "networth @@networth" +leagues = "@@distance leagues" +honourLoss = "you lose @@honour% honour" +noHonourLoss = "no honour loss" +status_own = "Your own village." +status_clan = "Clan member: you cannot attack this village." +status_holiday = "On holiday: this village cannot be reached." +section_ownVillages = "Your villages" +section_clanVillages = "Your clan" +section_otherVillages = "Other villages nearby" +section_attackedYou = "They attacked you" +section_fought = "You attacked" +section_visited = "Profiles you looked at" +empty_suggested = "Nothing to suggest right now." +empty_nearby = "No villages found nearby." +empty_search = "I couldn't find any village with that name." +empty_ownVillages = "You have no other villages." +empty_clanVillages = "You are not in a clan, or your clan has no other villages." +empty_otherVillages = "No other villages found nearby." +empty_attackedYou = "Nobody attacked you this week." +empty_fought = "You have not attacked anyone yet." +empty_visited = "You have not looked at any village profiles yet." +attackedYou = "attacked you @@when" +youWon = "you won, @@when" +youLost = "you were repelled, @@when" +whyStrikeBack = "Strike back: they attacked you @@when" +whyClan = "They attacked your clan mate @@village (@@when)" +whyTribe = "Closest tribe: raids cost no honour" +whyPlayer = "Close by, no honour loss, not attacked by you this week" +premiumNearby = "Premium shows the 20 villages closest to you, with their distance and what an attack would cost you in honour." +premiumHonour = "Premium shows what each attack costs in honour." +premiumBenefit = "The 20 villages closest to you, and the honour cost of every attack, in the Battle window." +upgrade = "Get premium" +upgradeShort = "Get premium" +otherTarget = "Choose another target" diff --git a/resources/language/fr/battle.lng b/resources/language/fr/battle.lng index b18fa8a..6a23b5c 100644 --- a/resources/language/fr/battle.lng +++ b/resources/language/fr/battle.lng @@ -213,3 +213,54 @@ killed = "@@0 tués" loot = "@@0 ress." runes = "@@0 runes" rune = "1 rune" + +[targets] +title_attack = "Attaquer : choisissez une cible" +title_support = "Soutenir : choisissez un village" +tab_suggested = "Suggestions" +tab_nearby = "À proximité" +tab_recent = "Récents" +tab_search = "Recherche" +suggestedIntro = "Des villages qui valent le coup d'œil. À vous de décider." +filter_all = "Tous" +filter_players = "Joueurs" +filter_tribes = "Tribus" +closest = "les @@amount plus proches" +village = "Nom du village" +search = "Rechercher" +tribe = "Tribu" +networth = "points @@networth" +leagues = "@@distance lieues" +honourLoss = "vous perdez @@honour% d'honneur" +noHonourLoss = "aucune perte d'honneur" +status_own = "Votre propre village." +status_clan = "Membre du clan : vous ne pouvez pas attaquer ce village." +status_holiday = "En vacances : ce village est inaccessible." +section_ownVillages = "Vos villages" +section_clanVillages = "Votre clan" +section_otherVillages = "Autres villages à proximité" +section_attackedYou = "Ils vous ont attaqué" +section_fought = "Vous avez attaqué" +section_visited = "Profils consultés" +empty_suggested = "Rien à suggérer pour le moment." +empty_nearby = "Aucun village trouvé à proximité." +empty_search = "Je n'ai trouvé aucun village portant ce nom." +empty_ownVillages = "Vous n'avez pas d'autres villages." +empty_clanVillages = "Vous n'êtes pas dans un clan, ou votre clan n'a pas d'autres villages." +empty_otherVillages = "Aucun autre village trouvé à proximité." +empty_attackedYou = "Personne ne vous a attaqué cette semaine." +empty_fought = "Vous n'avez encore attaqué personne." +empty_visited = "Vous n'avez encore consulté aucun profil de village." +attackedYou = "vous a attaqué le @@when" +youWon = "victoire, @@when" +youLost = "repoussé, @@when" +whyStrikeBack = "Ripostez : ils vous ont attaqué le @@when" +whyClan = "Ils ont attaqué @@village, membre de votre clan (@@when)" +whyTribe = "Tribu la plus proche : les raids ne coûtent pas d'honneur" +whyPlayer = "Proche, sans perte d'honneur, pas attaqué par vous cette semaine" +premiumNearby = "Avec le premium, vous voyez les 20 villages les plus proches, avec leur distance et l'honneur qu'une attaque vous coûterait." +premiumHonour = "Avec le premium, vous voyez l'honneur que coûte chaque attaque." +premiumBenefit = "Les 20 villages les plus proches et le coût en honneur de chaque attaque, dans la fenêtre de bataille." +upgrade = "Passer au premium" +upgradeShort = "Passer au premium" +otherTarget = "Choisir une autre cible" diff --git a/resources/language/nl/battle.lng b/resources/language/nl/battle.lng index 13556dd..10a96a3 100755 --- a/resources/language/nl/battle.lng +++ b/resources/language/nl/battle.lng @@ -212,3 +212,54 @@ killed = "@@0 gedood" loot = "@@0 grondst." runes = "@@0 runen" rune = "1 rune" + +[targets] +title_attack = "Aanvallen: kies een doelwit" +title_support = "Steunen: kies een dorp" +tab_suggested = "Voorstellen" +tab_nearby = "In de buurt" +tab_recent = "Recent" +tab_search = "Zoeken" +suggestedIntro = "Dorpen die het bekijken waard zijn. Jij beslist." +filter_all = "Alle" +filter_players = "Spelers" +filter_tribes = "Stammen" +closest = "@@amount dichtstbij" +village = "Naam van het dorp" +search = "Zoeken" +tribe = "Stam" +networth = "score @@networth" +leagues = "@@distance league" +honourLoss = "je verliest @@honour% eer" +noHonourLoss = "geen eerverlies" +status_own = "Je eigen dorp." +status_clan = "Clanlid: je kan dit dorp niet aanvallen." +status_holiday = "Op vakantie: dit dorp is niet bereikbaar." +section_ownVillages = "Je dorpen" +section_clanVillages = "Je clan" +section_otherVillages = "Andere dorpen in de buurt" +section_attackedYou = "Zij vielen je aan" +section_fought = "Jij viel aan" +section_visited = "Profielen die je bekeek" +empty_suggested = "Op dit moment niets voor te stellen." +empty_nearby = "Geen dorpen in de buurt gevonden." +empty_search = "Ik vond geen dorp met die naam." +empty_ownVillages = "Je hebt geen andere dorpen." +empty_clanVillages = "Je zit niet in een clan, of je clan heeft geen andere dorpen." +empty_otherVillages = "Geen andere dorpen in de buurt gevonden." +empty_attackedYou = "Niemand viel je deze week aan." +empty_fought = "Je hebt nog niemand aangevallen." +empty_visited = "Je hebt nog geen dorpsprofielen bekeken." +attackedYou = "viel je aan op @@when" +youWon = "gewonnen, @@when" +youLost = "afgeslagen, @@when" +whyStrikeBack = "Sla terug: zij vielen je aan op @@when" +whyClan = "Zij vielen je clanlid @@village aan (@@when)" +whyTribe = "Dichtste stam: plunderen kost geen eer" +whyPlayer = "Dichtbij, geen eerverlies, deze week niet door jou aangevallen" +premiumNearby = "Met premium zie je de 20 dorpen die het dichtst bij je liggen, met hun afstand en wat een aanval je aan eer kost." +premiumHonour = "Met premium zie je wat elke aanval aan eer kost." +premiumBenefit = "De 20 dorpen het dichtst bij je en de eerkost van elke aanval, in het gevechtsvenster." +upgrade = "Neem premium" +upgradeShort = "Neem premium" +otherTarget = "Kies een ander doelwit" diff --git a/resources/language/pl/battle.lng b/resources/language/pl/battle.lng index be4cba5..8d2225a 100755 --- a/resources/language/pl/battle.lng +++ b/resources/language/pl/battle.lng @@ -211,3 +211,54 @@ killed = "zabici: @@0" loot = "surowce: @@0" runes = "runy: @@0" rune = "1 runa" + +[targets] +title_attack = "Atak: wybierz cel" +title_support = "Wsparcie: wybierz wioskę" +tab_suggested = "Propozycje" +tab_nearby = "W pobliżu" +tab_recent = "Ostatnie" +tab_search = "Szukaj" +suggestedIntro = "Wioski warte uwagi. Ty decydujesz." +filter_all = "Wszystkie" +filter_players = "Gracze" +filter_tribes = "Plemiona" +closest = "@@amount najbliższych" +village = "Nazwa wioski" +search = "Szukaj" +tribe = "Plemię" +networth = "punkty @@networth" +leagues = "@@distance mil" +honourLoss = "tracisz @@honour% honoru" +noHonourLoss = "bez utraty honoru" +status_own = "Twoja własna wioska." +status_clan = "Członek klanu: nie możesz zaatakować tej wioski." +status_holiday = "Na urlopie: tej wioski nie można osiągnąć." +section_ownVillages = "Twoje wioski" +section_clanVillages = "Twój klan" +section_otherVillages = "Inne wioski w pobliżu" +section_attackedYou = "Zaatakowali cię" +section_fought = "Zaatakowałeś" +section_visited = "Oglądane profile" +empty_suggested = "W tej chwili nie ma nic do zaproponowania." +empty_nearby = "Nie znaleziono wiosek w pobliżu." +empty_search = "Nie znalazłem wioski o tej nazwie." +empty_ownVillages = "Nie masz innych wiosek." +empty_clanVillages = "Nie jesteś w klanie albo twój klan nie ma innych wiosek." +empty_otherVillages = "Nie znaleziono innych wiosek w pobliżu." +empty_attackedYou = "Nikt cię nie zaatakował w tym tygodniu." +empty_fought = "Jeszcze nikogo nie zaatakowałeś." +empty_visited = "Nie oglądałeś jeszcze żadnych profili wiosek." +attackedYou = "zaatakował cię @@when" +youWon = "zwycięstwo, @@when" +youLost = "odparty, @@when" +whyStrikeBack = "Odwet: zaatakowali cię @@when" +whyClan = "Zaatakowali członka twojego klanu, @@village (@@when)" +whyTribe = "Najbliższe plemię: najazdy nie kosztują honoru" +whyPlayer = "Blisko, bez utraty honoru, nie atakowany przez ciebie w tym tygodniu" +premiumNearby = "Z kontem premium widzisz 20 najbliższych wiosek, ich odległość i ile honoru kosztowałby atak." +premiumHonour = "Z kontem premium widzisz, ile honoru kosztuje każdy atak." +premiumBenefit = "20 najbliższych wiosek i koszt honoru każdego ataku w oknie bitwy." +upgrade = "Kup premium" +upgradeShort = "Kup premium" +otherTarget = "Wybierz inny cel" diff --git a/resources/templates/default/account/benefits.phpt b/resources/templates/default/account/benefits.phpt index 2afefc8..da07db1 100755 --- a/resources/templates/default/account/benefits.phpt +++ b/resources/templates/default/account/benefits.phpt @@ -7,4 +7,5 @@
' . $this->getText ($key) . '
' + . '‹ =$this->getText ('allBattles', 'report2')?>
+ +=$this->getText ('suggestedIntro')?>
+ + + + + + + + + ++ + · + + =$this->getText ('filter_' . $f)?> + + =$this->getText ('filter_' . $f)?> + + + =$this->putIntoText ($this->getText ('closest'), array ('amount' => $nearbyCount))?> +
+ + + + =$upgrade ('premiumNearby')?> + 0) { ?> + + +=$this->getText ('empty_' . ($s['title'] ? $s['title'] : $tab))?>
+ + + + + +=$this->getText ('premiumHonour')?> =$this->getText ('upgradeShort')?>
+ + +'.$text->get ('vacationMode', 'main', 'main').'
'; } - elseif (isset ($data['target'])) + // Back to the list, from any step: forget the target the window remembers. + elseif (isset ($input['overview']) && !isset ($input['report'])) { $this->setAjaxPollSeconds (0); - //$this->updateRequestData (array ('vid' => $this->village->getId ())); - return $this->getChallenge ($data['target']); + $this->updateRequestData (array ('vid' => $this->village->getId ())); + return $this->getBattleOverview (); } - - elseif (isset ($input['action'])) + + // Choose a (new) target. Picking one sends the target along, which goes to the next step. + elseif (isset ($input['action']) && !isset ($input['target'])) { $this->setAjaxPollSeconds (0); - //$this->updateRequestData (array ('vid' => $this->village->getId ())); + $this->updateRequestData (array ('vid' => $this->village->getId ())); return $this->getChooseTarget (); } + + elseif (isset ($data['target'])) + { + $this->setAjaxPollSeconds (0); + return $this->getChallenge ($data['target']); + } elseif (isset ($input['report'])) { @@ -121,12 +129,108 @@ public function getContent () } } + /* + Choose a target for an attack or support: tabs Suggested (attacks only), Nearby, Recent and Search. + Nearby and the honour cost are premium (Dolumar_Battle_TargetList::canSeeNeighbourhood). + */ private function getChooseTarget () { - $structure = new Neuron_Structure_ChooseTarget ($this->getInputData (), $this->village, false, true); - return $structure->getHTML (); + $input = $this->getInputData (); + $mode = isset ($input['command']) && $input['command'] == 'support' ? 'support' : 'attack'; + + $premium = Dolumar_Battle_TargetList::canSeeNeighbourhood ($this->village->getOwner ()); + $list = new Dolumar_Battle_TargetList ($this->village, $mode, $premium); + + $suggestions = $list->getSuggestions (); + + $tabs = array (); + if (count ($suggestions) > 0) + { + $tabs[] = 'suggested'; + } + $tabs[] = 'nearby'; + $tabs[] = 'recent'; + $tabs[] = 'search'; + + $query = isset ($input['sVillageName']) ? (string) $input['sVillageName'] : ''; + + // Free players start on Recent rather than on the locked Nearby tab. + $default = $tabs[0] === 'nearby' && !$premium ? 'recent' : $tabs[0]; + $tab = isset ($input['tab']) && in_array ($input['tab'], $tabs) ? $input['tab'] : $default; + if ($query !== '') + { + $tab = 'search'; + } + + $filter = isset ($input['filter']) && in_array ($input['filter'], array ('players', 'tribes')) ? $input['filter'] : 'all'; + + $page = new Neuron_Core_Template (); + $page->setTextSection ('targets', 'battle'); + + $page->set ('mode', $mode); + $page->set ('tabs', $tabs); + $page->set ('tab', $tab); + $page->set ('filter', $filter); + $page->set ('premium', $premium); + $page->set ('query', Neuron_Core_Tools::output_varchar ($query)); + $page->set ('nearbyCount', Dolumar_Battle_TargetList::NEARBY); + + $sections = array (); + switch ($tab) + { + case 'suggested': + $sections[] = array ('title' => null, 'rows' => $suggestions); + break; + + case 'nearby': + if ($mode === 'support') + { + $sections[] = array ('title' => 'ownVillages', 'rows' => $list->getOwnVillages ()); + $sections[] = array ('title' => 'clanVillages', 'rows' => $list->getClanVillages ()); + $sections[] = array ('title' => 'otherVillages', 'rows' => $list->getNearby ($filter), 'nearby' => true); + } + else + { + $sections[] = array ('title' => null, 'rows' => $list->getNearby ($filter), 'nearby' => true); + } + break; + + case 'recent': + if ($mode === 'attack') + { + $sections[] = array ('title' => 'attackedYou', 'rows' => $list->getAttackers ()); + } + $sections[] = array ('title' => 'fought', 'rows' => $list->getFought ()); + $sections[] = array ('title' => 'visited', 'rows' => $list->getVisited ()); + break; + + case 'search': + $sections[] = array ('title' => null, 'rows' => $list->search ($query), 'search' => $query !== ''); + break; + } + + $page->set ('sections', $sections); + + return $page->parse ('battle/chooseTarget.phpt'); } - + + /* + The way back from every step after a target was picked: choose another target, or the battle list. + */ + private function getBackLinks ($action = null) + { + if ($action === null) + { + $data = $this->getInputData (); + $action = isset ($data['command']) && $data['command'] == 'support' ? 'support' : 'attack'; + } + + $page = new Neuron_Core_Template (); + $page->setTextSection ('targets', 'battle'); + $page->set ('action', $action); + return $page->parse ('battle/backLinks.phpt'); + } + private function getChallenge ($targetId, $error = null) { $text = Neuron_Core_Text::__getInstance (); @@ -168,22 +272,22 @@ private function getChallenge ($targetId, $error = null) ) { // User is currently in vacation mode - return ''.$text->get ('vacationMode', 'challenge', 'battle').'
'; + return $this->getBackLinks () . ''.$text->get ('vacationMode', 'challenge', 'battle').'
'; } elseif ($target->isFound () && $target->isActive () && $distance !== false) { - return ''.$text->get ('ownVillage', 'challenge', 'battle').'
'; + return $this->getBackLinks () . ''.$text->get ('ownVillage', 'challenge', 'battle').'
'; } elseif ($distance === false) { - return ''.$text->get ('unreachable', 'challenge', 'battle').'
'; + return $this->getBackLinks () . ''.$text->get ('unreachable', 'challenge', 'battle').'
'; } else { - return ''.$text->get ('teamNotFound', 'challenge', 'battle').'
'; + return $this->getBackLinks () . ''.$text->get ('teamNotFound', 'challenge', 'battle').'
'; } } @@ -218,7 +322,7 @@ private function getAttackVillage ($target) if ($this->village->attackVillage ($target, $squads, $specials)) { reloadStatusCounters (); - return (''.$text->get ('bdone', 'challenge', 'battle').'
'); + return $this->getBackLinks ('attack') . ''.$text->get ('bdone', 'challenge', 'battle').'
'; } else { @@ -264,6 +368,7 @@ private function getSendSupport ($target) $page = new Neuron_Core_Template (); $page->set ('target', Neuron_Core_Tools::output_varchar ($target->getName ())); $page->set ('action', 'support'); + $page->set ('backLinks', $this->getBackLinks ('support')); $input = $this->getInputData (); @@ -333,6 +438,7 @@ private function getChooseUnits ($target, $error = null) $page->set ('target', Neuron_Core_Tools::output_varchar ($target->getName ())); $page->set ('action', 'attack'); $page->set ('target_id', $target->getId ()); + $page->set ('backLinks', $this->getBackLinks ('attack')); $showForm = true; @@ -426,6 +532,7 @@ private function getChooseSpecialUnits ($target, $squads, $error = null) $page->setTextSection ('specialUnits', 'battle'); $page->set ('error', $error); + $page->set ('backLinks', $this->getBackLinks ('attack')); $page->set ('target', Neuron_Core_Tools::output_varchar ($target->getName ())); $page->set ('targetId', $target->getId ()); @@ -533,7 +640,7 @@ private function processChallengeInput ($target) } else { - $this->updateContent (''.$text->get ('teamNotFound').'
'); + $this->updateContent ($this->getBackLinks () . ''.$text->get ('teamNotFound').'
'); } } diff --git a/tests/Battle/TargetListTest.php b/tests/Battle/TargetListTest.php new file mode 100644 index 0000000..1e39908 --- /dev/null +++ b/tests/Battle/TargetListTest.php @@ -0,0 +1,17 @@ +assertSame ('attack', $list->getMode ()); + $this->assertSame ('support', (new Dolumar_Battle_TargetList (null, 'support', false, 0))->getMode ()); + } +} diff --git a/tests/Integration/BattleTargetListTest.php b/tests/Integration/BattleTargetListTest.php new file mode 100644 index 0000000..9f8c248 --- /dev/null +++ b/tests/Integration/BattleTargetListTest.php @@ -0,0 +1,129 @@ +query (" + SELECT v.vid FROM villages v + INNER JOIN map_buildings b ON b.village = v.vid AND b.buildingType = 1 AND b.destroyDate = 0 + WHERE v.isActive = '1' AND v.plid > 0 + ORDER BY v.vid LIMIT 1 + "); + $this->assertNotEmpty ($rows, 'Needs an active player village'); + $this->me = Dolumar_Players_Village::getVillage ((int) $rows[0]['vid']); + + $rows = $db->query (" + SELECT v.vid FROM villages v + INNER JOIN map_buildings b ON b.village = v.vid AND b.buildingType = 1 AND b.destroyDate = 0 + WHERE v.isActive = '1' AND v.plid = 0 + ORDER BY v.vid LIMIT 1 + "); + if (count ($rows) === 0) + { + $this->markTestSkipped ('Needs an active tribe village'); + } + $this->tribe = Dolumar_Players_Village::getVillage ((int) $rows[0]['vid']); + } + + protected function tearDown (): void + { + if ($this->reportId) + { + Neuron_DB_Database::getInstance ()->query ("DELETE FROM battle_report WHERE reportId = " . (int) $this->reportId); + } + } + + public function testNearbyNeedsPremium (): void + { + $list = new Dolumar_Battle_TargetList ($this->me, 'attack', false); + $this->assertSame (array (), $list->getNearby ()); + } + + public function testNearbyIsClosestFirstAndAtMostTwenty (): void + { + $list = new Dolumar_Battle_TargetList ($this->me, 'attack', true); + $rows = $list->getNearby (); + + $this->assertNotEmpty ($rows); + $this->assertLessThanOrEqual (Dolumar_Battle_TargetList::NEARBY, count ($rows)); + + $last = -1; + foreach ($rows as $r) + { + $this->assertNotSame ((int) $this->me->getId (), $r['id']); + $this->assertGreaterThanOrEqual ($last, $r['distance']); + $last = $r['distance']; + } + + foreach ($list->getNearby ('tribes') as $r) + { + $this->assertTrue ($r['npc']); + $this->assertSame (0, $r['honour'], 'Raids on tribes cost no honour'); + } + } + + public function testHonourIsHiddenWithoutPremium (): void + { + $list = new Dolumar_Battle_TargetList ($this->me, 'attack', false); + $row = $list->getRow ($this->tribe); + $this->assertNull ($row['honour']); + $this->assertTrue ($row['canPick']); + } + + public function testAttackerIsListedAndSuggested (): void + { + $now = time (); + $this->reportId = Neuron_DB_Database::getInstance ()->query (" + INSERT INTO battle_report SET battleId = 0, fightDate = " . ($now - 3600) . ", fightDuration = 0, + fromId = " . (int) $this->tribe->getId () . ", targetId = " . (int) $this->me->getId () . ", + squads = '', slots = '', fightLog = '', battleLog = '', resultLog = '', victory = 0.5, execDate = NOW() + "); + + $list = new Dolumar_Battle_TargetList ($this->me, 'attack', false, $now); + + $ids = array_column ($list->getAttackers (), 'id'); + $this->assertContains ((int) $this->tribe->getId (), $ids); + + $suggestions = $list->getSuggestions (); + $this->assertNotEmpty ($suggestions); + $this->assertSame ((int) $this->tribe->getId (), $suggestions[0]['id']); + + // Support never suggests. + $support = new Dolumar_Battle_TargetList ($this->me, 'support', false, $now); + $this->assertSame (array (), $support->getSuggestions ()); + } + + public function testDistanceIsInLeagues (): void + { + $list = new Dolumar_Battle_TargetList ($this->me, 'attack', false); + $this->assertSame ('0.5', $list->formatLeagues (5)); + $this->assertSame ('8.9', $list->formatLeagues (88.6)); + $this->assertSame ('14', $list->formatLeagues (140.2)); + } + + public function testSearchFindsVillageByPartOfItsName (): void + { + $list = new Dolumar_Battle_TargetList ($this->me, 'attack', false); + $name = $this->tribe->getName (); + $ids = array_column ($list->search (substr ($name, 0, 6)), 'id'); + $this->assertContains ((int) $this->tribe->getId (), $ids); + $this->assertSame (array (), $list->search (' ')); + } +}