Files
Battle-for-Tismo/shared/game_state/movement.js
T

636 lines
27 KiB
JavaScript

// Movement: translating orders into paths, advancing units along them each
// tick, and the travel-cost model the browser mirrors for prediction.
import { key, mapToLocal, localToMap, flightLine } from "../hex.js";
import { TRANSPORT_BY_ID } from "../data/improvements.js";
import {
MIN_STEP_COST,
DEFAULT_UNIT_SPEED,
CONTROLLED_SPEED_MULTIPLIER,
} from "./constants.js";
export const movementMethods = {
requestMove(unitId, goal) {
return this.requestGroupMove([unitId], goal);
},
// Orders one or more units onto `goal`. Friendly units may stack there; a
// hostile military unit turns the move into an attack. Units moving together
// adopt the speed of the slowest member, so a stack holds formation.
//
// `speed` (optional) lets the caller pin that formation pace. The server
// splits a big group move across ticks and passes the pace it computed for
// the whole group, so every slice still moves at the slowest member's speed
// even when the slow unit lands in a later slice.
requestGroupMove(unitIds, goal, speed = null) {
const units = this._moveableUnits(unitIds);
if (units.length === 0) return false;
const pace = speed !== null && speed !== undefined
? speed
: (units.length > 1 ? Math.min(...units.map((unit) => this._unitSpeed(unit))) : null);
// Units of the same type starting from the same tile share a route, so the
// search runs once per type instead of once per unit in the stack.
const pathCache = new Map();
let moved = 0;
for (const unit of units) {
// Only a unit that actually receives the new route drops its scheduled
// stops; a unit whose route cannot be found keeps its existing movement
// rather than being silently emptied out.
if (this._orderMove(unit, goal, pace, pathCache)) {
unit.waypoints = [];
moved += 1;
}
}
if (moved === 0) return false;
this._emitChanged();
return true;
},
// Explicit attack on the hostile force holding `goal`: an enemy unit stack or
// an enemy city. The attack is a march to the tile; combat is joined on
// arrival. If the enemy has moved away by then, the order is simply a move.
requestAttack(unitIds, goal) {
const units = this._moveableUnits(unitIds).filter((unit) => this._isMilitary(unit));
if (units.length === 0) return false;
const civ = units[0].civ;
const enemyMilitary = this.enemyUnitsAt(goal, civ).some((other) => this._isMilitary(other));
const city = this.cityAt(goal);
const enemyCity = !!city && city.civ !== civ;
if (!enemyMilitary && !enemyCity) return false;
return this.requestGroupMove(units.map((unit) => unit.id), goal);
},
// Orders units onto one or more destinations in turn. With `append`, the new
// destinations join the queue already scheduled for each unit; otherwise they
// replace it. When a unit reaches one destination a fresh path is computed to
// the next, so the route adapts to a changing world.
requestScheduledMove(unitIds, goals, append = false) {
const destinations = (Array.isArray(goals) ? goals : [goals])
.filter((goal) => goal && Number.isFinite(goal.x) && Number.isFinite(goal.y))
.map((goal) => ({ x: goal.x, y: goal.y }));
if (destinations.length === 0) return false;
const units = this._moveableUnits(unitIds);
if (units.length === 0) return false;
const speed = units.length > 1
? Math.min(...units.map((unit) => this._unitSpeed(unit)))
: null;
const pathCache = new Map();
let moved = 0;
for (const unit of units) {
if (!Array.isArray(unit.waypoints)) unit.waypoints = [];
if (append) {
for (const goal of destinations) unit.waypoints.push({ ...goal });
if (this._hasRoute(unit) || this._followNextWaypoint(unit, speed, pathCache)) moved += 1;
} else {
const previous = unit.waypoints;
unit.waypoints = destinations.slice(1).map((goal) => ({ ...goal }));
if (this._orderMove(unit, destinations[0], speed, pathCache)) moved += 1;
// An unroutable first leg leaves the unit exactly as it was.
else unit.waypoints = previous;
}
}
if (moved === 0) return false;
this._emitChanged();
return true;
},
_moveableUnits(unitIds) {
const ids = Array.isArray(unitIds) ? unitIds : [unitIds];
const units = [];
for (const id of ids) {
const unit = this.findUnit(Number(id));
if (!unit) continue;
const proto = this.unitProto(unit);
if (!proto || !proto.moveable) continue;
units.push(unit);
}
return units;
},
// Whether the unit still has an unfinished route to its current destination.
_hasRoute(unit) {
return !!unit.path && unit.path.length > 1 && unit.pathIndex < unit.path.length - 1;
},
// Pops the next scheduled destination and routes to it, keeping the rest of
// the queue. Returns false when nothing is left or the next destination is
// unreachable (in which case the whole schedule is abandoned).
_followNextWaypoint(unit, speed = null, pathCache = null) {
if (!unit.waypoints || unit.waypoints.length === 0) return false;
const next = unit.waypoints.shift();
const pace = speed !== null ? speed : unit.moveSpeed;
if (!this._orderMove(unit, next, pace, pathCache)) {
this._stopUnit(unit);
return false;
}
return true;
},
_orderMove(unit, goal, speed, pathCache = null) {
// Attacking: a hostile military unit standing on the goal. Combat itself is
// joined on arrival; this only decides whether the march declares war.
const target = this.enemyUnitsAt(goal, unit.civ).find((other) => this._isMilitary(other)) || null;
const attacking = !!target && this._isMilitary(unit);
const proto = this.unitProto(unit);
const air = this._isAirProto(proto);
const path = this.findPath(unit, unit.coords, goal, pathCache);
if (path.length < 2) {
return false;
}
// Aircraft may only be based at a friendly airport: they cannot be sent to
// an empty tile or to an allied city without a runway. Rebasing is a ferry
// flight, whose only bound is the fuel in the tanks (the mission radius
// limits combat sorties, not ferrying). A strike is issued through
// requestAirStrike, which routes the plane to the enemy instead.
if (air) {
const city = this.cityAt(goal);
const airport = !!(city && city.civ === unit.civ && this.hasCityBuilding(city, "airport"));
if (!airport) return false;
const fuel = unit.airHours === undefined ? proto.enduranceHours : unit.airHours;
if (this._pathHours(unit, path) > fuel + 1e-9) return false;
}
// Marching a military unit into, or attacking, a country we are at peace
// with declares war on it. The browser confirms the move first, but the
// server enforces the rule so the two can never disagree. Aircraft fly
// over foreign land without provoking anyone; only a strike declares war.
if (this._isMilitary(unit)) {
if (attacking) this._declareWar(unit.civ, target.civ);
if (!air) {
for (const coords of path) {
const owner = this.civAt(coords);
// Crossing an ally's or an at-war nation's land is legal and declares
// nothing; only a neutral third party's land turns the march into war.
if (!this._canTraverseTerritory(unit.civ, owner, -1)) {
this._declareWar(unit.civ, owner);
}
}
}
}
unit.path = path;
unit.pathIndex = 0;
unit.progressHours = 0;
unit.moveSpeed = speed;
unit.autoReturn = false;
// A move cancels any standing bombardment; the battery is relocating.
unit.strikeTarget = null;
return true;
},
advanceMovement(hours) {
if (hours <= 0) return;
// Units sharing a tile with a hostile military unit are locked in place
// until the daily combat round resolves, so neither side can slip away
// mid-fight. The set grows as units charge in so an ordered swap cannot let
// two enemies march through each other.
const engaged = this._engagedUnits();
// Iterate a copy: air defense can shoot a plane down inside this loop, and
// splicing the live array would skip the unit that follows it.
for (const unit of this.units.slice()) {
if (!this.findUnit(unit.id)) continue;
if (!this._hasRoute(unit) && (!unit.waypoints || unit.waypoints.length === 0)) continue;
// Aircraft and launched missiles fly straight lines in pixel space; the
// tile walker below cannot advance them.
if (this._isFlier(unit)) {
this._advanceFlier(unit, hours);
continue;
}
if (engaged.has(unit)) continue;
let hoursLeft = hours;
while (hoursLeft > 0) {
// Reaching the end of one leg picks up the next scheduled destination
// and recomputes the path from where the unit now stands.
if (!this._hasRoute(unit) && !this._followNextWaypoint(unit)) break;
const next = unit.path[unit.pathIndex + 1];
const occupants = this.unitsAt(next).filter((other) => other !== unit);
const enemy = occupants.find((other) => other.civ !== unit.civ);
if (enemy && !this._isBlockingEnemy(unit, enemy)) {
// A hostile civilian is an obstacle; route around it, and when the
// destination has become unreachable, abandon the move instead of
// stalling on the spot forever.
if (this._repathAround(unit)) {
const ahead = this.unitsAt(unit.path[unit.pathIndex + 1])
.find((other) => other !== unit && other.civ !== unit.civ);
if (ahead) break;
continue;
}
this._stopUnit(unit);
break;
}
// A hostile military unit does not stop the charge: combat is fought on
// a single tile, so the unit steps in and the two stacks share it.
// Friendly occupants do not block either: the unit simply stacks.
const cost = Math.max(this._tileTravelHours(unit, next), MIN_STEP_COST);
const needed = cost - unit.progressHours;
// A hair of slack lets a run of equal steps that sums to exactly the
// tick's hours (for example ten 0.2-hour air steps) finish in one tick
// instead of stalling on a floating-point remainder.
if (hoursLeft + 1e-9 >= needed) {
hoursLeft -= needed;
unit.pathIndex += 1;
unit.progressHours = 0;
this._moveUnitTo(unit, next);
this._visibilityDirty = true;
this._onUnitArrived(unit);
if (this._hasHostileOn(unit.coords, unit.civ)) {
// The fight is joined: hold here and lock every participant. The
// unit that charged in is the attacker; the occupants defend.
this._stopUnit(unit);
unit.invader = true;
for (const other of this.unitsAt(unit.coords)) {
if (other.civ !== unit.civ && this._isMilitary(other)) {
other.invader = false;
engaged.add(other);
}
}
engaged.add(unit);
break;
}
} else {
unit.progressHours += hoursLeft;
hoursLeft = 0;
}
}
if (!this._hasRoute(unit) && (!unit.waypoints || unit.waypoints.length === 0)) {
this._stopUnit(unit);
}
}
},
// Advances a flier along its straight pixel path: aircraft and launched
// missiles. Unlike the tile walker there is no terrain, no occupancy and no
// obstacle to route around, so the leg simply runs its hours and the plane's
// tile is derived from where the pixel line is.
_advanceFlier(unit, hours) {
unit._flightHalted = false;
let hoursLeft = hours;
while (hoursLeft > 1e-9) {
if (!this._hasRoute(unit)) {
if (!unit.waypoints || unit.waypoints.length === 0) break;
if (!this._followNextWaypoint(unit)) break;
continue;
}
const a = unit.path[unit.pathIndex];
const b = unit.path[unit.pathIndex + 1];
const total = this._segmentFlightHours(unit, a, b);
if (!(total > 0)) {
// A zero-length leg (already at the destination) completes at once.
this._completeFlightLeg(unit);
if (!this.findUnit(unit.id) || unit._flightHalted) break;
continue;
}
const needed = total - unit.progressHours;
if (hoursLeft + 1e-9 < needed) {
unit.progressHours += hoursLeft;
hoursLeft = 0;
this._syncFlierCoords(unit);
break;
}
hoursLeft -= needed;
unit.progressHours = total;
this._completeFlightLeg(unit);
if (!this.findUnit(unit.id) || unit._flightHalted) break;
}
if (this.findUnit(unit.id) && !this._hasRoute(unit) &&
(!unit.waypoints || unit.waypoints.length === 0)) {
this._stopUnit(unit);
}
},
// Finishes one flight leg: lands the flier on its destination tile, resolves
// a bomb run or missile warhead, and lets a scheduled move pick up its next
// destination. `_flightHalted` tells the mover to stop consuming hours, for
// example after a plane turns for home.
_completeFlightLeg(unit) {
unit.pathIndex += 1;
unit.progressHours = 0;
unit.coords = localToMap(unit.path[unit.pathIndex], this.topology);
this._visibilityDirty = true;
// A missile's flight is its whole life: the warhead goes off where it lands,
// and it must never capture the tile it flies over (a missile is not an
// aircraft, so the capture rules would otherwise treat it as a ground unit).
if (this._isMissileUnit(unit)) {
this._detonateMissile(unit);
return;
}
this._onUnitArrived(unit);
if (!this.findUnit(unit.id)) return;
// A bomb run that has reached its target bombs once, then turns for home.
if (unit.strikeTarget &&
unit.coords.x === unit.strikeTarget.x && unit.coords.y === unit.strikeTarget.y) {
this._resolveAirStrike(unit);
this._stopUnit(unit);
this._orderAirReturn(unit);
unit._flightHalted = true;
}
},
// Keeps a flying unit's tile in step with its pixel position, so visibility,
// combat and the snapshot follow the plane between snapshot ticks.
_syncFlierCoords(unit) {
if (!this._hasRoute(unit)) return;
const a = unit.path[unit.pathIndex];
const b = unit.path[unit.pathIndex + 1];
const total = this._segmentFlightHours(unit, a, b);
const t = total > 0 ? Math.min(1, Math.max(0, unit.progressHours / total)) : 0;
const pixel = { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
const tile = localToMap(pixel, this.topology);
if (tile.x !== unit.coords.x || tile.y !== unit.coords.y) {
unit.coords = tile;
this._visibilityDirty = true;
}
},
// A hostile military unit standing on the next tile is an opponent to fight,
// not an obstacle to route around.
_isBlockingEnemy(unit, occupant) {
return this._isMilitary(unit) && this._isMilitary(occupant) && occupant.civ !== unit.civ;
},
// Every unit on the tile, ours or not, that is hostile to `civ`.
_hasHostileOn(coords, civ) {
return this.unitsAt(coords).some((unit) => unit.civ !== civ);
},
// Recomputes a route from where the unit stands to the destination it was
// already heading for. Returns false when no route exists any more.
_repathAround(unit) {
if (!unit.path || unit.path.length < 2) return false;
const goal = unit.path[unit.path.length - 1];
const path = this.findPath(unit, unit.coords, goal);
if (path.length < 2) return false;
unit.path = path;
unit.pathIndex = 0;
unit.progressHours = 0;
return true;
},
// Abandons every movement for the unit: no route, no partial step and no
// remaining scheduled destinations.
_stopUnit(unit) {
unit.path = [];
unit.pathIndex = 0;
unit.progressHours = 0;
unit.moveSpeed = null;
unit.waypoints = [];
},
// ------------------------------------------------------ travel cost --
_isControlledBy(unit, coords) {
return this.civAt(coords) === unit.civ;
},
_unitSpeed(unit) {
const proto = this.unitProto(unit);
return Math.max(proto ? proto.speed : DEFAULT_UNIT_SPEED, 0.0001);
},
_effectiveSpeedAt(unit, coords) {
// A group move pins every unit to the slowest member's speed.
let speed = unit.moveSpeed || this._unitSpeed(unit);
if (this._isControlledBy(unit, coords)) speed *= CONTROLLED_SPEED_MULTIPLIER;
return speed;
},
_tileTravelHours(unit, coords) {
// Aircraft fly over the ground: their speed is a flat tiles-per-hour figure,
// untouched by terrain, roads or whose land lies below.
if (this._isAirUnit(unit)) {
return 1 / (unit.moveSpeed || this._unitSpeed(unit));
}
// A road or railway levels the terrain: every such tile costs the same to
// cross, a railway half again as cheap as a road, and a nation's trains
// technology shortens its own railways further.
return this._tileMovementMultiplier(coords, unit.civ) / this._effectiveSpeedAt(unit, coords);
},
// The cheapest transport multiplier a tile offers: its railway, else its road,
// else the terrain's own. Flat across the whole tile, so a road levels hills
// and forests alike. A nation's trains technology speeds up its railways only.
_tileMovementMultiplier(coords, civ = -1) {
const k = key(coords.x, coords.y);
if (this.railways.has(k)) {
const base = TRANSPORT_BY_ID.railway.movementCostMultiplier;
return civ >= 0 ? base * this.railSpeedMultiplier(civ) : base;
}
if (this.roads.has(k)) return TRANSPORT_BY_ID.road.movementCostMultiplier;
return this.terrainStats.movementCostMultiplier(coords);
},
_fastestTileTravelHours(unit) {
// A railway (or, failing that, a road) is the cheapest possible tile, so the
// heuristic must use it when it beats the most generous terrain or A* could
// overestimate and miss the cheapest route. A railway can be cheaper still
// for a nation with trains technology.
const railway = TRANSPORT_BY_ID.railway.movementCostMultiplier *
this.railSpeedMultiplier(unit.civ);
const fastest = Math.min(
this._minMovementCost,
TRANSPORT_BY_ID.road.movementCostMultiplier,
railway
);
return fastest / (this._unitSpeed(unit) * CONTROLLED_SPEED_MULTIPLIER);
},
_heuristic(unit, from, to) {
const delta = this.topology.pixelDelta(from, to);
const tiles = Math.hypot(delta.x, delta.y) / this._maxStepLength;
return tiles * this._fastestTileTravelHours(unit);
},
_computeMaxStepLength() {
let longest = 0;
for (const neighbour of this._neighbours({ x: 0, y: 0 })) {
const delta = this.topology.pixelDelta({ x: 0, y: 0 }, neighbour);
longest = Math.max(longest, Math.hypot(delta.x, delta.y));
}
return longest > 0 ? longest : 1.0;
},
// --------------------------------------------------------- pathfinding --
// Whether `unit` may occupy `coords`: passable terrain and no hostile unit
// there unless the move is an attack (military into military, which turns the
// step into a fight). Friendly units may share a tile, so they never block.
// Enemy cities are only open to military units, since walking one in
// captures it.
//
// A foreign land is a wall only when its owner is a neutral third party: a
// route may cross the land of the nation it is aimed at (`targetCiv`), of an
// ally, and of a nation already at war (the war was declared regardless), as
// `_canTraverseTerritory` spells out. So a march never drags a bystander into
// a war for purely technical reasons.
//
// A hostile unit or city is only a valid destination (`isGoal`), never a tile
// to pass through: pathfinding routes around enemies instead of through them.
//
// `occupancy` (optional) is the per-tile unit index built once by findPath.
// Without it every candidate tile scans the whole army; the search therefore
// looks up occupancy in constant time, exactly as the browser's prediction
// does in map_view/motion.js.
_canUnitEnter(unit, coords, isGoal = false, targetCiv = -1, occupancy = null, zoc = null, from = null) {
const proto = this.unitProto(unit);
const air = this._isAirProto(proto);
if (air) {
// Aircraft fly over fog and foreign lands alike, so only the tile's
// existence and its terrain class constrain them.
const tile = this.tiles[key(coords.x, coords.y)];
if (!tile || !proto.traversableTerrains.includes(tile.terrainClass)) return false;
} else if (!this.canEnter(unit.civ, proto, coords)) {
return false;
}
// Zone of control: a hostile unit denies its own tile and the six around it.
// Troops may enter or leave the ring, but never cross straight from one
// denied tile into another. A deliberate attack on the goal is exempt, so
// the ring never shields a garrison from a charge.
if (!air && !isGoal && zoc && from &&
zoc.has(key(from.x, from.y)) && zoc.has(key(coords.x, coords.y))) {
return false;
}
const owner = this.civAt(coords);
// A spy slips through foreign territory at will; every other unit treats a
// neutral third party's land as a wall (the targeted civilisation, an ally
// and anyone already at war are all exempt).
if (!air && !isGoal && !proto.spy && !this._canTraverseTerritory(unit.civ, owner, targetCiv)) {
return false;
}
const occupants = occupancy
? (occupancy.get(key(coords.x, coords.y)) || [])
: this.unitsAt(coords).filter((other) => other !== unit);
if (occupants.length > 0) {
const hostile = occupants.filter((other) => other.civ !== unit.civ);
if (hostile.length > 0) {
if (!isGoal) return false;
if (!this._isMilitary(unit)) return false;
if (!hostile.some((other) => this._isMilitary(other))) return false;
}
}
const city = this._cityByCoords.get(key(coords.x, coords.y));
if (city && city.civ !== unit.civ) {
// A hostile city is only ever a destination: ground troops march in to
// capture it, and aircraft may bomb its garrison. Aircraft never occupy
// it (captures skip them), so the tile stays closed as a through-route.
if (!isGoal) return false;
if (!this._isMilitary(unit)) return false;
}
return true;
},
// Whether `civ` may cross land owned by `owner` on a through-route. A nation's
// own land is always open, and so is the land of the nation a route is aimed
// at (`targetCiv`), of an ally, and of a nation already at war -- the war was
// declared regardless. Only a neutral third party is a wall, so a march never
// drags a bystander into a war for purely technical reasons. Mirrored by the
// browser in map_view/motion.js `_canTraverseTerritory`.
_canTraverseTerritory(civ, owner, targetCiv = -1) {
if (owner < 0 || owner === civ || owner === targetCiv) return true;
if (this.isAtWar(civ, owner)) return true;
if (this.areAllied(civ, owner)) return true;
return false;
},
// Every unit bucketed by the tile it stands on, so a search can look up
// occupancy in constant time instead of scanning the whole army for every
// tile it considers. The mover is left out: it never blocks itself. The
// index is thrown away after the search, so it can never drift from the
// live unit list.
_buildOccupancy(unit) {
const index = new Map();
for (const other of this.units) {
if (other === unit) continue;
const k = key(other.coords.x, other.coords.y);
const list = index.get(k);
if (list) list.push(other);
else index.set(k, [other]);
}
return index;
},
// The tiles denied to `civ` by hostile ground forces: every hostile military
// unit's tile and the six around it. Aircraft project no zone.
_zoneOfControl(civ) {
const denied = new Set();
for (const other of this.units) {
if (other.civ === civ) continue;
if (!this._isMilitary(other) || this._isAirUnit(other)) continue;
const k = key(other.coords.x, other.coords.y);
denied.add(k);
for (const neighbour of this._neighbours(other.coords)) {
denied.add(key(neighbour.x, neighbour.y));
}
}
return denied;
},
// `pathCache` (optional) shares routes between units whose civ, type, start
// and goal are identical: the route shape depends only on the unit type's
// traversable terrain and the owning civ's territory/fog, while movement
// speed merely scales every step cost by the same factor and so cannot change
// which route is cheapest. The path array is treated as read-only by callers.
findPath(unit, start, goal, pathCache = null) {
const cacheKey = pathCache
? `${unit.civ}|${unit.proto}|${start.x},${start.y}|${goal.x},${goal.y}`
: null;
if (cacheKey && pathCache.has(cacheKey)) return pathCache.get(cacheKey);
// A flier ignores the ground: a straight line is its whole route, so the A*
// search -- and with it terrain, zone-of-control and occupancy -- never
// runs. The route is a pair of world-pixel points, the same line the
// browser draws and predicts.
if (this._isFlier(unit)) {
const path = start.x === goal.x && start.y === goal.y
? [mapToLocal(start.x, start.y)]
: flightLine(mapToLocal(start.x, start.y), goal, this.topology);
if (cacheKey) pathCache.set(cacheKey, path);
return path;
}
const occupancy = this._buildOccupancy(unit);
const targetCiv = this._targetCivFor(unit, goal, occupancy);
const zoc = this._zoneOfControl(unit.civ);
const enterCache = new Map();
const costCache = new Map();
const heuristicCache = new Map();
const path = this._pathfinder.findPath(
start,
goal,
(coords) => this._neighbours(coords),
(coords, isGoal, from) => {
const fromKey = from ? key(from.x, from.y) : "";
const k = `${key(coords.x, coords.y)}${isGoal ? ":goal" : ""}@${fromKey}`;
if (!enterCache.has(k)) {
enterCache.set(
k,
this._canUnitEnter(unit, coords, isGoal, targetCiv, occupancy, zoc, from)
);
}
return enterCache.get(k);
},
(_from, to) => {
const k = key(to.x, to.y);
if (!costCache.has(k)) costCache.set(k, this._tileTravelHours(unit, to));
return costCache.get(k);
},
(from, to) => {
const k = key(from.x, from.y);
if (!heuristicCache.has(k)) heuristicCache.set(k, this._heuristic(unit, from, to));
return heuristicCache.get(k);
}
);
if (cacheKey) pathCache.set(cacheKey, path);
return path;
},
// The civilisation a route is aimed at: the enemy city or unit on the goal,
// or failing that whoever owns the goal tile. Crossing that civ's land is
// allowed; every other foreign territory is a wall.
_targetCivFor(unit, goal, occupancy = null) {
const city = this._cityByCoords.get(key(goal.x, goal.y));
if (city && city.civ !== unit.civ) return city.civ;
const occupants = occupancy
? (occupancy.get(key(goal.x, goal.y)) || [])
: this.unitsAt(goal);
for (const other of occupants) {
if (other.civ !== unit.civ) return other.civ;
}
return this.civAt(goal);
},
};