DEFENSE · TOWER ATTACK PRIORITY

How to Make Towers Attack Hostiles with Explainable Priorities

Filter FIND_HOSTILE_CREEPS through an explicit allowed-user policy, score active combat parts, use range and name as deterministic tie-breakers, require active owned Towers with TOWER_ENERGY_COST, save attack() results, and re-evaluate targets every tick.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — StructureTower.attack(), FIND_HOSTILE_CREEPS, TOWER_ENERGY_COST, whole-room range, falloff and return codes · Diplomacy boundary: The allowed-user list and combat-part weights are project policies, not official hostility or diplomacy rules

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — StructureTower.attack(), FIND_HOSTILE_CREEPS, TOWER_ENERGY_COST, whole-room range, falloff and return codes
Diplomacy boundary
The allowed-user list and combat-part weights are project policies, not official hostility or diplomacy rules
Execution boundary
OK schedules an attack; target hits, movement, healing and survival require later observation
JavaScript syntax
Passed
Offline target review
Passed — allowed users, active combat parts, threat order, nearest-Tower range, stable ties, Energy and activity states
Screeps Console test
Pending
Live Tower damage, falloff, boost, diplomacy and multi-Tower focus test
Pending
Last verified
July 26, 2026

Quick answer

Find current non-owned Creeps, remove users allowed by your own reviewed policy, score active combat and Controller-threatening parts, select a deterministic target by threat, nearest-Tower range and name, then let only owned active Towers with at least TOWER_ENERGY_COST Energy call tower.attack(target). Save every return code and refresh the target on every tick.

FIND_HOSTILE_CREEPS is not a diplomacy policy

FIND_HOSTILE_CREEPS is a game query, not an alliance database. It cannot know whether another player is an ally, temporary visitor, scout or permitted task participant.

Memory.defense ??= {};
Memory.defense.allowedUsers ??= [];
function getAttackableHostiles(room) {
  const allowedUsers = new Set(
    Array.isArray(Memory.defense?.allowedUsers)
      ? Memory.defense.allowedUsers
      : []
  );

  return room.find(FIND_HOSTILE_CREEPS, {
    filter: creep =>
      typeof creep.owner?.username === 'string'
      && !allowedUsers.has(creep.owner.username)
  });
}

An allowed-user list is operationally dangerous when stale or entered incorrectly. Production diplomacy may need expiration, room scope, incident overrides and event-log review.

Build an explainable threat score

This baseline scores current active parts rather than original body length:

function getTowerThreatScore(creep) {
  if (!creep) {
    return 0;
  }

  return (
    creep.getActiveBodyparts(ATTACK) * 5
    + creep.getActiveBodyparts(RANGED_ATTACK) * 5
    + creep.getActiveBodyparts(HEAL) * 4
    + creep.getActiveBodyparts(CLAIM) * 3
    + creep.getActiveBodyparts(WORK) * 2
  );
}

The weights are a project policy. WORK can dismantle structures and CLAIM can threaten Controller state, so a filter that checks only ATTACK can miss meaningful threats.

Select one deterministic target

function selectTowerAttackTarget(towers, hostiles) {
  if (towers.length === 0 || hostiles.length === 0) {
    return null;
  }

  return [...hostiles].sort((left, right) => {
    const threatDifference =
      getTowerThreatScore(right)
      - getTowerThreatScore(left);

    if (threatDifference !== 0) {
      return threatDifference;
    }

    const leftRange = Math.min(
      ...towers.map(tower =>
        tower.pos.getRangeTo(left)
      )
    );
    const rightRange = Math.min(
      ...towers.map(tower =>
        tower.pos.getRangeTo(right)
      )
    );

    if (leftRange !== rightRange) {
      return leftRange - rightRange;
    }

    return left.name.localeCompare(right.name);
  })[0] || null;
}

The name tie-breaker has no combat meaning. It prevents an otherwise equal sort from changing unpredictably. This version focuses all available Towers on one target and does not estimate overkill.

Require active Towers with Energy

function getAvailableDefenseTowers(room) {
  return room.find(FIND_MY_STRUCTURES, {
    filter: structure =>
      structure.structureType === STRUCTURE_TOWER
      && structure.isActive() === true
      && structure.store.getUsedCapacity(
        RESOURCE_ENERGY
      ) >= TOWER_ENERGY_COST
  });
}

