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
9 changes: 7 additions & 2 deletions .ai/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,14 @@ Under `src/Dolumar/`:
20 s memcache of the rows), `ArmyFactory` (turns a row into a `Dolumar_NewMap_Army` with one engine `Movement` per
phase: march out, wait at the target, march home; portal routes jump), and
`MapObjectManager::getMovingObjects()`, which the engine's `map/objects` endpoint calls. Routes are stored at
dispatch time in `battle.route` and `squad_commands.sc_route` (JSON waypoints from `Dolumar_Map_Distance`).
dispatch time in `battle.route`, `squad_commands.sc_route` and `villages_specialunits.vsu_route` (JSON legs from
`Dolumar_Map_Distance`: a list of point lists, a portal jump between legs; older rows hold a flat point list and
are still read). Walking goes around lakes: `Map_LandRoute` keeps the straight line and only searches a way
around each stretch that crosses water (grid A* in a box around it), smooths the result, caches it in memcache
and falls back to the straight line when there is no way around. Ranking and profile lists pass
`$overLand = false` for a cheap straight-line distance.
Related viewers (own or clan villages involved) get the full path and a tap action; everyone else sees a neutral
sprite with only the next 30 s of movement. The kind texts come from `resources/language/*/map.lng` through
sprite with only the next 30 s of movement. Thieves are secret: only their owner and clan see them at all. The kind texts come from `resources/language/*/map.lng` through
`CONFIG_MAPOBJECT_TEXT` in `gameserver/index/header.phpt`; the engine layer itself is generic (`Game.mapObjects`, `mapobjects.js`).
- `Underworld/`: a separate, turn-based mini-game with its own models, mappers and windows.
- `Mappers/` and `Underworld/Mappers/`: the few classes that follow a mapper pattern. Most models query the database
Expand Down
2 changes: 2 additions & 0 deletions dump/dump.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1548,6 +1548,8 @@ CREATE TABLE `villages_specialunits` (
`vsu_location` int(11) DEFAULT NULL,
`vsu_moveStart` datetime DEFAULT NULL,
`vsu_moveEnd` datetime DEFAULT NULL,
`vsu_from` int(11) DEFAULT NULL,
`vsu_route` text DEFAULT NULL,
PRIMARY KEY (`vsu_id`),
KEY `v_id` (`v_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1638 DEFAULT CHARSET=utf8;
Expand Down
17 changes: 17 additions & 0 deletions dump/upgrades/20261105-thief-routes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- Thieves on the map: where a moving thief left from and its route around lakes (JSON legs, like battle.route),
-- stored when it is sent so the map can draw it. Thieves sent before this have no vsu_from and are not drawn.
-- Runs on every deploy (tools/setup.php), so each ALTER only runs when the column is still missing.

SET @dolumar_sql = (SELECT IF (COUNT(*) = 0, 'ALTER TABLE `villages_specialunits` ADD COLUMN `vsu_from` int(11) DEFAULT NULL', 'DO 0')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'villages_specialunits' AND COLUMN_NAME = 'vsu_from');
PREPARE dolumar_stmt FROM @dolumar_sql;
EXECUTE dolumar_stmt;
DEALLOCATE PREPARE dolumar_stmt;

SET @dolumar_sql = (SELECT IF (COUNT(*) = 0, 'ALTER TABLE `villages_specialunits` ADD COLUMN `vsu_route` text DEFAULT NULL', 'DO 0')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'villages_specialunits' AND COLUMN_NAME = 'vsu_route');
PREPARE dolumar_stmt FROM @dolumar_sql;
EXECUTE dolumar_stmt;
DEALLOCATE PREPARE dolumar_stmt;
Binary file added public/static/images/sprites/armies/thief.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/language/de/map.lng
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ attack = "Angreifende Armee"
waiting = "Armee vor den Toren"
returning = "Zurückkehrende Armee"
squad = "Regiment unterwegs"
thief = "Dieb unterwegs"
neutral = "Eine Armee"
open = "Öffnen"
close = "Schließen"
1 change: 1 addition & 0 deletions resources/language/en/map.lng
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ attack = "Attacking army"
waiting = "Army at the gates"
returning = "Returning army"
squad = "Regiment on the move"
thief = "Thief on the move"
neutral = "An army"
open = "Open"
close = "Close"
1 change: 1 addition & 0 deletions resources/language/fr/map.lng
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ attack = "Armée en marche"
waiting = "Armée aux portes"
returning = "Armée de retour"
squad = "Régiment en déplacement"
thief = "Voleur en route"
neutral = "Une armée"
open = "Ouvrir"
close = "Fermer"
2 changes: 1 addition & 1 deletion resources/templates/default/gameserver/index/header.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// Kind texts for the engine map-objects layer (javascript/mapobjects.js reads CONFIG_MAPOBJECT_TEXT)
$armyText = Neuron_Core_Text::__getInstance ();
$armyTexts = array ();
foreach (array ('attack', 'waiting', 'returning', 'squad', 'neutral', 'open', 'close') as $armyKind)
foreach (array ('attack', 'waiting', 'returning', 'squad', 'thief', 'neutral', 'open', 'close') as $armyKind)
{
$armyTexts[$armyKind] = $armyText->get ($armyKind, 'armies', 'map');
}
Expand Down
91 changes: 65 additions & 26 deletions src/Dolumar/Map/Distance.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ class Dolumar_Map_Distance
/*
Distance only; see getDistanceAndRoute for the waypoints.
*/
public static function getDistance ($loc1, $loc2, $portals, $ignoreImpassable = true)
public static function getDistance ($loc1, $loc2, $portals, $ignoreImpassable = true, $overLand = true)
{
$result = self::getDistanceAndRoute ($loc1, $loc2, $portals, $ignoreImpassable);
$result = self::getDistanceAndRoute ($loc1, $loc2, $portals, $ignoreImpassable, $overLand);
if ($result === false)
{
return false;
Expand All @@ -39,15 +39,17 @@ public static function getDistance ($loc1, $loc2, $portals, $ignoreImpassable =
}

/*
Returns array ('distance' => float, 'route' => array of array (x, y))
from $loc1 to $loc2 inclusive, with portal jumps as consecutive
waypoints, or false when no route exists.
Returns array ('distance' => float, 'route' => legs) or false when no
route exists. The route is a list of legs from $loc1 to $loc2, each leg a
list of array (x, y) walked point to point; between two legs is a portal
jump. With $overLand, walking goes around lakes (Dolumar_Map_LandRoute);
without, every walk is a straight line (cheap, for lists of distances).
*/
public static function getDistanceAndRoute ($loc1, $loc2, $portals, $ignoreImpassable = true)
public static function getDistanceAndRoute ($loc1, $loc2, $portals, $ignoreImpassable = true, $overLand = true)
{
if ($loc1[0] == $loc2[0] && $loc1[1] == $loc2[1])
{
return array ('distance' => 0, 'route' => array (array ($loc1[0], $loc1[1])));
return array ('distance' => 0, 'route' => array (array (array ($loc1[0], $loc1[1]))));
}

// Show the route
Expand Down Expand Up @@ -75,7 +77,7 @@ public static function getDistanceAndRoute ($loc1, $loc2, $portals, $ignoreImpas
}
$profiler->stop ();

$distance = self::getShortestDistance ($loc1, $loc2, $portals, $ignoreImpassable);
$distance = self::getShortestDistance ($loc1, $loc2, $portals, $ignoreImpassable, $overLand);

if ($distance === false)
{
Expand All @@ -93,11 +95,30 @@ public static function getDistanceAndRoute ($loc1, $loc2, $portals, $ignoreImpas

$profiler->stop ();

// getRoute() builds the route destination-first with cost keys; flip it.
$route = array ();
foreach (array_reverse ($distance[1]) as $v)
// getRoute() builds the route destination-first; flip it and fill in
// the walked legs. 'jump' marks a node reached through a portal.
$nodes = array_reverse ($distance[1]);
$route = array (array (array ($nodes[0][0], $nodes[0][1])));
for ($i = 1; $i < count ($nodes); $i ++)
{
$route[] = array ($v[0], $v[1]);
$here = array ($nodes[$i][0], $nodes[$i][1]);

if (!empty ($nodes[$i]['jump']))
{
$route[] = array ($here);
continue;
}

$walk = $overLand
? Dolumar_Map_LandRoute::walk (array ($nodes[$i - 1][0], $nodes[$i - 1][1]), $here)
: array ('points' => array (array ($nodes[$i - 1][0], $nodes[$i - 1][1]), $here));

$leg = count ($route) - 1;
$points = $walk['points'];
for ($j = 1; $j < count ($points); $j ++)
{
$route[$leg][] = array ($points[$j][0], $points[$j][1]);
}
}

return array ('distance' => $distance[0], 'route' => $route);
Expand All @@ -108,9 +129,10 @@ public static function getDistanceAndRoute ($loc1, $loc2, $portals, $ignoreImpas
When max_distance is reached, skip any other calculations.
We are only interested in the shortest route.
*/
private static function getShortestDistance ($start, $destination, $portals, $ignoreIslands)
private static function getShortestDistance ($start, $destination, $portals, $ignoreIslands, $overLand)
{
$came_from = array ();
$jump = array ();

$g = array ();
$h = array ();
Expand Down Expand Up @@ -159,8 +181,9 @@ private static function getShortestDistance ($start, $destination, $portals, $ig
{
$route = array ($destination);
$route[0]['d'] = 0;
$route[0]['jump'] = !empty ($jump[$fk]);

self::getRoute ($came_from, $fk, $route);
self::getRoute ($came_from, $jump, $fk, $route);

return array ($g[$fk], $route);
}
Expand All @@ -171,7 +194,7 @@ private static function getShortestDistance ($start, $destination, $portals, $ig
unset ($open[$fk]);
$closed[$fk] = $x;

$neighbours = self::getNeighbourNodes ($x, $destination, $portals, $ignoreIslands);
$neighbours = self::getNeighbourNodes ($x, $destination, $portals, $ignoreIslands, $overLand);

foreach ($neighbours as $k => $v)
{
Expand All @@ -196,6 +219,7 @@ private static function getShortestDistance ($start, $destination, $portals, $ig

$came_from[$k] = $x;
$came_from[$k]['d'] = $v['cost'];
$jump[$k] = !empty ($v['jump']);
}
}
}
Expand All @@ -219,12 +243,15 @@ private static function getShortestDistance ($start, $destination, $portals, $ig
return false;
}

private static function getRoute ($came_from, $k, &$array)
private static function getRoute ($came_from, $jump, $k, &$array)
{
if (isset ($came_from[$k]))
{
$array[] = $came_from[$k];
self::getRoute ($came_from, self::getKeyName ($came_from[$k]), $array);
$node = $came_from[$k];
$nk = self::getKeyName ($node);
$node['jump'] = !empty ($jump[$nk]);
$array[] = $node;
self::getRoute ($came_from, $jump, $nk, $array);
}
return;
}
Expand All @@ -238,7 +265,7 @@ private static function getKeyName ($loc)
Return an array with all the neighbour locations
(output = array (array (x, y), distance))
*/
private static function getNeighbourNodes ($loc, $des, $portals, $ignoreIslands)
private static function getNeighbourNodes ($loc, $des, $portals, $ignoreIslands, $overLand)
{
$out = array ();

Expand All @@ -248,15 +275,15 @@ private static function getNeighbourNodes ($loc, $des, $portals, $ignoreIslands)
$out[self::getKeyName ($des)] = array
(
'key' => $des,
'cost' => self::calculateDistance ($loc[0], $loc[1], $des[0], $des[1]),
'cost' => self::walkingDistance ($loc, $des, $overLand),
/* self::calculateDistance ($loc[0], $loc[1], $des[0], $des[1]) */
'heuristics' => self::calculateDistance ($loc[0], $loc[1], $des[0], $des[1])
);
}

foreach ($portals as $v)
{
self::addPortalNodes ($out, $v, $loc, $des);
self::addPortalNodes ($out, $v, $loc, $des, $overLand);
}

return $out;
Expand All @@ -274,7 +301,7 @@ private static function getHeuristics ($loc1, $loc2, $isPortal = false)
return $h;
}

private static function addPortalNodes (&$out, $portal, $loc, $des)
private static function addPortalNodes (&$out, $portal, $loc, $des, $overLand)
{
$loc1 = $portal->getCasterLocation ();
$loc2 = $portal->getTargetLocation ();
Expand All @@ -295,7 +322,8 @@ private static function addPortalNodes (&$out, $portal, $loc, $des)
(
'key' => $loc2,
'cost' => $portal->getDistancePenalty (),
'heuristics' => self::getHeuristics ($loc2, $des, true)
'heuristics' => self::getHeuristics ($loc2, $des, true),
'jump' => true
);
}

Expand All @@ -305,7 +333,7 @@ private static function addPortalNodes (&$out, $portal, $loc, $des)
$out[self::getKeyName ($loc1)] = array
(
'key' => $loc1,
'cost' => self::calculateDistance ($loc1[0], $loc1[1], $loc[0], $loc[1]),
'cost' => self::walkingDistance ($loc, $loc1, $overLand),
'heuristics' => self::getHeuristics ($loc1, $des, true)
);
}
Expand All @@ -317,7 +345,8 @@ private static function addPortalNodes (&$out, $portal, $loc, $des)
(
'key' => $loc1,
'cost' => $portal->getDistancePenalty (),
'heuristics' => self::getHeuristics ($loc1, $des, true)
'heuristics' => self::getHeuristics ($loc1, $des, true),
'jump' => true
);
}

Expand All @@ -327,12 +356,22 @@ private static function addPortalNodes (&$out, $portal, $loc, $des)
$out[self::getKeyName ($loc2)] = array
(
'key' => $loc2,
'cost' => self::calculateDistance ($loc2[0], $loc2[1], $loc[0], $loc[1]),
'cost' => self::walkingDistance ($loc, $loc2, $overLand),
'heuristics' => self::getHeuristics ($loc2, $des, true)
);
}
}

private static function walkingDistance ($from, $to, $overLand)
{
if (!$overLand)
{
return self::calculateDistance ($from[0], $from[1], $to[0], $to[1]);
}
$walk = Dolumar_Map_LandRoute::walk (array ($from[0], $from[1]), array ($to[0], $to[1]));
return $walk['distance'];
}

public static function calculateDistance ($x1, $y1, $x2, $y2)
{
return sqrt (pow ($x1 - $x2, 2) + pow ($y1 - $y2, 2) );
Expand Down
Loading
Loading