|
- import { Creature } from './entity'
- import { TextLike, DynText, ToBe, LiveText } from './language'
- import { LogEntry, LogLines, FAElem, LogLine, FormatEntry, FormatOpt, PropElem, nilLog } from './interface'
-
- export enum DamageType {
- Pierce = "Pierce",
- Slash = "Slash",
- Crush = "Crush",
- Acid = "Acid",
- Seduction = "Seduction",
- Dominance = "Dominance",
- Heal = "Heal"
- }
-
- export interface DamageInstance {
- type: DamageType;
- amount: number;
- target: Vigor | Stat;
- }
-
- export enum Vigor {
- Health = "Health",
- Stamina = "Stamina",
- Resolve = "Resolve"
- }
-
- export const VigorIcons: {[key in Vigor]: string} = {
- Health: "fas fa-heart",
- Stamina: "fas fa-bolt",
- Resolve: "fas fa-brain"
- }
-
- export const VigorDescs: {[key in Vigor]: string} = {
- Health: "How much damage you can take",
- Stamina: "How much energy you have",
- Resolve: "How much dominance you can resist"
- }
-
- export type Vigors = {[key in Vigor]: number}
-
- export enum Stat {
- Toughness = "Toughness",
- Power = "Power",
- Speed = "Speed",
- Willpower = "Willpower",
- Charm = "Charm"
- }
-
- export type Stats = {[key in Stat]: number}
-
- export const StatIcons: {[key in Stat]: string} = {
- Toughness: 'fas fa-heartbeat',
- Power: 'fas fa-fist-raised',
- Speed: 'fas fa-feather',
- Willpower: 'fas fa-book',
- Charm: 'fas fa-comments'
- }
-
- export const StatDescs: {[key in Stat]: string} = {
- Toughness: 'Your physical resistance',
- Power: 'Your physical power',
- Speed: 'How quickly you can act',
- Willpower: 'Your mental resistance',
- Charm: 'Your mental power'
- }
-
- export enum VoreStat {
- Mass = "Mass",
- Bulk = "Bulk",
- PreyCount = "Prey Count"
- }
-
- export type VoreStats = {[key in VoreStat]: number}
-
- export const VoreStatIcons: {[key in VoreStat]: string} = {
- [VoreStat.Mass]: "fas fa-weight",
- [VoreStat.Bulk]: "fas fa-weight-hanging",
- [VoreStat.PreyCount]: "fas fa-utensils"
- }
-
- export const VoreStatDescs: {[key in VoreStat]: string} = {
- [VoreStat.Mass]: "How much you weigh",
- [VoreStat.Bulk]: "Your weight, plus the weight of your prey",
- [VoreStat.PreyCount]: "How many creatures you've got inside of you"
- }
-
- export interface CombatTest {
- test: (user: Creature, target: Creature) => boolean;
- odds: (user: Creature, target: Creature) => number;
- explain: (user: Creature, target: Creature) => LogEntry;
- }
-
- /**
- * An instance of damage. Contains zero or more [[DamageInstance]] objects
- */
- export class Damage {
- readonly damages: DamageInstance[]
-
- constructor (...damages: DamageInstance[]) {
- this.damages = damages
- }
-
- scale (factor: number): Damage {
- const results: Array<DamageInstance> = []
-
- this.damages.forEach(damage => {
- results.push({
- type: damage.type,
- amount: damage.amount * factor,
- target: damage.target
- })
- })
-
- return new Damage(...results)
- }
-
- // TODO make this combine damage instances when appropriate
- combine (other: Damage): Damage {
- return new Damage(...this.damages.concat(other.damages))
- }
-
- toString (): string {
- return this.damages.map(damage => damage.amount + " " + damage.type).join("/")
- }
-
- render (): LogEntry {
- return new LogLine(...this.damages.flatMap(instance => {
- if (instance.target in Vigor) {
- return [instance.amount.toString(), new FAElem(VigorIcons[instance.target as Vigor]), " " + instance.type]
- } else if (instance.target in Stat) {
- return [instance.amount.toString(), new FAElem(StatIcons[instance.target as Stat]), " " + instance.type]
- } else {
- // this should never happen!
- return []
- }
- }))
- }
-
- renderShort (): LogEntry {
- const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {})
- const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => { total[key] = 0; return total }, {})
- this.damages.forEach(instance => {
- const factor = instance.type === DamageType.Heal ? -1 : 1
- if (instance.target in Vigor) {
- vigorTotals[instance.target as Vigor] += factor * instance.amount
- } else if (instance.target in Stat) {
- statTotals[instance.target as Stat] += factor * instance.amount
- }
- })
-
- const vigorEntries = Object.keys(Vigor).flatMap(key => vigorTotals[key as Vigor] === 0 ? [] : [new PropElem(key as Vigor, vigorTotals[key as Vigor]), ' '])
- const statEntries = Object.keys(Stat).flatMap(key => statTotals[key as Stat] === 0 ? [] : [new PropElem(key as Stat, statTotals[key as Stat]), ' '])
- return new FormatEntry(new LogLine(...vigorEntries.concat(statEntries)), FormatOpt.DamageInst)
- }
- }
-
- /**
- * Computes damage given the source and target of the damage.
- */
- export interface DamageFormula {
- calc (user: Creature, target: Creature): Damage;
- describe (user: Creature, target: Creature): LogEntry;
- explain (user: Creature): LogEntry;
- }
-
- /**
- * Simply returns the damage it was given.
- */
- export class ConstantDamageFormula implements DamageFormula {
- calc (user: Creature, target: Creature): Damage {
- return this.damage
- }
-
- constructor (private damage: Damage) {
-
- }
-
- describe (user: Creature, target: Creature): LogEntry {
- return this.explain(user)
- }
-
- explain (user: Creature): LogEntry {
- return new LogLine('Deal ', this.damage.renderShort(), ' damage')
- }
- }
-
- /**
- * Randomly scales the damage it was given with a factor of (1-x) to (1+x)
- */
- export class UniformRandomDamageFormula implements DamageFormula {
- calc (user: Creature, target: Creature): Damage {
- return this.damage.scale(Math.random() * this.variance * 2 - this.variance + 1)
- }
-
- constructor (private damage: Damage, private variance: number) {
-
- }
-
- describe (user: Creature, target: Creature): LogEntry {
- return this.explain(user)
- }
-
- explain (user: Creature): LogEntry {
- return new LogLine('Deal between ', this.damage.scale(1 - this.variance).renderShort(), ' and ', this.damage.scale(1 + this.variance).renderShort(), ' damage.')
- }
- }
-
- export class StatDamageFormula implements DamageFormula {
- calc (user: Creature, target: Creature): Damage {
- const instances: Array<DamageInstance> = this.factors.map(factor => {
- if (factor.stat in Stat) {
- return {
- amount: factor.fraction * user.stats[factor.stat as Stat],
- target: factor.target,
- type: factor.type
- }
- } else if (factor.stat in VoreStat) {
- return {
- amount: factor.fraction * user.voreStats[factor.stat as VoreStat],
- target: factor.target,
- type: factor.type
- }
- } else {
- // should be impossible; .stat is Stat|VoreStat
- return {
- amount: 0,
- target: Vigor.Health,
- type: DamageType.Heal
- }
- }
- })
-
- return new Damage(...instances)
- }
-
- describe (user: Creature, target: Creature): LogEntry {
- return new LogLine(
- this.explain(user),
- `, for a total of `,
- this.calc(user, target).renderShort()
- )
- }
-
- explain (user: Creature): LogEntry {
- return new LogLine(
- `Deal `,
- ...this.factors.map(factor => new LogLine(
- `${factor.fraction * 100}% of your `,
- new PropElem(factor.stat),
- ` as `,
- new PropElem(factor.target)
- )).joinGeneral(new LogLine(`, `), new LogLine(` and `))
- )
- }
-
- constructor (private factors: Array<{ stat: Stat|VoreStat; fraction: number; type: DamageType; target: Vigor|Stat }>) {
-
- }
- }
-
- export enum Side {
- Heroes,
- Monsters
- }
-
- /**
- * A Combatant has a list of possible actions to take, as well as a side.
- */
- export interface Combatant {
- actions: Array<Action>;
- groupActions: Array<GroupAction>;
- side: Side;
- }
-
- /**
- * An Action is anything that can be done by a [[Creature]] to a [[Creature]].
- */
- export abstract class Action {
- allowed (user: Creature, target: Creature): boolean {
- return this.conditions.every(cond => cond.allowed(user, target))
- }
-
- abstract execute (user: Creature, target: Creature): LogEntry
-
- abstract describe (user: Creature, target: Creature): LogEntry
-
- constructor (public name: TextLike, public desc: TextLike, private conditions: Array<Condition> = []) {
-
- }
-
- toString (): string {
- return this.name.toString()
- }
- }
-
- /**
- * A Condition describes whether or not something is permissible between two [[Creature]]s
- */
- export interface Condition {
- allowed: (user: Creature, target: Creature) => boolean;
- }
-
- export interface Actionable {
- actions: Array<Action>;
- }
-
- export abstract class GroupAction extends Action {
- allowedGroup (user: Creature, targets: Array<Creature>): Array<Creature> {
- return targets.filter(target => this.allowed(user, target))
- }
-
- executeGroup (user: Creature, targets: Array<Creature>): LogEntry {
- return new LogLines(...targets.map(target => this.execute(user, target)))
- }
-
- abstract describeGroup (user: Creature, targets: Array<Creature>): LogEntry
-
- constructor (name: TextLike, desc: TextLike, conditions: Array<Condition>) {
- super(name, desc, conditions)
- }
- }
-
- /**
- * Individual status effects, items, etc. should override some of these hooks.
- * Some hooks just produce a log entry.
- * Some hooks return results along with a log entry.
- */
- export class Effective {
- onApply (creature: Creature): LogEntry { return nilLog }
-
- onRemove (creature: Creature): LogEntry { return nilLog }
-
- preAction (creature: Creature): { prevented: boolean; log: LogEntry } {
- return {
- prevented: false,
- log: nilLog
- }
- }
-
- preDamage (creature: Creature, damage: Damage): Damage {
- return damage
- }
-
- preAttack (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } {
- return {
- prevented: false,
- log: nilLog
- }
- }
- }
- /**
- * A displayable status effect
- */
- export interface VisibleStatus {
- name: TextLike;
- desc: TextLike;
- icon: TextLike;
- topLeft: string;
- bottomRight: string;
- }
-
- /**
- * This kind of status is never explicitly applied to an entity -- e.g., a dead entity will show
- * a status indicating that it is dead, but entities cannot be "given" the dead effect
- */
- export class ImplicitStatus implements VisibleStatus {
- constructor (public name: TextLike, public desc: TextLike, public icon: string) {
-
- }
-
- topLeft = ''
- bottomRight = ''
- }
-
- /**
- * This kind of status is explicitly given to a creature.
- */
- export abstract class StatusEffect extends Effective implements VisibleStatus {
- constructor (public name: TextLike, public desc: TextLike, public icon: string) {
- super()
- }
-
- get topLeft () { return '' }
- get bottomRight () { return '' }
- }
-
- /**
- * An Encounter describes a fight: who is in it and whose turn it is
- */
- export class Encounter {
- private initiatives: Map<Creature, number>
- currentMove: Creature
- turnTime = 100
-
- constructor (public combatants: Creature[]) {
- this.initiatives = new Map()
-
- combatants.forEach(combatant => this.initiatives.set(combatant, 0))
- this.currentMove = combatants[0]
-
- this.nextMove()
- }
-
- nextMove (): void {
- this.initiatives.set(this.currentMove, 0)
- const times = new Map<Creature, number>()
-
- this.combatants.forEach(combatant => {
- // this should never be undefined
- const currentProgress = this.initiatives.get(combatant) ?? 0
- const remaining = (this.turnTime - currentProgress) / Math.max(combatant.stats.Speed, 1)
- times.set(combatant, remaining)
- })
-
- this.currentMove = this.combatants.reduce((closest, next) => {
- const closestTime = times.get(closest) ?? 0
- const nextTime = times.get(next) ?? 0
-
- return closestTime <= nextTime ? closest : next
- }, this.combatants[0])
- const closestRemaining = (this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) / Math.max(this.currentMove.stats.Speed, 1)
-
- this.combatants.forEach(combatant => {
- // still not undefined...
- const currentProgress = this.initiatives.get(combatant) ?? 0
- this.initiatives.set(combatant, currentProgress + closestRemaining * Math.max(combatant.stats.Speed, 1))
- console.log(combatant.name.toString(), currentProgress, closestRemaining)
- })
-
- // TODO: still let the creature use drained-vigor moves
-
- console.log(this.currentMove.name.toString())
- if (this.currentMove.disabled) {
- this.nextMove()
- }
- }
- }
|