Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .ai/superpowers/specs/2026-11-05-battle-target-picker.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions public/dolumar/css/battlereport.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
51 changes: 51 additions & 0 deletions resources/language/de/battle.lng
Original file line number Diff line number Diff line change
Expand Up @@ -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"
51 changes: 51 additions & 0 deletions resources/language/en/battle.lng
Original file line number Diff line number Diff line change
Expand Up @@ -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"
51 changes: 51 additions & 0 deletions resources/language/fr/battle.lng
Original file line number Diff line number Diff line change
Expand Up @@ -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"
51 changes: 51 additions & 0 deletions resources/language/nl/battle.lng
Original file line number Diff line number Diff line change
Expand Up @@ -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"
51 changes: 51 additions & 0 deletions resources/language/pl/battle.lng
Original file line number Diff line number Diff line change
Expand Up @@ -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"
1 change: 1 addition & 0 deletions resources/templates/default/account/benefits.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
<li><?=$this->getText ('tqueue'); ?></li>
<li><?=$this->getText ('uinfo'); ?></li>
<li><?=$this->getText ('searchtool'); ?></li>
<li><?=$this->getText ('premiumBenefit', 'targets', 'battle'); ?></li>
</ul>
9 changes: 9 additions & 0 deletions resources/templates/default/battle/backLinks.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
/* The way back from every step after a target was picked (Dolumar_Windows_Battle::getBackLinks). */
$again = htmlspecialchars (json_encode (array ('action' => $action, 'command' => $action)), ENT_QUOTES);
?>
<div class="battle-report"><p class="br-back">
<a href="javascript:void(0);" onclick="windowAction(this, <?=$again?>);">‹ <?=$this->getText ('otherTarget')?></a>
<span class="ui-muted">·</span>
<a href="javascript:void(0);" onclick="windowAction(this, {'overview':'true'});"><?=$this->getText ('allBattles', 'report2')?></a>
</p></div>
Loading
Loading