import { Registry } from "../../core/registry.ts";
import { game } from "../../core/game.ts";
import { events } from "../../core/event-bus.ts";
import {
ORGANIC,
FIRE_RESISTANT,
MELEE_WEAPON,
AMMUNITION,
} from "../../core/flags.ts";
import {
BLACK,
BLUE,
Color,
LIGHTBLUE,
NONE,
PALEVIOLETRED,
WHITE,
colorByName,
} from "../../core/color.ts";
import { Sym } from "../../core/sym.ts";
import { BaseSerializer } from "../../core/serializer.ts";
import { State, stateFromString } from "../../core/state.ts";
import { Targeting, getDistance } from "./targeting.ts";
import { AbstractAgent } from "./creatures.ts";
import { Animated } from "../../core/animated.ts";
import { Cell } from "../../core/cell.ts";
import { GameEvent } from "../../core/game-event.ts";
import { Agent } from "../../core/agent.ts";
import { Direction } from "../../core/direction.ts";
import { Item } from "../../core/item.ts";
import { Board } from "../../core/board.ts";
const CTBH = {
ARCHER: 30,
COMMONER: 50,
NOBLE: 20,
RIFLEMAN: 30,
WIZARD: 40,
};
const DAMAGE = {
ARCHER: 20,
COMMONER: 5,
NOBLE: 20,
RIFLEMAN: 20,
WIZARD: 15,
};
/**
* 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.
*/
export class NPC extends AbstractAgent implements Animated {
static #ENCOUNTER_TARGETING = new Targeting().attackPlayer(10);
state: State;
message: string;
questColor: Color;
doneColor: Color;
inQuestMsg: string;
flag: number;
damage: number;
constructor(
name: string,
state: State,
color: Color,
flags: number,
message: string,
questColor: Color,
doneColor: Color,
inQuestMsg: string,
flag: number,
chanceToHit: number,
damage: number,
glyph: string,
) {
const sym = state.isOn()
? Sym.of(glyph, PALEVIOLETRED)
: questColor != null && questColor !== NONE && doneColor == null
? Sym.of(glyph, LIGHTBLUE, null, BLUE, null)
: Sym.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;
}
randomSeed(): boolean {
return true;
}
onFrame(event: GameEvent, cell: Cell, frame: number) {
if (this.state.isOff()) {
const playerCell = event.board.getCurrentCell();
if (
this.#hasColor(this.questColor) &&
getDistance(cell, playerCell) < 3 &&
Math.random() * 100 < 70
) {
game.agentMove(cell, this, NPC.#ENCOUNTER_TARGETING);
} else {
this.friendlyTurn(event, cell, frame);
}
} else {
this.hostileTurn(event, cell, frame);
}
}
protected hostileTurn(event: GameEvent, cell: Cell, frame: number) { }
protected friendlyTurn(event: GameEvent, cell: Cell, frame: number) { }
changeHealth(delta: number): number {
// Quest providers are indestructible
return this.#isQuestProvider() ? 1 : super.changeHealth(delta);
}
onHit(event: GameEvent, attackerLoc: Cell, agentLoc: Cell, agent: Agent) {
if (this.state.isOn()) {
agent.changeHealth(this.damage);
}
}
onHitBy(event: GameEvent, agentLoc: Cell, agent: Agent, dir: Direction) {
super.onHitBy(event, agentLoc, agent, dir);
if (this.state.isOff()) {
const selected = event.player.bag.getSelected();
if (selected.is(MELEE_WEAPON) && !this.#isQuestProvider()) {
this.#alert(event.board);
} else {
const cell = agentLoc.getAdjacentCell(dir);
if (cell) {
this.talk(event, cell);
}
}
}
}
onHitByItem(event: GameEvent, itemLoc: Cell, item: Item, dir: Direction) {
super.onHitByItem(event, itemLoc, item, dir);
if (this.state.isOff() && item.is(AMMUNITION)) {
this.#alert(event.board);
}
}
#alert(board: Board) {
board.visit((cell) => {
const agent = cell.agent;
if (
agent instanceof NPC &&
!agent.#isQuestProvider() &&
agent.state.isOff()
) {
let key = Registry.serialize(agent);
key = key.replace(/\boff\b/g, "on");
const hostile = Registry.agent(key);
if (hostile) cell.setAgent(hostile);
}
return false;
});
}
#isQuestProvider() {
return this.#hasColor(this.questColor) || this.#hasColor(this.doneColor);
}
#hasColor(color: Color) {
return color != null && color !== NONE;
}
talk(event: GameEvent, cell: Cell) {
const board = event.board;
const player = event.player;
if (this.#hasColor(this.questColor)) {
board.fireColorEvent(event, this.questColor, cell);
this.#turnOffColor(cell, 4);
} else if (
this.#hasColor(this.doneColor) &&
player.matchesFlagOrItem(this.flag)
) {
board.fireColorEvent(event, this.doneColor, cell);
this.#turnOffColor(cell, 5);
} else if (this.#hasColor(this.doneColor)) {
events.fireModalMessage(this.inQuestMsg);
} else if (this.message) {
events.fireModalMessage(this.message);
}
}
#turnOffColor(cell: Cell, index: number) {
const agent = cell.agent;
if (!agent) return;
const key = Registry.serialize(agent);
const parts = key.split("|");
parts[index] = "None";
const next = Registry.agent(parts.join("|"));
if (next) cell.setAgent(next);
}
}
// TODO: Obviously any is pretty bad here, but it's complex to fix
function makeNpc(args: string[], Cls: any) {
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],
);
}
function makeNpcSerializer(typeId: string, Cls: any, tag = "People") {
return new (class extends BaseSerializer {
create(args: string[]) {
return makeNpc(args, Cls);
}
store(n: NPC) {
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() {
return `${typeId}|{state}|{color}|{message}|{questColor?}|{doneColor?}|{inQuestMsg?}|{flag?}`;
}
tag() {
return tag;
}
})();
}
const WANDER_TARGETING = new Targeting().moveRandomly();
export class Commoner extends NPC {
static #FLEE_TARGETING = new Targeting().fleePlayer(10).moveRandomly();
constructor(
state: State,
color: Color,
message: string,
questColor: Color,
doneColor: Color,
inQuestMsg: string,
flag: number,
name = "Commoner",
glyph = "A",
) {
super(
name,
state,
color,
ORGANIC,
message,
questColor,
doneColor,
inQuestMsg,
flag,
CTBH.COMMONER,
DAMAGE.COMMONER,
glyph,
);
}
friendlyTurn(event: GameEvent, cell: Cell, frame: number) {
if (frame % 10 === 0) {
game.agentMove(cell, this, WANDER_TARGETING);
}
}
hostileTurn(event: GameEvent, cell: Cell, frame: number) {
if (frame % 4 === 0) {
game.agentMove(cell, this, Commoner.#FLEE_TARGETING);
}
}
static SERIALIZER = makeNpcSerializer("Commoner", Commoner);
}
export class Noble extends NPC {
static #ATTACK_TARGETING = new Targeting()
.moveRandomly()
.attackPlayer(15)
.trackPlayer();
constructor(
state: State,
color: Color,
message: string,
questColor: Color,
doneColor: Color,
inQuestMsg: string,
flag: number,
name = "Noble",
glyph = "Ä",
) {
super(
name,
state,
color,
ORGANIC,
message,
questColor,
doneColor,
inQuestMsg,
flag,
CTBH.NOBLE,
DAMAGE.NOBLE,
glyph,
);
}
friendlyTurn(event: GameEvent, cell: Cell, frame: number) {
if (frame % 10 === 0) {
game.agentMove(cell, this, WANDER_TARGETING);
}
}
hostileTurn(event: GameEvent, cell: Cell, frame: number) {
if (frame % 4 === 0) {
game.agentMove(cell, this, Noble.#ATTACK_TARGETING);
}
}
static SERIALIZER = makeNpcSerializer("Noble", Noble);
}
export class Archer extends NPC {
static #ATTACK_TARGETING = new Targeting()
.moveRandomly()
.attackPlayer(10)
.trackPlayer()
.keepDistance(4);
static #FIRE_TARGETING = new Targeting().attackPlayer(10);
constructor(
state: State,
color: Color,
message: string,
questColor: Color,
doneColor: Color,
inQuestMsg: string,
flag: number,
name = "Archer",
glyph = "Á",
) {
super(
name,
state,
color,
ORGANIC,
message,
questColor,
doneColor,
inQuestMsg,
flag,
CTBH.ARCHER,
DAMAGE.ARCHER,
glyph,
);
}
friendlyTurn(event: GameEvent, cell: Cell, frame: number) {
if (frame % 10 === 0) {
game.agentMove(cell, this, WANDER_TARGETING);
}
}
hostileTurn(event: GameEvent, cell: Cell, frame: number) {
if (frame % 4 === 0) {
game.agentMove(cell, this, Archer.#ATTACK_TARGETING);
}
if (frame % 5 === 0) {
game.agentShoot(cell, this, Registry.item("Arrow"), Archer.#FIRE_TARGETING);
}
}
static SERIALIZER = makeNpcSerializer("Archer", Archer);
}
export class Rifleman extends NPC {
static #ATTACK_TARGETING = new Targeting()
.moveRandomly()
.attackPlayer(10)
.trackPlayer()
.keepDistance(4);
static #FIRE_TARGETING = new Targeting().attackPlayer(10);
constructor(
state: State,
color: Color,
message: string,
questColor: Color,
doneColor: Color,
inQuestMsg: string,
flag: number,
name = "Rifleman",
glyph = "Â",
) {
super(
name,
state,
color,
ORGANIC,
message,
questColor,
doneColor,
inQuestMsg,
flag,
CTBH.RIFLEMAN,
DAMAGE.RIFLEMAN,
glyph,
);
}
friendlyTurn(event: GameEvent, cell: Cell, frame: number) {
if (frame % 10 === 0) {
game.agentMove(cell, this, WANDER_TARGETING);
}
}
hostileTurn(event: GameEvent, cell: Cell, frame: number) {
if (frame % 4 === 0) {
game.agentMove(cell, this, Rifleman.#ATTACK_TARGETING);
}
if (frame % 5 === 0) {
game.agentShoot(cell, this, Registry.item("Bullet"), Rifleman.#FIRE_TARGETING);
}
}
static SERIALIZER = makeNpcSerializer("Rifleman", Rifleman);
}
export class Wizard extends NPC {
static #ATTACK_TARGETING = new Targeting()
.moveRandomly()
.attackPlayer(10)
.trackPlayer()
.keepDistance(4);
static #FIRE_TARGETING = new Targeting().attackPlayer(10);
constructor(
state: State,
color: Color,
message: string,
questColor: Color,
doneColor: Color,
inQuestMsg: string,
flag: number,
name = "Wizard",
glyph = "Ã",
) {
super(
name,
state,
color,
ORGANIC | FIRE_RESISTANT,
message,
questColor,
doneColor,
inQuestMsg,
flag,
CTBH.WIZARD,
DAMAGE.WIZARD,
glyph,
);
}
friendlyTurn(event: GameEvent, cell: Cell, frame: number) {
if (frame % 10 === 0) {
game.agentMove(cell, this, WANDER_TARGETING);
}
}
hostileTurn(event: GameEvent, cell: Cell, frame: number) {
if (frame % 5 === 0) {
const ammo = Math.random() * 3 < 1 ? Registry.item("Parabullet") : Registry.item("Fireball");
game.agentShoot(cell, this, ammo, Wizard.#FIRE_TARGETING);
} else if (frame % 7 === 0) {
game.agentMove(cell, this, Wizard.#ATTACK_TARGETING);
}
}
static SERIALIZER = makeNpcSerializer("Wizard", Wizard);
}
export class MallocCommoner extends Commoner {
constructor(state: State, color: Color, message: string, questColor: Color, doneColor: Color, inQuestMsg: string, flag: number) {
super(
state,
color,
message,
questColor,
doneColor,
inQuestMsg,
flag,
"Malloc Grunt",
"O",
);
}
static SERIALIZER = makeNpcSerializer(
"MallocCommoner",
MallocCommoner,
"Mallocs",
);
}
export class MallocNoble extends Noble {
constructor(state: State, color: Color, message: string, questColor: Color, doneColor: Color, inQuestMsg: string, flag: number) {
super(
state,
color,
message,
questColor,
doneColor,
inQuestMsg,
flag,
"Malloc Lord",
"Ö",
);
}
static SERIALIZER = makeNpcSerializer("MallocNoble", MallocNoble, "Mallocs");
}
export class MallocArcher extends Archer {
constructor(state: State, color: Color, message: string, questColor: Color, doneColor: Color, inQuestMsg: string, flag: number) {
super(
state,
color,
message,
questColor,
doneColor,
inQuestMsg,
flag,
"Malloc Archer",
"Ó",
);
}
static SERIALIZER = makeNpcSerializer(
"MallocArcher",
MallocArcher,
"Mallocs",
);
}
export class MallocRifleman extends Rifleman {
constructor(state: State, color: Color, message: string, questColor: Color, doneColor: Color, inQuestMsg: string, flag: number) {
super(
state,
color,
message,
questColor,
doneColor,
inQuestMsg,
flag,
"Malloc Carbiner",
"Ô",
);
}
static SERIALIZER = makeNpcSerializer(
"MallocRifleman",
MallocRifleman,
"Mallocs",
);
}
export class MallocWizard extends Wizard {
constructor(state: State, color: Color, message: string, questColor: Color, doneColor: Color, inQuestMsg: string, flag: number) {
super(
state,
color,
message,
questColor,
doneColor,
inQuestMsg,
flag,
"Malloc Shaman",
"Õ",
);
}
static SERIALIZER = makeNpcSerializer(
"MallocWizard",
MallocWizard,
"Mallocs",
);
}
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);
}