pieces/agents/npcs.js

import { Registry } from "../../core/registry.js";
import { events } from "../../core/events.js";
import { ORGANIC, FIRE_RESISTANT } from "../../core/flags.js";
import {
  BLACK,
  BLUE,
  LIGHTBLUE,
  NONE,
  PALEVIOLETRED,
  WHITE,
  colorByName,
} from "../../core/color.js";
import { Symbol } from "../../core/symbol.js";
import { BaseSerializer } from "../../core/serializer.js";
import { stateFromString } from "../../core/state.js";
import { AbstractAgent } from "./creatures.js";

// ── Combat stats for NPCs ─────────────────────────────────────────────────────

const CTBH = {
  ARCHER: 30,
  COMMONER: 50,
  NOBLE: 20,
  RIFLEMAN: 30,
  WIZARD: 40,
};

const DAMAGE = {
  ARCHER: 20,
  COMMONER: 5,
  NOBLE: 20,
  RIFLEMAN: 20,
  WIZARD: 15,
};

// ── NPC base ──────────────────────────────────────────────────────────────────

/**
 * Base class for human/NPC agents. NPCs can be friendly (off) or hostile (on).
 * In friendly mode they wander; in hostile mode they attack.
 * Quest NPCs can assign and complete quests by firing color events.
 */
class NPC extends AbstractAgent {
  constructor(
    name,
    state,
    color,
    flags,
    message,
    questColor,
    doneColor,
    inQuestMsg,
    flag,
    chanceToHit,
    damage,
    glyph,
  ) {
    const sym = state.isOn()
      ? Symbol.of(glyph, PALEVIOLETRED)
      : questColor != null && questColor !== NONE && doneColor == null
        ? Symbol.of(glyph, LIGHTBLUE, null, BLUE, null)
        : Symbol.of(glyph, WHITE, null, BLACK, null);
    super(name, flags, color, chanceToHit, sym);
    this._state = state;
    this._message = message;
    this._questColor = questColor ?? null;
    this._doneColor = doneColor ?? null;
    this._inQuestMsg = inQuestMsg ?? null;
    this._flag = flag ?? null;
    this._damage = damage;
  }

  onHit(event, _attackerLoc, agentLoc, agent) {
    if (this._state.isOn()) {
      agent.changeHealth(this._damage);
    }
  }

  onHitBy(event, agentLoc, agent, dir) {
    if (this._state.isOff()) {
      // Friendly: talk to the player when bumped into
      this._talk(event, agentLoc.getAdjacentCell?.(dir));
    }
  }

  _talk(event, cell) {
    const board = event.board;
    const player = event.player;
    if (this._questColor && this._questColor !== NONE) {
      board?.fireColorEvent(event, this._questColor, cell);
    } else if (
      this._doneColor &&
      this._doneColor !== NONE &&
      player?.matchesFlagOrItem?.(this._flag)
    ) {
      board?.fireColorEvent(event, this._doneColor, cell);
    } else if (this._message) {
      events.fireModalMessage(this._message);
    }
  }
}

// ── NPC factory helpers ───────────────────────────────────────────────────────

function makeNpc(args, Cls) {
  if (args.length >= 7) {
    return new Cls(
      stateFromString(args[0]),
      colorByName(args[1]) ?? NONE,
      args[2],
      colorByName(args[3]) ?? NONE,
      colorByName(args[4]) ?? NONE,
      args[5],
      args[6],
    );
  }
  return new Cls(
    stateFromString(args[0]),
    colorByName(args[1]) ?? NONE,
    args[2],
  );
}

// ── NPC serializer helper ────────────────────────────────────────────────────

function makeNpcSerializer(typeId, Cls, tag = "People") {
  return new (class extends BaseSerializer {
    create(args) {
      return makeNpc(args, Cls);
    }
    store(n) {
      const q = n._questColor,
        d = n._doneColor,
        iq = n._inQuestMsg,
        f = n._flag;
      if (q != null && d != null && iq != null && f != null) {
        return `${typeId}|${n._state}|${n.color.name}|${n._message}|${q.name}|${d.name}|${iq}|${f}`;
      }
      return `${typeId}|${n._state}|${n.color.name}|${n._message}`;
    }
    example() {
      return makeNpc(["off", "White", "Hello"], Cls);
    }
    template(_id) {
      return `${typeId}|{state}|{color}|{message}|{questColor?}|{doneColor?}|{inQuestMsg?}|{flag?}`;
    }
    tag() {
      return tag;
    }
  })();
}

// ── Commoner ──────────────────────────────────────────────────────────────────

class Commoner extends NPC {
  constructor(state, color, message, questColor, doneColor, inQuestMsg, flag) {
    super(
      "Commoner",
      state,
      color,
      ORGANIC,
      message,
      questColor,
      doneColor,
      inQuestMsg,
      flag,
      CTBH.COMMONER,
      DAMAGE.COMMONER,
      "A",
    );
  }
  static SERIALIZER = makeNpcSerializer("Commoner", Commoner);
}

// ── Noble ─────────────────────────────────────────────────────────────────────

class Noble extends NPC {
  constructor(state, color, message, questColor, doneColor, inQuestMsg, flag) {
    super(
      "Noble",
      state,
      color,
      ORGANIC,
      message,
      questColor,
      doneColor,
      inQuestMsg,
      flag,
      CTBH.NOBLE,
      DAMAGE.NOBLE,
      "\u00C4",
    ); // Ä
  }
  static SERIALIZER = makeNpcSerializer("Noble", Noble);
}

