import grammar from "./grammar.ohm-bundle"; import { Affinity, ElementalType, evaluateOperands, EvaluationContext, FailReason, lookupOperands, MarkdownContext, MarkdownOutput, NumberSign, OperandItems, Operands, OperandSets, Source, Target, } from "../model/Messages"; import {IterationNode, Node, NonterminalNode, TerminalNode} from "ohm-js"; import { applyCharacterPrivacy, CharacterPrivacy, CharacterResources, CharacterSide, CharacterStatuses, isZpReady } from "../model/Character"; import {GameState, TimerDirection} from "../model/GameState"; import { formatMeteredResource, formatResourceDelta, formatResourceMax, formatResourceValue, isMeteredResource, isUnmeteredResource, MeteredResource, Resource, UnmeteredResource } from "../model/Resources"; import {ClockMode, ClockResources} from "../model/Clock"; import {isDefined} from "../types/type_check"; export const interpreter = grammar.createSemantics() export interface InterpreterNode extends Node { readonly element: ElementalType|null readonly affinity: Affinity readonly sign: NumberSign readonly numberValue: number readonly textValue: string readonly identifier: string readonly resource: Resource|null readonly operands: Operands readonly targets: Operands readonly sources: Operands readonly silenced: boolean readonly currentValue: number readonly maxValue: number readonly failReason: FailReason readonly side: CharacterSide readonly privacy: CharacterPrivacy // TODO: Implement all of the things listed below. // TODO: create rules for these to describe clocks/timers/items/statuses/characters and implement them readonly nameText: string readonly descriptionText: string // TODO: use for portraits, status icons, backdrops, BGM, and SFX readonly url: string readonly clockMode: ClockMode readonly timerDirection: TimerDirection readonly timerDurationMs: number // TODO: add backdrop and music change and sfx commands // TODO: make sure that FP and UP spent gets saved in the appropriate counters evaluate(ctx: EvaluationContext): EvaluationContext renderMarkdown(ctx: MarkdownContext): MarkdownOutput } export interface EvaluationNode extends InterpreterNode { readonly args: {readonly ctx: EvaluationContext} } export interface MarkdownNode extends InterpreterNode { readonly args: {readonly ctx: MarkdownContext} } interpreter.addAttribute("element", { fireElement(_: TerminalNode): ElementalType.Fire { return ElementalType.Fire }, waterElement(_: TerminalNode): ElementalType.Water { return ElementalType.Water }, lightningElement(_: TerminalNode): ElementalType.Lightning { return ElementalType.Lightning }, iceElement(_: TerminalNode): ElementalType.Ice { return ElementalType.Ice }, earthElement(_: TerminalNode): ElementalType.Earth { return ElementalType.Earth }, windElement(_: TerminalNode): ElementalType.Wind { return ElementalType.Wind }, lightElement(_: TerminalNode): ElementalType.Light { return ElementalType.Light }, darkElement(_: TerminalNode): ElementalType.Dark { return ElementalType.Dark }, physicalElement(_: TerminalNode): ElementalType.Physical { return ElementalType.Physical }, nonElement(_: TerminalNode): ElementalType.Nonelemental { return ElementalType.Nonelemental }, healingElement(_: TerminalNode): ElementalType.Healing { return ElementalType.Healing }, elementalType(elementNode: NonterminalNode): ElementalType { const element = (elementNode as InterpreterNode).element if (element === null) { throw Error("Unexpectedly null element when an element was specified") } return element }, DeltaOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, elementalType: IterationNode): ElementalType | null { return (elementalType as InterpreterNode).element }, DeltaOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, resource: IterationNode, elementalType: IterationNode): ElementalType | null { return (elementalType as InterpreterNode).element }, DeltaOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, elementalType: IterationNode): ElementalType | null { return (elementalType as InterpreterNode).element }, _iter(...children: readonly Node[]): ElementalType|null { if (this.isOptional() && children.length === 0) { return null } else if (this.isOptional() && children.length === 1) { return (children[0] as InterpreterNode).element } else { throw Error(`No idea what to say ${this.ctorName} iteration node's element is when there are multiple children`) } }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's element is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's element is`) } }) interpreter.addAttribute("affinity", { absorbAffinity(_: TerminalNode): Affinity.Absorbs { return Affinity.Absorbs }, immuneAffinity(_: TerminalNode): Affinity.Immune { return Affinity.Immune }, resistAffinity(_: TerminalNode): Affinity.Resistant { return Affinity.Resistant }, vulnerableAffinity(_: TerminalNode): Affinity.Vulnerable { return Affinity.Vulnerable }, normalAffinity(_: TerminalNode): Affinity.Normal { return Affinity.Normal }, affinity(affinityNode: NonterminalNode): Affinity { return (affinityNode as InterpreterNode).affinity }, DeltaOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, _elementalType: IterationNode): Affinity { return (affinity as InterpreterNode).affinity }, DeltaOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, _resource: IterationNode, _elementalType: IterationNode): Affinity { return (affinity as InterpreterNode).affinity }, DeltaOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, _elementalType: IterationNode): Affinity { return (affinity as InterpreterNode).affinity }, _iter(...children: readonly Node[]): Affinity { if (this.isOptional() && children.length === 0) { return Affinity.Normal } else if (this.isOptional() && children.length === 1) { return (children[0] as InterpreterNode).affinity } else { throw Error(`No idea what to say ${this.ctorName} iteration node's affinity is when there are multiple children`) } }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's affinity is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's affinity is`) } }) interpreter.addAttribute("sign", { deltaOperator(operator: NonterminalNode): NumberSign { return (operator as InterpreterNode).sign }, plus(_: TerminalNode): NumberSign.Positive { return NumberSign.Positive }, minus(_: TerminalNode): NumberSign.Negative { return NumberSign.Negative }, StatusOrItemCounterDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, delta: NonterminalNode): NumberSign { return (delta as InterpreterNode).sign }, _iter(): never { throw Error(`No idea what to say ${this.ctorName} iteration node's number sign is`) }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's number sign is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's number sign is`) } }) interpreter.addAttribute("numberValue", { integer(digits: IterationNode): number { return parseInt(digits.sourceString) }, Delta(sign: NonterminalNode, integer: NonterminalNode): number { const number = (integer as InterpreterNode).numberValue switch ((sign as InterpreterNode).sign) { case NumberSign.Negative: return -number case NumberSign.Positive: return number } }, DeltaOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, _affinity: IterationNode, _elementalType: IterationNode): number { return (delta as InterpreterNode).numberValue }, DeltaOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, _affinity: IterationNode, _resource: IterationNode, _elementalType: IterationNode): number { return (delta as InterpreterNode).numberValue }, DeltaOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, delta: NonterminalNode, _affinity: IterationNode, _elementalType: IterationNode): number { return (delta as InterpreterNode).numberValue }, MaxValue(separator: NonterminalNode, integer: NonterminalNode): number { return (integer as InterpreterNode).numberValue; }, SetValueOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, integer: NonterminalNode): number { return (integer as InterpreterNode).numberValue; }, SetValueOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, integer: NonterminalNode, _resource: NonterminalNode): number { return (integer as InterpreterNode).numberValue; }, SetValueOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, integer: NonterminalNode): number { return (integer as InterpreterNode).numberValue; }, SetMaxOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, maxvalue: NonterminalNode): number { return (maxvalue as InterpreterNode).numberValue; }, SetMaxOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, maxvalue: NonterminalNode, _resource: NonterminalNode): number { return (maxvalue as InterpreterNode).numberValue; }, SetMaxOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, maxvalue: NonterminalNode): number { return (maxvalue as InterpreterNode).numberValue; }, StatusOrItemCounterUnwrapped(x: IterationNode, integer: NonterminalNode): number { return (integer as InterpreterNode).numberValue }, StatusOrItemCounterWrapped(lParen: TerminalNode, counter: NonterminalNode, _rParen: TerminalNode): number { return (counter as InterpreterNode).numberValue }, StatusOrItemAddOperation(operands: NonterminalNode, colon: NonterminalNode, deltaOperator: NonterminalNode, identifier: NonterminalNode, counter: IterationNode): number { if (counter.numChildren === 0) { return 0 } return (counter.child(0)).numberValue }, StatusOrItemCounterDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, delta: NonterminalNode): number { return (delta as InterpreterNode).numberValue }, StatusOrItemCounterSetOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): number { return (counter as InterpreterNode).numberValue }, _iter(): never { throw Error(`No idea what to say ${this.ctorName} iteration node's number value is`) }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's number value is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's number value is`) } }) interpreter.addAttribute("textValue", { identifier(_: TerminalNode, __: NonterminalNode): string { return this.sourceString }, _iter(): never { throw Error(`No idea what to say ${this.ctorName} iteration node's text value is`) }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's text value is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's text value is`) } }) interpreter.addAttribute("identifier", { identifier(_: TerminalNode, __: IterationNode): string { return this.sourceString }, StatusOrItemAddOperation(operands: NonterminalNode, colon: NonterminalNode, deltaOperator: NonterminalNode, identifier: NonterminalNode, _counter: NonterminalNode): string { return (identifier as InterpreterNode).identifier }, StatusOrItemCounterDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, _delta: NonterminalNode): string { return (identifier as InterpreterNode).identifier }, StatusOrItemCounterSetOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, _counter: NonterminalNode): string { return (identifier as InterpreterNode).identifier }, _iter(): never { throw Error(`No idea what to say ${this.ctorName} iteration node's identifier value is`) }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's identifier value is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's identifier value is`) } }) interpreter.addAttribute("resource", { BpResource(_: NonterminalNode): MeteredResource.Blood { return MeteredResource.Blood }, FabulaResource(_: NonterminalNode): UnmeteredResource.Fabula { return UnmeteredResource.Fabula; }, HpResource(_: NonterminalNode): MeteredResource.Health { return MeteredResource.Health; }, IpResource(_: NonterminalNode): MeteredResource.Items { return MeteredResource.Items; }, LevelResource(_: NonterminalNode): UnmeteredResource.Level { return UnmeteredResource.Level; }, MaterialsResource(_: NonterminalNode): UnmeteredResource.Materials { return UnmeteredResource.Materials; }, MeteredResource(res: NonterminalNode): MeteredResource { const r = (res as InterpreterNode).resource; if (!r || !isMeteredResource(r)) { throw Error(`unexpected null or unmetered resource ${r} in metered resource`) } return r }, MoneyResource(_: NonterminalNode): UnmeteredResource.Zenit { return UnmeteredResource.Zenit; }, MpResource(_: NonterminalNode): MeteredResource.Magic { return MeteredResource.Magic; }, Resource(res: NonterminalNode): Resource { const r = (res as InterpreterNode).resource; if (r === null) { throw Error("unexpected null resource in resource node") } return r }, SegmentResource(_: NonterminalNode): MeteredResource.Segments { return MeteredResource.Segments }, SpResource(type: NonterminalNode): UnmeteredResource.Special|UnmeteredResource.Fabula|UnmeteredResource.Ultima { const r = (type as InterpreterNode).resource; switch (r) { case UnmeteredResource.Special: case UnmeteredResource.Fabula: case UnmeteredResource.Ultima: return r default: throw Error("unexpected non-SP resources in an SP node") } }, SpecialResource(_: TerminalNode): UnmeteredResource.Special { return UnmeteredResource.Special; }, TpResource(_: TerminalNode): MeteredResource.Turns { return MeteredResource.Turns; }, UltimaResource(_: TerminalNode): Resource { return UnmeteredResource.Ultima; }, OrderResource(_: TerminalNode): UnmeteredResource.Order { return UnmeteredResource.Order }, UnmeteredResource(res: NonterminalNode): UnmeteredResource { const r = (res as InterpreterNode).resource; if (!r || !isUnmeteredResource(r)) { throw Error(`unexpected null or metered resource ${r} in max operation`) } return r }, XpResource(_: TerminalNode): MeteredResource.Experience { return MeteredResource.Experience; }, ZpResource(_: TerminalNode): MeteredResource.Zero { return MeteredResource.Zero; }, DeltaOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, _colon: NonterminalNode, _delta: NonterminalNode, _affinity: IterationNode, _elementalType: IterationNode): Resource { const r = (resource as InterpreterNode).resource if (r === null) { throw Error("unexpected null resource in required resource node") } return r }, DeltaOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, resource: IterationNode, _elementalType: IterationNode): Resource|null { return (resource as InterpreterNode).resource }, DeltaOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, _delta: NonterminalNode, _affinity: IterationNode, _elementalType: IterationNode): Resource { const r = (resource as InterpreterNode).resource if (r === null) { throw Error("unexpected null resource in required resource node") } return r }, SetValueOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, _colon: NonterminalNode, _integer: NonterminalNode): Resource { const r = (resource as InterpreterNode).resource if (r === null) { throw Error("unexpected null resource in required resource node") } return r }, SetValueOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, integer: NonterminalNode, resource: NonterminalNode): Resource { const r = (resource as InterpreterNode).resource if (r === null) { throw Error("unexpected null resource in required resource node") } return r }, SetValueOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, _integer: NonterminalNode): Resource { const r = (resource as InterpreterNode).resource if (r === null) { throw Error("unexpected null resource in required resource node") } return r }, SetMaxOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, _colon: NonterminalNode, _maxvalue: NonterminalNode): MeteredResource { const r = (resource as InterpreterNode).resource; if (!r || !isMeteredResource(r)) { throw Error(`unexpected null or unmetered resource ${r} in max operation`) } return r }, SetMaxOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, maxvalue: NonterminalNode, resource: NonterminalNode): MeteredResource { const r = (resource as InterpreterNode).resource; if (!r || !isMeteredResource(r)) { throw Error(`unexpected null or unmetered resource ${r} in max operation`) } return r }, SetMaxOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, _maxvalue: NonterminalNode): MeteredResource { const r = (resource as InterpreterNode).resource; if (!r || !isMeteredResource(r)) { throw Error(`unexpected null or unmetered resource ${r} in max operation`) } return r }, _iter(...children: readonly Node[]): Resource|null { if (this.isOptional() && children.length === 0) { return null } else if (this.isOptional() && children.length === 1) { return (children[0] as InterpreterNode).resource } else { throw Error(`No idea what to say ${this.ctorName} iteration node's resource is when there are multiple children`) } }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's resource value is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's resource value is`) }, SetMeteredOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, _colon: NonterminalNode, _value: NonterminalNode): MeteredResource { const r = (resource as InterpreterNode).resource; if (!r || !isMeteredResource(r)) { throw Error(`unexpected null or unmetered resource ${r} in metered set`) } return r }, SetMeteredOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, value: NonterminalNode, resource: NonterminalNode): MeteredResource { const r = (resource as InterpreterNode).resource; if (!r || !isMeteredResource(r)) { throw Error(`unexpected null or unmetered resource ${r} in metered set`) } return r }, SetMeteredOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, _value: NonterminalNode): MeteredResource { const r = (resource as InterpreterNode).resource; if (!r || !isMeteredResource(r)) { throw Error(`unexpected null or unmetered resource ${r} in metered set`) } return r }, ClearOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, _colon: NonterminalNode, _nul: NonterminalNode): Resource { const r = (resource as InterpreterNode).resource if (r === null) { throw Error("unexpected null resource in required resource node") } return r }, ClearOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, nul: NonterminalNode, resource: NonterminalNode): Resource { const r = (resource as InterpreterNode).resource if (r === null) { throw Error("unexpected null resource in required resource node") } return r }, ClearOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, _nul: NonterminalNode): Resource { const r = (resource as InterpreterNode).resource if (r === null) { throw Error("unexpected null resource in required resource node") } return r }, }) interpreter.addAttribute("operands", { CompleteOperation(silence: IterationNode, operation: NonterminalNode, _terminator: NonterminalNode): Operands { return (operation as InterpreterNode).operands }, Operation(operation: NonterminalNode): Operands { return (operation as InterpreterNode).operands }, ClearOperation(operands: NonterminalNode, _space: NonterminalNode, _resource: NonterminalNode, _colon: NonterminalNode, _nul: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, ClearOperationAlternate(operands: NonterminalNode, _colon: NonterminalNode, _nul: NonterminalNode, _resource: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, ClearOperationAlternate2(operands: NonterminalNode, _colon: NonterminalNode, _resource: NonterminalNode, _nul: NonterminalNode): Operands { return (operands as InterpreterNode).operands; }, DeltaOperation(operands: NonterminalNode, _space: NonterminalNode, _resource: NonterminalNode, colon: NonterminalNode, _delta: NonterminalNode, _affinity: IterationNode, _element: IterationNode): Operands { return (operands as InterpreterNode).operands; }, DeltaOperationAlternate(operands: NonterminalNode, _colon: NonterminalNode, _delta: NonterminalNode, _affinity: IterationNode, _resource: IterationNode, _element: IterationNode): Operands { return (operands as InterpreterNode).operands; }, DeltaOperationAlternate2(operands: NonterminalNode, _colon: NonterminalNode, _resource: NonterminalNode, _delta: NonterminalNode, _affinity: IterationNode, _element: IterationNode): Operands { return (operands as InterpreterNode).operands; }, FailOperation(operands: NonterminalNode, _colon: NonterminalNode, _fail: NonterminalNode): Operands { return (operands as InterpreterNode).operands; }, PrintOperation(_: NonterminalNode, __: NonterminalNode): Set { return OperandItems() }, SetMaxOperation(operands: NonterminalNode, _space: NonterminalNode, _resource: NonterminalNode, _colon: NonterminalNode, _max: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, SetMaxOperationAlternate(operands: NonterminalNode, _colon: NonterminalNode, _max: NonterminalNode, _resource: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, SetMaxOperationAlternate2(operands: NonterminalNode, _colon: NonterminalNode, _resource: NonterminalNode, _max: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, SetMeteredOperation(operands: NonterminalNode, _space: NonterminalNode, _resource: NonterminalNode, _colon: NonterminalNode, _meter: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, SetMeteredOperationAlternate(operands: NonterminalNode, _colon: NonterminalNode, _meter: NonterminalNode, _resource: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, SetMeteredOperationAlternate2(operands: NonterminalNode, _colon: NonterminalNode, _resource: NonterminalNode, _meter: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, SetSourceOperation(source: NonterminalNode, operands: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, SetTargetOperation(target: NonterminalNode, operands: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, SetValueOperation(operands: NonterminalNode, _space: NonterminalNode, _resource: NonterminalNode, _colon: NonterminalNode, _value: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, SetValueOperationAlternate(operands: NonterminalNode, _colon: NonterminalNode, _value: NonterminalNode, _resource: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, SetValueOperationAlternate2(operands: NonterminalNode, _colon: NonterminalNode, _resource: NonterminalNode, _value: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, StatusOrItemCounterDeltaOperation(operands: NonterminalNode, _colon: NonterminalNode, _statusOrItem: NonterminalNode, _delta: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, StatusOrItemCounterSetOperation(operands: NonterminalNode, _colon: NonterminalNode, _statusOrItem: NonterminalNode, _value: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, StatusOrItemAddOperation(operands: NonterminalNode, _colon: NonterminalNode, _delta: NonterminalNode, _statusOrItem: NonterminalNode, _counter: IterationNode): Operands { return (operands as InterpreterNode).operands }, Operands(arg0: NonterminalNode): Operands { return (arg0.asIteration() as InterpreterNode).operands }, _iter(...children: readonly Node[]): Operands { return OperandSets(children.map((child) => (child as InterpreterNode).operands)) }, operand(operand: TerminalNode): Operands { return (operand as InterpreterNode).operands }, identifier(_: TerminalNode, __: NonterminalNode): Set { return OperandItems((this as InterpreterNode).identifier) }, target(_: TerminalNode): Set { return OperandItems(Target) }, source(_: TerminalNode): Set { return OperandItems(Source); }, null(_: TerminalNode): Set { return OperandItems() }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's operands value is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's operands value is`) }, }) interpreter.addAttribute("targets", { Block(start: NonterminalNode, _terminator: NonterminalNode, _code: NonterminalNode, _end: NonterminalNode, _terminator2: NonterminalNode): Operands { return (start as InterpreterNode).targets }, BlockStart(silenced: IterationNode, begin: NonterminalNode, source: IterationNode, target: IterationNode, _title: NonterminalNode): Operands { return (target as InterpreterNode).operands }, SetTargetOperation(_arg0: NonterminalNode, _arg1: NonterminalNode): Operands { return (this as InterpreterNode).operands }, _iter(): Operands { throw Error(`No idea what to say ${this.ctorName} iteration node's block targets value is`) }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's block targets value is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's block targets value is`) }, }) interpreter.addAttribute("sources", { Block(start: NonterminalNode, _terminator: NonterminalNode, _code: NonterminalNode, _end: NonterminalNode, _terminator2: NonterminalNode): Operands { return (start as InterpreterNode).sources }, BlockStart(silenced: IterationNode, begin: NonterminalNode, source: IterationNode, _target: IterationNode, _title: NonterminalNode): Operands { return (source as InterpreterNode).operands }, SetSourceOperation(_arg0: NonterminalNode, _arg1: NonterminalNode): Operands { return (this as InterpreterNode).operands }, _iter(): Operands { throw Error(`No idea what to say ${this.ctorName} iteration node's block sources value is`) }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's block sources value is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's block sources value is`) } }) interpreter.addAttribute("silenced", { Block(start: NonterminalNode, _terminator: NonterminalNode, _code: NonterminalNode, _end: NonterminalNode, _terminator2: NonterminalNode): boolean { return (start as InterpreterNode).silenced }, BlockStart(silenced: IterationNode, begin: NonterminalNode, source: IterationNode, target: IterationNode, _title: NonterminalNode): boolean { return (target as InterpreterNode).silenced }, CompleteOperation(silenced: IterationNode, _operation: NonterminalNode, _terminator: NonterminalNode): boolean { return (silenced as InterpreterNode).silenced }, silentOperator(_: TerminalNode): boolean { return true }, _iter(): boolean { if (this.isOptional() && this.children.length === 0) { return false } else if (this.isOptional() && this.children.length === 1) { return (this.children[0] as InterpreterNode).silenced } else { throw Error(`No idea what to say ${this.ctorName} iteration node's element is when there are multiple children`) } }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's silenced status is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's silenced status is`) } }) interpreter.addAttribute("currentValue", { SetValueOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, integer: NonterminalNode): number { return (integer as InterpreterNode).numberValue; }, SetValueOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, integer: NonterminalNode, _resource: NonterminalNode): number { return (integer as InterpreterNode).numberValue; }, SetValueOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, integer: NonterminalNode): number { return (integer as InterpreterNode).numberValue; }, MeteredValue(current: NonterminalNode, _max: NonterminalNode): number { return (current as InterpreterNode).numberValue }, SetMeteredOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, value: NonterminalNode): number { return (value as InterpreterNode).currentValue; }, SetMeteredOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, value: NonterminalNode, _resource: NonterminalNode): number { return (value as InterpreterNode).currentValue; }, SetMeteredOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, value: NonterminalNode): number { return (value as InterpreterNode).currentValue; }, StatusOrItemAddOperation(operands: NonterminalNode, colon: NonterminalNode, deltaOperator: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): number { return (counter as InterpreterNode).numberValue }, StatusOrItemCounterSetOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): number { return (counter as InterpreterNode).numberValue }, _iter(): never { throw Error(`No idea what to say ${this.ctorName} iteration node's current value is`) }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's current value is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's current value is`) } }) interpreter.addAttribute("maxValue", { MaxValue(_sep: TerminalNode, _num: NonterminalNode) { return (this as InterpreterNode).numberValue }, SetMaxOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, maxvalue: NonterminalNode): number { return (maxvalue as InterpreterNode).maxValue; }, SetMaxOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, maxvalue: NonterminalNode, _resource: NonterminalNode): number { return (maxvalue as InterpreterNode).maxValue; }, SetMaxOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, maxvalue: NonterminalNode): number { return (maxvalue as InterpreterNode).maxValue; }, MeteredValue(current: NonterminalNode, max: NonterminalNode): number { return (max as InterpreterNode).maxValue }, SetMeteredOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, value: NonterminalNode): number { return (value as InterpreterNode).maxValue; }, SetMeteredOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, value: NonterminalNode, _resource: NonterminalNode): number { return (value as InterpreterNode).maxValue; }, SetMeteredOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, value: NonterminalNode): number { return (value as InterpreterNode).maxValue; }, _iter(): never { throw Error(`No idea what to say ${this.ctorName} iteration node's max value is`) }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's max value is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's max value is`) } }) interpreter.addAttribute("failReason", { avoid(_: TerminalNode): FailReason.Avoid { return FailReason.Avoid }, dodge(_: TerminalNode): FailReason.Dodge { return FailReason.Dodge }, miss(_: TerminalNode): FailReason.Miss { return FailReason.Miss }, resist(_: TerminalNode): FailReason.Resist { return FailReason.Resist }, fail(_: TerminalNode): FailReason.Fail { return FailReason.Fail }, block(_: TerminalNode): FailReason.Block { return FailReason.Block }, parry(_: TerminalNode): FailReason.Parry { return FailReason.Parry }, FailReason(reason: NonterminalNode): FailReason { return (reason as InterpreterNode).failReason }, FailOperation(operands: NonterminalNode, colon: NonterminalNode, reason: NonterminalNode): FailReason { return (reason as InterpreterNode).failReason }, _iter(): never { throw Error(`No idea what to say ${this.ctorName} iteration node's fail reason is`) }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's fail reason is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's fail reason is`) } }) interpreter.addAttribute("side", { characterSide(side: NonterminalNode): CharacterSide { return (side as InterpreterNode).side }, enemySide(_: TerminalNode): CharacterSide.Enemy { return CharacterSide.Enemy }, allySide(_: TerminalNode): CharacterSide.Ally { return CharacterSide.Ally }, _iter(): never { throw Error(`No idea what to say ${this.ctorName} iteration node's loyalties are`) }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's loyalties are`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's loyalties are`) } }) interpreter.addAttribute("privacy", { Privacy(level: NonterminalNode): CharacterPrivacy { return (level as InterpreterNode).privacy }, AllyPrivacy(_: TerminalNode): CharacterPrivacy.Friend { return CharacterPrivacy.Friend }, FullScanPrivacy(_: NonterminalNode, __: NonterminalNode, ___: NonterminalNode): CharacterPrivacy.FullyScannedEnemy { return CharacterPrivacy.FullyScannedEnemy }, ScanPrivacy(_: NonterminalNode, __: NonterminalNode, ___: NonterminalNode): CharacterPrivacy.ScannedEnemy { return CharacterPrivacy.ScannedEnemy }, LightScanPrivacy(_: NonterminalNode, __: NonterminalNode, ___: NonterminalNode): CharacterPrivacy.LightlyScannedEnemy { return CharacterPrivacy.LightlyScannedEnemy }, NotScannedPrivacy(_: NonterminalNode, __: NonterminalNode, ___: NonterminalNode): CharacterPrivacy.UnscannedEnemy { return CharacterPrivacy.UnscannedEnemy }, SecretPrivacy(_: TerminalNode): CharacterPrivacy.SecretiveEnemy { return CharacterPrivacy.SecretiveEnemy }, HiddenPrivacy(_: TerminalNode): CharacterPrivacy.Hidden { return CharacterPrivacy.Hidden }, _iter(): never { throw Error(`No idea what to say ${this.ctorName} iteration node's privacy is`) }, _nonterminal(): never { throw Error(`No idea what to say ${this.ctorName} nonterminal node's privacy is`) }, _terminal(): never { throw Error(`No idea what to say ${this.ctorName} terminal node's privacy is`) } }) function EvaluateDelta(node: EvaluationNode): EvaluationContext { const ctx = node.args.ctx const operands = evaluateOperands(node.operands, ctx) const resource = node.resource const delta = node.numberValue if (delta === 0 || operands.size === 0) { return ctx } return { ...ctx, game: { ...ctx.game, characters: ctx.game.characters.map((c) => operands.has(c.id) ? CharacterResources.applyDelta(c, resource ?? MeteredResource.Health, delta) : c), clocks: ctx.game.clocks.map((c) => operands.has(c.id) ? ClockResources.applyDelta(c, resource ?? MeteredResource.Segments, delta) : c), } } } function EvaluateMeteredSet(node: EvaluationNode): EvaluationContext { const ctx = node.args.ctx const operands = evaluateOperands(node.operands, ctx) const resource = node.resource const current = node.currentValue const max = node.maxValue if (operands.size === 0) { return ctx } return { ...ctx, game: { ...ctx.game, characters: ctx.game.characters.map((c) => operands.has(c.id) ? CharacterResources.setMetered(c, resource ?? MeteredResource.Health, current, max) : c), clocks: ctx.game.clocks.map((c) => operands.has(c.id) ? ClockResources.setMetered(c, resource ?? MeteredResource.Segments, current, max) : c), } } } function EvaluateValueSet(node: EvaluationNode): EvaluationContext { const ctx = node.args.ctx const operands = evaluateOperands(node.operands, ctx) const resource = node.resource const current = node.currentValue if (operands.size === 0) { return ctx } return { ...ctx, game: { ...ctx.game, characters: ctx.game.characters.map((c) => operands.has(c.id) ? CharacterResources.setValue(c, resource ?? MeteredResource.Health, current) : c), clocks: ctx.game.clocks.map((c) => operands.has(c.id) ? ClockResources.setValue(c, resource ?? MeteredResource.Segments, current) : c), } } } function EvaluateMaxSet(node: EvaluationNode): EvaluationContext { const ctx = node.args.ctx const operands = evaluateOperands(node.operands, ctx) const resource = node.resource const max = node.maxValue if (operands.size === 0) { return ctx } return { ...ctx, game: { ...ctx.game, characters: ctx.game.characters.map((c) => operands.has(c.id) ? CharacterResources.setMax(c, resource ?? MeteredResource.Health, max) : c), clocks: ctx.game.clocks.map((c) => operands.has(c.id) ? ClockResources.setMax(c, resource ?? MeteredResource.Segments, max) : c), } } } function EvaluateClear(node: EvaluationNode): EvaluationContext { const ctx = node.args.ctx const operands = evaluateOperands(node.operands, ctx) const resource = node.resource if (operands.size === 0 || !resource) { return ctx } return { ...ctx, game: { ...ctx.game, characters: ctx.game.characters.map((c) => operands.has(c.id) ? CharacterResources.clear(c, resource) : c), clocks: ctx.game.clocks.map((c) => operands.has(c.id) ? ClockResources.clear(c, resource) : c), } } } interpreter.addOperation("evaluate(ctx)", { CodeSegment(items: IterationNode): EvaluationContext { let ctx = (this as EvaluationNode).args.ctx return (items as InterpreterNode).evaluate(ctx) }, EmptyLines(_: TerminalNode): EvaluationContext { // Has no effect return (this as EvaluationNode).args.ctx }, Block(start: NonterminalNode, terminator: NonterminalNode, code: NonterminalNode, _end: NonterminalNode, _finalTerminator: NonterminalNode): EvaluationContext { let ctx = (this as EvaluationNode).args.ctx ctx = (start as InterpreterNode).evaluate(ctx) ctx = (code as InterpreterNode).evaluate(ctx) return ctx }, BlockStart(silentOperator: IterationNode, beginKeyword: NonterminalNode, source: IterationNode, target: IterationNode, _title: IterationNode): EvaluationContext { let ctx = (this as EvaluationNode).args.ctx ctx = (source as InterpreterNode).evaluate(ctx) ctx = (target as InterpreterNode).evaluate(ctx) return ctx }, CompleteOperation(silenced: IterationNode, operation: NonterminalNode, _terminator: NonterminalNode): EvaluationContext { let ctx = (this as EvaluationNode).args.ctx ctx = (operation as InterpreterNode).evaluate(ctx) return ctx }, Operation(operator: NonterminalNode): EvaluationContext { let ctx = (this as EvaluationNode).args.ctx return (operator as InterpreterNode).evaluate(ctx) }, DeltaOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: NonterminalNode, _arg5: IterationNode, _arg6: IterationNode): EvaluationContext { return EvaluateDelta(this as EvaluationNode) }, DeltaOperationAlternate(_operators: NonterminalNode, _colon: NonterminalNode, _delta: NonterminalNode, _affinity: IterationNode, _res: IterationNode, _element: IterationNode): EvaluationContext { return EvaluateDelta(this as EvaluationNode) }, DeltaOperationAlternate2(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: IterationNode, _arg5: IterationNode): EvaluationContext { return EvaluateDelta(this as EvaluationNode) }, SetMeteredOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: NonterminalNode): EvaluationContext { return EvaluateMeteredSet(this as EvaluationNode) }, SetMeteredOperationAlternate(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): EvaluationContext { return EvaluateMeteredSet(this as EvaluationNode) }, SetMeteredOperationAlternate2(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): EvaluationContext { return EvaluateMeteredSet(this as EvaluationNode) }, SetValueOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: NonterminalNode): EvaluationContext { return EvaluateValueSet(this as EvaluationNode); }, SetValueOperationAlternate(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): EvaluationContext { return EvaluateValueSet(this as EvaluationNode); }, SetValueOperationAlternate2(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): EvaluationContext { return EvaluateValueSet(this as EvaluationNode); }, SetMaxOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: NonterminalNode): EvaluationContext { return EvaluateMaxSet(this as EvaluationNode); }, SetMaxOperationAlternate(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): EvaluationContext { return EvaluateMaxSet(this as EvaluationNode); }, SetMaxOperationAlternate2(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): EvaluationContext { return EvaluateMaxSet(this as EvaluationNode); }, StatusOrItemAddOperation(_operators: NonterminalNode, _colon: NonterminalNode, _sign: NonterminalNode, _identifier: NonterminalNode, _counter: IterationNode): EvaluationContext { const ctx = (this as EvaluationNode).args.ctx const operands = evaluateOperands((this as InterpreterNode).operands, ctx) const statusOrItemId = (this as InterpreterNode).identifier const stacks = (this as InterpreterNode).numberValue return { ...ctx, game: { ...ctx.game, characters: ctx.game.characters.map((c) => operands.has(c.id) ? CharacterStatuses.addStatus(c, statusOrItemId, stacks) : c), } } }, ClearOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: NonterminalNode): EvaluationContext { return EvaluateClear(this as EvaluationNode); }, ClearOperationAlternate(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): EvaluationContext { return EvaluateClear(this as EvaluationNode); }, ClearOperationAlternate2(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): EvaluationContext { return EvaluateClear(this as EvaluationNode); }, SetSourceOperation(_: TerminalNode, __: NonterminalNode): EvaluationContext { let ctx = (this as EvaluationNode).args.ctx return { ...ctx, source: [...evaluateOperands((this as InterpreterNode).operands, ctx)] } }, SetTargetOperation(_: TerminalNode, __: NonterminalNode): EvaluationContext { let ctx = (this as EvaluationNode).args.ctx return { ...ctx, target: [...evaluateOperands((this as InterpreterNode).operands, ctx)] } }, _iter(...children: readonly Node[]): EvaluationContext { let ctx = (this as EvaluationNode).args.ctx for (const child of children) { ctx = (child as InterpreterNode).evaluate(ctx) } return ctx }, _nonterminal(): never { throw Error(`No idea how to evaluate ${this.ctorName} nonterminal node.`) }, _terminal(): never { throw Error(`No idea how to evaluate ${this.ctorName} terminal node.`) } }) function RenderDelta(node: MarkdownNode): MarkdownOutput { const ctx = node.args.ctx const result: string[] = [] const resource = node.resource const delta = node.numberValue const affinity = node.affinity const element = node.element const operandsBefore = lookupOperands(node.operands, ctx) const ctxAfter = EvaluateDelta(node) const operandsAfter = lookupOperands(node.operands, ctxAfter) for (const [id, after] of operandsAfter) { const before = operandsBefore.get(id) if (!before) { throw Error("Somehow an object appeared?") } switch (before.type) { case "character": if (after.type !== "character") { throw Error(`character ${id} somehow changed type between calls?`) } const beforeChar = before.character const afterChar = after.character const charName = (afterChar.privacy ? applyCharacterPrivacy(afterChar) : afterChar)?.name if (resource === MeteredResource.Zero && typeof afterChar.zp === "number" && typeof afterChar.maxZp === "number" && afterChar.maxZp > 0) { result.push(`${charName}: [Zero Charge: _${(100 * afterChar.zp / afterChar.maxZp).toFixed(0)}%_]`) } else { result.push(`${charName}: [${formatResourceDelta(resource ?? MeteredResource.Health, affinity, delta)}]${ element !== null ? " " + element : ""}`) } switch (resource) { case MeteredResource.Health: case null: if (beforeChar.health !== afterChar.health && typeof afterChar.health !== "undefined") { result.push(`${charName}: [**${afterChar.health}**]`) } break case MeteredResource.Zero: if (isZpReady(beforeChar) === false && isZpReady(afterChar) === true) { result.push(`${charName}: [**Zero Power Ready**]`) } break case MeteredResource.Experience: const oldLevel = beforeChar.level const newLevel = afterChar.level if (typeof oldLevel === "number" && typeof newLevel === "number" && oldLevel !== newLevel) { const levelDelta = newLevel - oldLevel if (levelDelta < 0) { result.push(`${charName}: [_Level Down...${levelDelta < -1 ? ` x${-levelDelta}` : ""}_]`) } else { result.push(`${charName}: [**Level Up!!${levelDelta > 1 ? ` x${levelDelta}` : ""}**]`) } } break } break case "clock": if (after.type !== "clock") { throw Error(`clock ${id} somehow changed type between calls?`) } const afterClock = after.clock const clockText = afterClock.text result.push(`${clockText}: [${formatResourceDelta(resource ?? MeteredResource.Segments, affinity, delta)}]${ element !== null ? " " + element : ""}`) break default: throw Error(`Don't know how to render delta on ${before.type} ${id}`) } } return { ...ctxAfter, output: result.length > 0 ? result.join("\n") : null } } function RenderMeteredSet(node: MarkdownNode): MarkdownOutput { const ctx = node.args.ctx const result: string[] = [] const resource = node.resource const newValue = node.currentValue const newMax = node.maxValue const operandsBefore = lookupOperands(node.operands, ctx) const ctxAfter = EvaluateDelta(node) const operandsAfter = lookupOperands(node.operands, ctxAfter) for (const [id, after] of operandsAfter) { const before = operandsBefore.get(id) if (!before) { throw Error("Somehow an object appeared?") } switch (before.type) { case "character": if (after.type !== "character") { throw Error(`character ${id} somehow changed type between calls?`) } if (resource && !isMeteredResource(resource)) { throw Error(`somehow got non-metered resource in metered set`) } const beforeChar = before.character const afterChar = after.character const charName = (afterChar.privacy ? applyCharacterPrivacy(afterChar) : afterChar)?.name if (resource === MeteredResource.Zero && newMax > 0) { result.push(`${charName}: [Zero Charge: _${(100 * newValue / newMax).toFixed(0)}%_]`) } else { result.push(`${charName}: [${formatMeteredResource(resource ?? MeteredResource.Health, newValue, newMax)}]`) } switch (resource) { case MeteredResource.Health: case null: if (beforeChar.health !== afterChar.health && typeof afterChar.health !== "undefined") { result.push(`${charName}: [**${afterChar.health}**]`) } break case MeteredResource.Zero: if (isZpReady(beforeChar) === false && isZpReady(afterChar) === true) { result.push(`${charName}: [**Zero Power Ready**]`) } break case MeteredResource.Experience: const oldLevel = beforeChar.level const newLevel = afterChar.level if (typeof oldLevel === "number" && typeof newLevel === "number" && oldLevel !== newLevel) { const levelDelta = newLevel - oldLevel if (levelDelta < 0) { result.push(`${charName}: [_Level Down...${levelDelta < -1 ? ` x${-levelDelta}` : ""}_]`) } else { result.push(`${charName}: [**Level Up!!${levelDelta > 1 ? ` x${levelDelta}` : ""}**]`) } } break } break case "clock": if (after.type !== "clock") { throw Error(`clock ${id} somehow changed type between calls?`) } const afterClock = after.clock const clockText = afterClock.text if (resource && !isMeteredResource(resource)) { throw Error(`somehow got non-metered resource in metered set`) } result.push(`${clockText}: [${formatMeteredResource(resource ?? MeteredResource.Segments, newValue, newMax)}`) break default: throw Error(`Don't know how to render metered set on ${before.type} ${id}`) } } return { ...ctxAfter, output: result.length > 0 ? result.join("\n") : null } } function RenderValueSet(node: MarkdownNode): MarkdownOutput { const ctx = node.args.ctx const result: string[] = [] const resource = node.resource const newValue = node.currentValue const operandsBefore = lookupOperands(node.operands, ctx) const ctxAfter = EvaluateDelta(node) const operandsAfter = lookupOperands(node.operands, ctxAfter) for (const [id, after] of operandsAfter) { const before = operandsBefore.get(id) if (!before) { throw Error("Somehow an object appeared?") } switch (before.type) { case "character": if (after.type !== "character") { throw Error(`character ${id} somehow changed type between calls?`) } const beforeChar = before.character const afterChar = after.character const charName = (afterChar.privacy ? applyCharacterPrivacy(afterChar) : afterChar)?.name if (resource === MeteredResource.Zero && isDefined(afterChar.maxZp) && afterChar.maxZp > 0) { result.push(`${charName}: [Zero Charge: _${(100 * newValue / afterChar.maxZp).toFixed(0)}%_]`) } else { result.push(`${charName}: [${formatResourceValue(resource ?? MeteredResource.Health, newValue)}]`) } switch (resource) { case MeteredResource.Health: case null: if (beforeChar.health !== afterChar.health && typeof afterChar.health !== "undefined") { result.push(`${charName}: [**${afterChar.health}**]`) } break case MeteredResource.Zero: if (isZpReady(beforeChar) === false && isZpReady(afterChar) === true) { result.push(`${charName}: [**Zero Power Ready**]`) } break case MeteredResource.Experience: const oldLevel = beforeChar.level const newLevel = afterChar.level if (typeof oldLevel === "number" && typeof newLevel === "number" && oldLevel !== newLevel) { const levelDelta = newLevel - oldLevel if (levelDelta < 0) { result.push(`${charName}: [_Level Down...${levelDelta < -1 ? ` x${-levelDelta}` : ""}_]`) } else { result.push(`${charName}: [**Level Up!!${levelDelta > 1 ? ` x${levelDelta}` : ""}**]`) } } break } break case "clock": if (after.type !== "clock") { throw Error(`clock ${id} somehow changed type between calls?`) } const afterClock = after.clock const clockText = afterClock.text result.push(`${clockText}: [${formatResourceValue(resource ?? MeteredResource.Segments, newValue)}`) break default: throw Error(`Don't know how to render metered set on ${before.type} ${id}`) } } return { ...ctxAfter, output: result.length > 0 ? result.join("\n") : null } } function RenderMaxSet(node: MarkdownNode): MarkdownOutput { const ctx = node.args.ctx const result: string[] = [] const resource = node.resource const newMax = node.maxValue const operandsBefore = lookupOperands(node.operands, ctx) const ctxAfter = EvaluateDelta(node) const operandsAfter = lookupOperands(node.operands, ctxAfter) for (const [id, after] of operandsAfter) { const before = operandsBefore.get(id) if (!before) { throw Error("Somehow an object appeared?") } switch (before.type) { case "character": if (after.type !== "character") { throw Error(`character ${id} somehow changed type between calls?`) } if (resource && !isMeteredResource(resource)) { throw Error(`somehow got non-metered resource in metered set`) } const beforeChar = before.character const afterChar = after.character const charName = (afterChar.privacy ? applyCharacterPrivacy(afterChar) : afterChar)?.name if (resource === MeteredResource.Zero && isDefined(afterChar.zp)) { result.push(`${charName}: [Zero Charge: _${(100 * afterChar.zp / newMax).toFixed(0)}%_]`) } else { result.push(`${charName}: [${formatResourceMax(resource ?? MeteredResource.Health, newMax)}]`) } switch (resource) { case MeteredResource.Health: case null: if (beforeChar.health !== afterChar.health && typeof afterChar.health !== "undefined") { result.push(`${charName}: [**${afterChar.health}**]`) } break case MeteredResource.Zero: if (isZpReady(beforeChar) === false && isZpReady(afterChar) === true) { result.push(`${charName}: [**Zero Power Ready**]`) } break case MeteredResource.Experience: const oldLevel = beforeChar.level const newLevel = afterChar.level if (typeof oldLevel === "number" && typeof newLevel === "number" && oldLevel !== newLevel) { const levelDelta = newLevel - oldLevel if (levelDelta < 0) { result.push(`${charName}: [_Level Down...${levelDelta < -1 ? ` x${-levelDelta}` : ""}_]`) } else { result.push(`${charName}: [**Level Up!!${levelDelta > 1 ? ` x${levelDelta}` : ""}**]`) } } break } break case "clock": if (after.type !== "clock") { throw Error(`clock ${id} somehow changed type between calls?`) } const afterClock = after.clock const clockText = afterClock.text if (resource && !isMeteredResource(resource)) { throw Error(`somehow got non-metered resource in metered set`) } result.push(`${clockText}: [${formatResourceMax(resource ?? MeteredResource.Segments, newMax)}`) break default: throw Error(`Don't know how to render metered set on ${before.type} ${id}`) } } return { ...ctxAfter, output: result.length > 0 ? result.join("\n") : null } } function RenderClear(node: MarkdownNode): MarkdownOutput { const ctx = node.args.ctx const result: string[] = [] const resource = node.resource const newMax = node.maxValue const operandsBefore = lookupOperands(node.operands, ctx) const ctxAfter = EvaluateDelta(node) const operandsAfter = lookupOperands(node.operands, ctxAfter) for (const [id, after] of operandsAfter) { const before = operandsBefore.get(id) if (!before) { throw Error("Somehow an object appeared?") } switch (before.type) { case "character": if (after.type !== "character") { throw Error(`character ${id} somehow changed type between calls?`) } const afterChar = after.character const charName = (afterChar.privacy ? applyCharacterPrivacy(afterChar) : afterChar)?.name result.push(`${charName}: [${resource} -]`) break case "clock": if (after.type !== "clock") { throw Error(`clock ${id} somehow changed type between calls?`) } const afterClock = after.clock const clockText = afterClock.text if (resource && !isMeteredResource(resource)) { throw Error(`somehow got non-metered resource in metered set`) } result.push(`${clockText}: [${formatResourceMax(resource ?? MeteredResource.Segments, newMax)}`) break default: throw Error(`Don't know how to render metered set on ${before.type} ${id}`) } } return { ...ctxAfter, output: result.length > 0 ? result.join("\n") : null } } interpreter.addOperation("renderMarkdown(ctx)", { CodeSegment(items: IterationNode): MarkdownOutput { let ctx = (this as MarkdownNode).args.ctx return (items as InterpreterNode).renderMarkdown(ctx) }, EmptyLines(_items: IterationNode): MarkdownOutput { let ctx = (this as MarkdownNode).args.ctx return {...ctx, output: null} }, DeltaOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: NonterminalNode, _arg5: IterationNode, _arg6: IterationNode): MarkdownOutput { return RenderDelta(this as MarkdownNode) }, DeltaOperationAlternate(_operators: NonterminalNode, _colon: NonterminalNode, _delta: NonterminalNode, _affinity: IterationNode, _res: IterationNode, _element: IterationNode): MarkdownOutput { return RenderDelta(this as MarkdownNode) }, DeltaOperationAlternate2(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: IterationNode, _arg5: IterationNode): MarkdownOutput { return RenderDelta(this as MarkdownNode) }, SetMeteredOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: NonterminalNode): MarkdownOutput { return RenderMeteredSet(this as MarkdownNode) }, SetMeteredOperationAlternate(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): MarkdownOutput { return RenderMeteredSet(this as MarkdownNode) }, SetMeteredOperationAlternate2(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): MarkdownOutput { return RenderMeteredSet(this as MarkdownNode) }, SetValueOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: NonterminalNode): MarkdownOutput { return RenderValueSet(this as MarkdownNode); }, SetValueOperationAlternate(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): MarkdownOutput { return RenderValueSet(this as MarkdownNode); }, SetValueOperationAlternate2(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): MarkdownOutput { return RenderValueSet(this as MarkdownNode); }, SetMaxOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: NonterminalNode): MarkdownOutput { return RenderMaxSet(this as MarkdownNode); }, SetMaxOperationAlternate(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): MarkdownOutput { return RenderMaxSet(this as MarkdownNode); }, SetMaxOperationAlternate2(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): MarkdownOutput { return RenderMaxSet(this as MarkdownNode); }, ClearOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: NonterminalNode): MarkdownOutput { return RenderClear(this as MarkdownNode); }, ClearOperationAlternate(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): MarkdownOutput { return RenderClear(this as MarkdownNode); }, ClearOperationAlternate2(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): MarkdownOutput { return RenderClear(this as MarkdownNode); }, _iter(...children: readonly Node[]): MarkdownOutput { let ctx = (this as MarkdownNode).args.ctx const output = [] for (const child of children) { const result = (child as InterpreterNode).renderMarkdown(ctx) if (result.output !== null) { output.push(result.output) } ctx = result } return { ...ctx, output: output.join('\n') } }, _nonterminal(): never { throw Error(`No idea how to evaluate ${this.ctorName} nonterminal node for markdown.`) }, _terminal(): never { throw Error(`No idea how to evaluate ${this.ctorName} terminal node for markdown.`) } }) export function evaluate(state: GameState, timestamp: number, code: string): GameState { const codeMatch = grammar.match(code, "CodeSegment") const context: EvaluationContext = { timestamp, game: state, target: [], source: [], } const result = (interpreter(codeMatch) as InterpreterNode).evaluate(context) return result.game }