The preflight removes obviously unavailable Towers, but another module can still try to assign a different action in the same tick. A complete room defense should have one Tower dispatcher.

Complete Tower attack example

function runTowerAttack(room) {
  if (!room) {
    return { status: 'room-not-visible' };
  }

  const towers = getAvailableDefenseTowers(room);
  if (towers.length === 0) {
    return { status: 'no-available-tower' };
  }

  const hostiles = getAttackableHostiles(room);
  const target = selectTowerAttackTarget(
    towers,
    hostiles
  );

  if (!target) {
    return { status: 'no-attack-target' };
  }

  const snapshot = {
    gameTick: Game.time,
    targetId: target.id,
    targetName: target.name,
    owner: target.owner?.username || null,
    hits: target.hits,
    hitsMax: target.hitsMax,
    threatScore: getTowerThreatScore(target)
  };
  const results = towers.map(tower => ({
    towerId: tower.id,
    range: tower.pos.getRangeTo(target),
    energyBefore: tower.store.getUsedCapacity(
      RESOURCE_ENERGY
    ),
    result: tower.attack(target)
  }));

  return {
    status: results.some(item => item.result === OK)
      ? 'attack-scheduled'
      : 'attack-rejected',
    snapshot,
    results
  };
}
module.exports.loop = function () {
  const room = Game.rooms.W1N1;
  if (!room) {
    return;
  }

  const outcome = runTowerAttack(room);
  if (
    outcome.status === 'attack-rejected'
    || Game.time % 50 === 0
  ) {
    console.log(JSON.stringify({
      type: 'tower-attack-status',
      roomName: room.name,
      ...outcome
    }));
  }
};

Refresh targets every tick

A target can die, leave, move, enter protection, lose active parts, receive a boost or change diplomatic status. Do not store the full object in Memory. Store an ID and diagnostic snapshot only when needed, then recover and re-score current objects.

function recoverPreviousTowerTarget(targetId) {
  return typeof targetId === 'string'
    ? Game.getObjectById(targetId)
    : null;
}

Understand distance and effect

Tower range covers the room, so attack does not use ERR_NOT_IN_RANGE. Actual damage falls with distance, and active Tower power effects may modify it. This guide uses range only as a tie-breaker and does not promise a kill in one volley.

Verify the next tick

Compare the stored target ID, hits and Tower Energy with current visible state. A missing target may mean death or loss of visibility; a smaller hits delta than expected may reflect healing, damage reduction, range or other simultaneous actions.

Handle return codes

CodeMeaningReview
OKAttack scheduledTarget hits later
ERR_NOT_OWNERTower not yoursSelection and ownership
ERR_NOT_ENOUGH_ENERGYInsufficient Tower EnergyStore and competing actions
ERR_INVALID_TARGETTarget invalidRefresh target this tick
ERR_RCL_NOT_ENOUGHTower inactiveRCL and isActive()

Debugging checklist

  • Refresh the visible room.
  • Review allowed usernames.
  • Use active body parts.
  • Keep threat weights documented.
  • Use deterministic target ties.
  • Require active owned Towers.
  • Check TOWER_ENERGY_COST.
  • Use one Tower dispatcher per tick.
  • Save every attack return code.
  • Verify damage without claiming guaranteed kills.

Scope and next steps

This guide does not model boosts, exact falloff damage, overkill, split fire, Power Creeps, Rampart protection, event-driven diplomacy or Safe Mode. Continue with Tower healing priorities.

Frequently asked questions

Why focus all Towers on one target?

It is deterministic and easy to verify. Advanced defense should estimate assigned damage and split Towers when focus fire would waste Energy.

Why include WORK and CLAIM?

They can threaten structures and Controller control even when the Creep has no direct ATTACK part.

Should an allowed user always be ignored?

Not necessarily. A stronger system can revoke permission after hostile actions or limit access to specific rooms and tasks.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide is rewritten for a focused search intent while preserving the technical scope and verification boundaries of its Chinese source. Live-room evidence is claimed only when the verification record says it exists.