// ── Archer ────────────────────────────────────────────────────────────────────

class Archer extends NPC {
  constructor(state, color, message, questColor, doneColor, inQuestMsg, flag) {
    super(
      "Archer",
      state,
      color,
      ORGANIC,
      message,
      questColor,
      doneColor,
      inQuestMsg,
      flag,
      CTBH.ARCHER,
      DAMAGE.ARCHER,
      "\u00C5",
    ); // Å
  }
  static SERIALIZER = makeNpcSerializer("Archer", Archer);
}

// ── Rifleman ──────────────────────────────────────────────────────────────────

class Rifleman extends NPC {
  constructor(state, color, message, questColor, doneColor, inQuestMsg, flag) {
    super(
      "Rifleman",
      state,
      color,
      ORGANIC,
      message,
      questColor,
      doneColor,
      inQuestMsg,
      flag,
      CTBH.RIFLEMAN,
      DAMAGE.RIFLEMAN,
      "\u00C2",
    ); // Â
  }
  static SERIALIZER = makeNpcSerializer("Rifleman", Rifleman);
}

// ── Wizard ────────────────────────────────────────────────────────────────────

class Wizard extends NPC {
  constructor(state, color, message, questColor, doneColor, inQuestMsg, flag) {
    super(
      "Wizard",
      state,
      color,
      ORGANIC | FIRE_RESISTANT,
      message,
      questColor,
      doneColor,
      inQuestMsg,
      flag,
      CTBH.WIZARD,
      DAMAGE.WIZARD,
      "\u00C3",
    ); // Ã
  }
  static SERIALIZER = makeNpcSerializer("Wizard", Wizard);
}

// ── Malloc variants (hostile faction) ────────────────────────────────────────

class MallocCommoner extends NPC {
  constructor(state, color, message, questColor, doneColor, inQuestMsg, flag) {
    super(
      "Malloc Grunt",
      state,
      color,
      ORGANIC,
      message,
      questColor,
      doneColor,
      inQuestMsg,
      flag,
      CTBH.COMMONER,
      DAMAGE.COMMONER,
      "O",
    );
  }
  static SERIALIZER = makeNpcSerializer(
    "MallocCommoner",
    MallocCommoner,
    "Mallocs",
  );
}

class MallocNoble extends NPC {
  constructor(state, color, message, questColor, doneColor, inQuestMsg, flag) {
    super(
      "Malloc Lord",
      state,
      color,
      ORGANIC,
      message,
      questColor,
      doneColor,
      inQuestMsg,
      flag,
      CTBH.NOBLE,
      DAMAGE.NOBLE,
      "\u00D6",
    ); // Ö
  }
  static SERIALIZER = makeNpcSerializer("MallocNoble", MallocNoble, "Mallocs");
}

class MallocArcher extends NPC {
  constructor(state, color, message, questColor, doneColor, inQuestMsg, flag) {
    super(
      "Malloc Archer",
      state,
      color,
      ORGANIC,
      message,
      questColor,
      doneColor,
      inQuestMsg,
      flag,
      CTBH.ARCHER,
      DAMAGE.ARCHER,
      "\u00D3",
    ); // Ó
  }
  static SERIALIZER = makeNpcSerializer(
    "MallocArcher",
    MallocArcher,
    "Mallocs",
  );
}

class MallocRifleman extends NPC {
  constructor(state, color, message, questColor, doneColor, inQuestMsg, flag) {
    super(
      "Malloc Carbiner",
      state,
      color,
      ORGANIC,
      message,
      questColor,
      doneColor,
      inQuestMsg,
      flag,
      CTBH.RIFLEMAN,
      DAMAGE.RIFLEMAN,
      "\u00D4",
    ); // Ô
  }
  static SERIALIZER = makeNpcSerializer(
    "MallocRifleman",
    MallocRifleman,
    "Mallocs",
  );
}

class MallocWizard extends NPC {
  constructor(state, color, message, questColor, doneColor, inQuestMsg, flag) {
    super(
      "Malloc Shaman",
      state,
      color,
      ORGANIC,
      message,
      questColor,
      doneColor,
      inQuestMsg,
      flag,
      CTBH.WIZARD,
      DAMAGE.WIZARD,
      "\u00D5",
    ); // Õ
  }
  static SERIALIZER = makeNpcSerializer(
    "MallocWizard",
    MallocWizard,
    "Mallocs",
  );
}

// ── Registry ──────────────────────────────────────────────────────────────────

export function registerNPCs() {
  Registry.register("Commoner", Commoner.SERIALIZER);
  Registry.register("Noble", Noble.SERIALIZER);
  Registry.register("Archer", Archer.SERIALIZER);
  Registry.register("Rifleman", Rifleman.SERIALIZER);
  Registry.register("Wizard", Wizard.SERIALIZER);
  Registry.register("MallocCommoner", MallocCommoner.SERIALIZER);
  Registry.register("MallocNoble", MallocNoble.SERIALIZER);
  Registry.register("MallocArcher", MallocArcher.SERIALIZER);
  Registry.register("MallocRifleman", MallocRifleman.SERIALIZER);
  Registry.register("MallocWizard", MallocWizard.SERIALIZER);
}