diff --git a/public/api/current.json b/public/api/current.json index 0a2cd81..d46061b 100644 --- a/public/api/current.json +++ b/public/api/current.json @@ -29,8 +29,17 @@ "spType": "Fabula", "portraitUrl": "/portraits/aelica.png", "turnsTotal": 1, - "turnsLeft": 1 + "turnsLeft": 1, + "statuses": [] }, + { + "id": "flow", + "side": "ally", + "minion": true, + "name": "Flow", + "portraitUrl": "/portraits/flow.png", + "statuses": [] + }, { "id": "athetyz", "side": "ally", @@ -49,14 +58,22 @@ "spType": "Fabula", "portraitUrl": "/portraits/athetyz.png", "turnsTotal": 1, - "turnsLeft": 0 + "turnsLeft": 0, + "statuses": [] + }, + { + "id": "galvelle", + "side": "ally", + "minion": true, + "name": "Galvelle", + "statuses": [] }, { "id": "echo", "side": "ally", "name": "Echo", "level": 32, - "hp": 67, + "hp": 1, "maxHp": 67, "mp": 62, "maxMp": 62, @@ -69,7 +86,23 @@ "spType": "Fabula", "portraitUrl": "/portraits/echo.png", "turnsTotal": 1, - "turnsLeft": 1 + "turnsLeft": 1, + "statuses": [] + }, + { + "id": "gale", + "side": "ally", + "minion": true, + "name": "Gale", + "level": 5, + "hp": 70, + "maxHp": 70, + "mp": 58, + "maxMp": 58, + "portraitUrl": "/portraits/gale.png", + "turnsTotal": 1, + "turnsLeft": 1, + "statuses": [] }, { "id": "gravitas", @@ -89,7 +122,16 @@ "spType": "Fabula", "portraitUrl": "/portraits/gravitas.png", "turnsTotal": 1, - "turnsLeft": 1 + "turnsLeft": 1, + "statuses": [] + }, + { + "id": "calor", + "side": "ally", + "minion": true, + "name": "Calor", + "portraitUrl": "/portraits/calor.png", + "statuses": [] }, { "id": "linnet", @@ -109,14 +151,23 @@ "spType": "Fabula", "portraitUrl": "/portraits/linnet.png", "turnsTotal": 1, - "turnsLeft": 1 + "turnsLeft": 1, + "statuses": [] + }, + { + "id": "terra", + "side": "ally", + "minion": true, + "name": "Terra", + "portraitUrl": "/portraits/terra.png", + "statuses": [] }, { "id": "prandia", "side": "ally", "name": "Prandia", "level": 32, - "hp": 90, + "hp": 37, "maxHp": 90, "koText": "rip lmao", "mp": 0, @@ -131,67 +182,15 @@ "portraitUrl": "/portraits/prandia.png", "turnsTotal": 1, "turnsLeft": 1, - "statuses": [ - { - "id": "digested", - "name": "Digested", - "count": 3, - "description": "Is currently being melted down for sustenance." - }, - { - "id": "digested2", - "name": "Digested", - "count": 3, - "description": "Is currently being melted down for sustenance." - }, - { - "id": "digested3", - "name": "Digested", - "count": 3, - "description": "Is currently being melted down for sustenance." - }, - { - "id": "digested4", - "name": "Digested", - "count": 3, - "description": "Is currently being melted down for sustenance." - }, - { - "id": "digested5", - "name": "Digested", - "count": 3, - "description": "Is currently being melted down for sustenance." - }, - { - "id": "digested6", - "name": "Digested", - "count": 3, - "description": "Is currently being melted down for sustenance." - }, - { - "id": "digested7", - "name": "Digested", - "count": 3, - "description": "Is currently being melted down for sustenance." - }, - { - "id": "digested8", - "name": "Digested", - "count": 3, - "description": "Is currently being melted down for sustenance." - }, - { - "id": "digested9", - "name": "Digested", - "count": 3, - "description": "Is currently being melted down for sustenance." - }, - { - "id": "digested10", - "name": "Digested", - "count": 3, - "description": "Is currently being melted down for sustenance." - }] + "statuses": [] + }, + { + "id": "selia", + "side": "ally", + "minion": true, + "name": "Sélia", + "portraitUrl": "/portraits/selia.png", + "statuses": [] }, { "id": "werespider", @@ -202,7 +201,34 @@ "sp": 5, "turnsLeft": 3, "turnsTotal": 3 + }, + { + "id": "kaumari", + "side": "enemy", + "name": "Kaumari", + "level": 600, + "health": "Full", + "spType": "Ultima", + "sp": 15, + "turnsLeft": 6, + "turnsTotal": 6, + "portraitUrl": "/portraits/kaumari.png" + }, + { + "id": "amita", + "side": "enemy", + "name": "Amita", + "level": 50, + "health": "Full", + "turnsLeft": 6, + "turnsTotal": 6, + "portraitUrl": "/portraits/amita.png" } ], - "statuses": [] + "statuses": [{ + "id": "Digesting", + "name": "Digesting", + "iconUrl": "/icons/devoured.webp", + "description": "Is currently being melted down for sustenance." + }] } \ No newline at end of file diff --git a/src/grammar/grammar.ohm b/src/grammar/grammar.ohm index f19ca66..93d8d70 100644 --- a/src/grammar/grammar.ohm +++ b/src/grammar/grammar.ohm @@ -1,69 +1,85 @@ FabulaDSL { + // CodeSegment: evaluate, renderMarkdown CodeSegment = (Block | CompleteOperation | EmptyLines)* + // identifier: identifier, operands, textValue identifier (an identifier) = identifierStart identifierContinue+ identifierStart = "_" | "$" | letter identifierContinue = identifierStart | digit newline = "\n" + // EmptyLines: evaluate (empty), renderMarkdown (empty) EmptyLines = operationTerminator+ wordSep = ~newline space spaces := (wordSep|blockComment)* + // target: operands target = "@" + // source: operands source = "&" null = "-" colon = ":" + // operand: operands operand (an operand) = identifier | target | source - comma = "," + comma = spaces "," spaces + // Operands: operands Operands (operands) = nonemptyListOf + // segments: resource segments = caseInsensitive<"segments">|caseInsensitive<"sections"> |caseInsensitive<"segment">|caseInsensitive<"section"> |caseInsensitive<"ticks">|caseInsensitive<"tick"> - |caseInsensitive<"clock"> - + |caseInsensitive<"steps">|caseInsensitive<"step"> + SegmentResource = segments points = caseInsensitive<"points">|caseInsensitive<"pts">|caseInsensitive<"power"> experience = caseInsensitive<"experience"> xp = caseInsensitive<"XP">|caseInsensitive<"EXP"> ExperienceFull = experience points + // XpResource: resource XpResource = ExperienceFull|experience|xp health = caseInsensitive<"health">|caseInsensitive<"life">|caseInsensitive<"hit"> HealthFull = health points hp = caseInsensitive<"HP"> + // HpResource: resource HpResource = HealthFull|health|hp mind = caseInsensitive<"mind">|caseInsensitive<"magic">|caseInsensitive<"mana"> MindFull = mind points mp = caseInsensitive<"MP"> + // MpResource: resource MpResource = MindFull|mind|mp item = caseInsensitive<"items">|caseInsensitive<"item">|caseInsensitive<"inventory"> ItemFull = item points ip = caseInsensitive<"IP"> + // IpResource: resource IpResource = ItemFull|item|ip zero = caseInsensitive<"zero"> charge = points|caseInsensitive<"charge">|segments ZeroFull = zero charge zp = caseInsensitive<"ZP">|caseInsensitive<"ZC"> + // ZpResource: resource ZpResource = ZeroFull|zero|zp blood = caseInsensitive<"blood">|caseInsensitive<"grave"> BloodFull = blood points bp = caseInsensitive<"BP"> + // BpResource: resource BpResource = BloodFull|blood|bp turns = caseInsensitive<"turns">|caseInsensitive<"turn">|caseInsensitive<"actions">|caseInsensitive<"action"> TurnsFull = turns points tp = caseInsensitive<"TP">|caseInsensitive<"AP"> + // TpResource: resource TpResource = TurnsFull|turns|tp - MeteredResource (the name of a metered resource) = segments|XpResource|HpResource|MpResource + // MeteredResource: resource + MeteredResource (the name of a metered resource) = SegmentResource|XpResource|HpResource|MpResource |IpResource|ZpResource|BpResource|TpResource fabula = caseInsensitive<"Fabula"> @@ -75,28 +91,41 @@ FabulaDSL { FabulaFull = fabula points UltimaFull = ultima points SpecialFull = special points + // FabulaResource, UltimaResource, SpecialResource, SpResource: resource FabulaResource = FabulaFull|fabula|fp UltimaResource = UltimaFull|ultima|up SpecialResource = SpecialFull|special|sp SpResource = FabulaResource|UltimaResource|SpecialResource + // LevelResource: resource LevelResource = caseInsensitive<"Level">|caseInsensitive<"lvl">|caseInsensitive<"lv"> money = caseInsensitive<"Zenit">|caseInsensitive<"z">|caseInsensitive<"gold">|caseInsensitive<"gil">|caseInsensitive<"gp"> + // MoneyResource: resource MoneyResource = money materials = caseInsensitive<"materials">|caseInsensitive<"mats"> - MaterialsFull = money wordSep materials + MaterialsFull = money #wordSep materials + // MaterialsResource: resource MaterialsResource = MaterialsFull|materials - UnmeteredResource (the name of an unmetered resource) = SpResource|LevelResource|MaterialsResource|MoneyResource + // OrderResource: resource + OrderResource = caseInsensitive<"order">|caseInsensitive<"index"> + + // UnmeteredResource: resource + UnmeteredResource (the name of an unmetered resource) = SpResource|LevelResource|MaterialsResource|MoneyResource|OrderResource + // Resource: resource Resource (the name of a resource) = MeteredResource|UnmeteredResource + // plus, minus, deltaOperator: sign plus = "+" minus = "-" deltaOperator = plus|minus + // integer: numberValue integer (an integer) = digit+ + // delta: numberValue, sign Delta (a delta) = deltaOperator integer + // elements: element fireElement = caseInsensitive<"fire">|caseInsensitive<"flame">|caseInsensitive<"burn"> waterElement = caseInsensitive<"water">|caseInsensitive<"poison">|caseInsensitive<"acid"> lightningElement = caseInsensitive<"lightning">|caseInsensitive<"zap">|caseInsensitive<"electricity"> @@ -116,6 +145,7 @@ FabulaDSL { |lightElement|darkElement |physicalElement|nonElement|healingElement + // affinities: affinity absorbAffinity = "??"|caseInsensitive<"drained">|caseInsensitive<"drains">|caseInsensitive<"drain"> |caseInsensitive<"absorbed">|caseInsensitive<"absorbs">|caseInsensitive<"absorb"> immuneAffinity = "."|caseInsensitive<"immunity">|caseInsensitive<"blocked">|caseInsensitive<"blocks">|caseInsensitive<"immune">|caseInsensitive<"block"> @@ -125,31 +155,48 @@ FabulaDSL { affinity (an affinity indicator) = vulnerableAffinity|resistAffinity|immuneAffinity|absorbAffinity|normalAffinity meteredSeparator = ~lineCommentStart ~blockCommentStart "/" - MeteredValue = integer MaxValue + // MaxValue: numberValue, maxValue MaxValue = meteredSeparator integer + // MeteredValue: currentValue, maxValue + MeteredValue = integer MaxValue - DeltaOperation = Operands wordSep Resource colon Delta affinity? elementalType? + // DeltaOperation/Alternate/Alternate2: operands, resource, numberValue, affinity, element, evaluate, renderMarkdown + DeltaOperation = Operands #wordSep Resource colon Delta affinity? elementalType? DeltaOperationAlternate = Operands colon Delta affinity? Resource? elementalType? DeltaOperationAlternate2 = Operands colon Resource Delta affinity? elementalType? - SetMeteredOperation = Operands wordSep MeteredResource colon MeteredValue + + // SetMeteredOperation/Alternate/Alternate2: operands, resource, currentValue, maxValue, evaluate, renderMarkdown + SetMeteredOperation = Operands #wordSep MeteredResource colon MeteredValue SetMeteredOperationAlternate = Operands colon MeteredValue MeteredResource SetMeteredOperationAlternate2 = Operands colon MeteredResource MeteredValue - SetValueOperation = Operands wordSep Resource colon integer + + // SetValueOperation/Alternate/Alternate2: operands, resource, numberValue, currentValue, evaluate, renderMarkdown + SetValueOperation = Operands #wordSep Resource colon integer SetValueOperationAlternate = Operands colon integer Resource SetValueOperationAlternate2 = Operands colon Resource integer - SetMaxOperation = Operands wordSep MeteredResource colon MaxValue + + // SetMaxOperation/Alternate/Alternate2: operands, resource, numberValue, maxValue, evaluate, renderMarkdown + SetMaxOperation = Operands #wordSep MeteredResource colon MaxValue SetMaxOperationAlternate = Operands colon MaxValue MeteredResource SetMaxOperationAlternate2 = Operands colon MeteredResource MaxValue - ClearOperation = Operands wordSep Resource colon null + + // ClearOperation/Alternate/Alternate2: operands, resource, evaluate, renderMarkdown + ClearOperation = Operands #wordSep Resource colon null ClearOperationAlternate = Operands colon null Resource ClearOperationAlternate2 = Operands colon Resource null + // TODO: continue implementation list from here + StatusOrItemCounterUnwrapped = "x"? integer StatusOrItemCounterWrapped = "(" StatusOrItemCounterUnwrapped ")" StatusOrItemCounter = StatusOrItemCounterWrapped | StatusOrItemCounterUnwrapped - StatusOrItemDeltaOperation = Operands colon deltaOperator identifier StatusOrItemCounter? + StatusOrItemDeltaWrapped = "(" Delta ")" + StatusOrItemDelta = StatusOrItemDeltaWrapped | Delta + StatusOrItemAddOperation = Operands colon plus identifier StatusOrItemCounter? + StatusOrItemRemoveOperation = Operands colon minus identifier StatusOrItemCounterDeltaOperation = Operands colon identifier Delta - StatusOrItemCounterSetOperation = Operands colon identifier StatusOrItemCounter + StatusOrItemCounterDeltaOperationAlternate = Operands colon Delta identifier + StatusOrItemCounterSetOperation = Operands colon identifier StatusOrItemCounter? notNewlineOrComment = ~newline ~lineCommentStart ~blockCommentStart any textToEndOfLine = notNewlineOrComment+ @@ -159,12 +206,13 @@ FabulaDSL { avoid = caseInsensitive<"avoided">|caseInsensitive<"avoids">|caseInsensitive<"avoid"> dodge = caseInsensitive<"dodged">|caseInsensitive<"dodges">|caseInsensitive<"dodge"> + immune = caseInsensitive<"immune"> miss = caseInsensitive<"missed">|caseInsensitive<"misses">|caseInsensitive<"miss"> resist = caseInsensitive<"resisted">|caseInsensitive<"resists">|caseInsensitive<"resist"> fail = caseInsensitive<"failed">|caseInsensitive<"fails">|caseInsensitive<"fail"> block = caseInsensitive<"blocked">|caseInsensitive<"blocks">|caseInsensitive<"block"> parry = caseInsensitive<"parried">|caseInsensitive<"parries">|caseInsensitive<"parry"> - FailReason = avoid|dodge|miss|resist|fail|block|parry + FailReason = avoid|dodge|immune|miss|resist|fail|block|parry FailOperation = Operands colon FailReason allySide = caseInsensitive<"allies">|caseInsensitive<"ally"> @@ -177,6 +225,77 @@ FabulaDSL { |caseInsensitive<"opponents">|caseInsensitive<"opponent">|caseInsensitive<"opposed"> characterSide = allySide|enemySide + AllyPrivacy = caseInsensitive<"ally">|caseInsensitive<"friendly">|caseInsensitive<"friend"> + full = caseInsensitive<"fully">|caseInsensitive<"full"> + average = caseInsensitive<"normally">|caseInsensitive<"average">|caseInsensitive<"normal"> + light = caseInsensitive<"lightly">|caseInsensitive<"light"> + scan = caseInsensitive<"scanned">|caseInsensitive<"scan"> + notPrefix = caseInsensitive<"un">|caseInsensitive<"non">|caseInsensitive<"not"> + secretive = caseInsensitive<"secretive">|caseInsensitive<"secret"> + hidden = caseInsensitive<"hidden"> + optionalDash = (space|"-")? + FullScanPrivacy = full optionalDash scan + ScanPrivacy = (average optionalDash)? scan + LightScanPrivacy = light optionalDash scan + NotScannedPrivacy = notPrefix optionalDash scan + SecretPrivacy = secretive + HiddenPrivacy = hidden + Privacy = AllyPrivacy|FullScanPrivacy|ScanPrivacy|LightScanPrivacy|NotScannedPrivacy|SecretPrivacy|HiddenPrivacy + + count = caseInsensitive<"counting">|caseInsensitive<"count">|caseInsensitive<"ticking">|caseInsensitive<"tick"> + upward = caseInsensitive<"up"> + downward = caseInsensitive<"down"> + stop = caseInsensitive<"stopped">|caseInsensitive<"stop"> + CountUp = (count optionalDash)? upward + CountDown = (count optionalDash)? downward + StopCount = stop (optionalDash count)? + TimerDirection = CountUp|CountDown|StopCount + + status = caseInsensitive<"status"> + clock = caseInsensitive<"clock"> + timer = caseInsensitive<"timer"> + character = caseInsensitive<"character"> + bgm = caseInsensitive<"bgm"> + bg = caseInsensitive<"bg"> + sfx = caseInsensitive<"sfx"> + + descriptionSeparator = "::" + urlIndicator = "@" + title = (~urlIndicator any)+ + url = (~space any)+ + + StatusDefinitionOperation = identifier status colon title urlIndicator url textToEndOfLine? + + ClockFill = caseInsensitive<"fill"> + ClockEmpty = caseInsensitive<"empty"> + ClockDirection = ClockFill|ClockEmpty + ClockDescription = descriptionSeparator textToEndOfLine + ClockDefinitionOperation = identifier clock colon title urlIndicator MeteredValue ClockDirection? ClockDescription? + + hours = caseInsensitive<"hours">|caseInsensitive<"hr">|caseInsensitive<"h"> + minutes = caseInsensitive<"minutes">|caseInsensitive<"min">|caseInsensitive<"m"> + seconds = caseInsensitive<"seconds">|caseInsensitive<"sec">|caseInsensitive<"s"> + HoursCounter = integer hours + MinutesCounter = integer minutes + SecondsCounter = integer seconds + Time = integer colon integer colon integer -- HMSColonTime + | colon? integer colon integer -- MSColonTime + | colon? integer -- SColonTime + | HoursCounter? MinutesCounter? SecondsCounter -- HMSLetterTime + | HoursCounter? MinutesCounter -- HMLetterTime + | HoursCounter -- HLetterTime + TimerDescription = descriptionSeparator textToEndOfLine + TimerDefinitionOperation = identifier timer colon title urlIndicator Time TimerDirection TimerDescription? + + CharacterDefinitionOperation = identifier character colon title urlIndicator url textToEndOfLine? + + BGMChangeOperation = bgm colon title urlIndicator url textToEndOfLine? + BGMClearOperation = bgm colon null + + SFXPlayOperation = sfx colon title urlIndicator url textToEndOfLine? + + DestroyOperation = Operands colon null + printOperator = ">" PrintOperation = printOperator textToEndOfLine PrintOperationWithOperands = Operands colon printOperator textToEndOfLine @@ -186,7 +305,8 @@ FabulaDSL { | SetValueOperation | SetValueOperationAlternate | SetValueOperationAlternate2 | SetMaxOperation | SetMaxOperationAlternate | SetMaxOperationAlternate2 | ClearOperation | ClearOperationAlternate | ClearOperationAlternate2 - | StatusOrItemDeltaOperation | StatusOrItemCounterDeltaOperation | StatusOrItemCounterSetOperation + | StatusOrItemAddOperation | StatusOrItemRemoveOperation | StatusOrItemCounterSetOperation + | StatusOrItemCounterDeltaOperation | StatusOrItemCounterDeltaOperationAlternate | SetTargetOperation | SetSourceOperation | FailOperation | PrintOperationWithOperands | PrintOperation silentOperator = "~" @@ -207,7 +327,8 @@ FabulaDSL { beginKeyword = caseInsensitive<"begin"> endKeyword = caseInsensitive<"end"> - BlockStart = silentOperator? beginKeyword SetSourceOperation? SetTargetOperation? colon textToEndOfLine + BlockTitle = colon textToEndOfLine + BlockStart = silentOperator? beginKeyword SetSourceOperation? SetTargetOperation? BlockTitle? BlockEnd = endKeyword textToEndOfLine? Block = BlockStart operationTerminator CodeSegment BlockEnd operationTerminatorOrEnd diff --git a/src/grammar/interpreter.ts b/src/grammar/interpreter.ts new file mode 100644 index 0000000..d7d4fce --- /dev/null +++ b/src/grammar/interpreter.ts @@ -0,0 +1,1528 @@ +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(oper: NonterminalNode): NumberSign { + return (oper 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(oper: NonterminalNode): Operands { + return (oper 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; is just for funsies. + 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(oper: NonterminalNode): EvaluationContext { + let ctx = (this as EvaluationNode).args.ctx + return (oper 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(opers: NonterminalNode, colon: NonterminalNode, delt: NonterminalNode, affin: 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(opers: 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(opers: NonterminalNode, colon: NonterminalNode, delt: NonterminalNode, affin: 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); + }, + _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 +} \ No newline at end of file diff --git a/src/grammar/parser.ts b/src/grammar/parser.ts deleted file mode 100644 index e4f07bb..0000000 --- a/src/grammar/parser.ts +++ /dev/null @@ -1,849 +0,0 @@ -import grammar from "./grammar.ohm-bundle"; -import { - Affinity, - ElementalType, - FailReason, - MarkdownContext, - MarkdownOutput, - MeteredResource, - NumberSign, - Operands, - OperandsFrom, - ParseContext, - Resource, - Source, - Target, - UnmeteredResource, -} from "../model/Messages"; -import {IterationNode, Node, NonterminalNode, TerminalNode} from "ohm-js"; -import {CharacterPrivacy, CharacterSide} from "../model/Character"; -import {ClockMode, TimerDirection} from "../model/GameState"; - -export const parser = grammar.createSemantics() - -export interface ParserNode extends Node { - readonly element: ElementalType|null - readonly affinity: Affinity - readonly sign: NumberSign - readonly numberValue: number - readonly identifier: string - readonly resource: Resource|null - readonly operands: Operands - readonly blockTargets: Operands - readonly blockSources: Operands - readonly silenced: boolean - readonly currentValue: number - readonly maxValue: number - readonly failReason: FailReason - readonly side: CharacterSide - - // TODO: Implement all of the things listed below. - readonly textValue: string - // 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 privacy: CharacterPrivacy - 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: ParseContext): ParseContext - renderMarkdown(ctx: MarkdownContext): MarkdownOutput -} - -parser.addAttribute("element", { - fireElement(): ElementalType.Fire { - return ElementalType.Fire - }, - waterElement(): ElementalType.Water { - return ElementalType.Water - }, - lightningElement(): ElementalType.Lightning { - return ElementalType.Lightning - }, - iceElement(): ElementalType.Ice { - return ElementalType.Ice - }, - earthElement(): ElementalType.Earth { - return ElementalType.Earth - }, - windElement(): ElementalType.Wind { - return ElementalType.Wind - }, - lightElement(): ElementalType.Light { - return ElementalType.Light - }, - darkElement(): ElementalType.Dark { - return ElementalType.Dark - }, - physicalElement(): ElementalType.Physical { - return ElementalType.Physical - }, - nonElement(): ElementalType.Nonelemental { - return ElementalType.Nonelemental - }, - healingElement(): ElementalType.Healing { - return ElementalType.Healing - }, - elementalType(elementNode: NonterminalNode): ElementalType { - const element = (elementNode as ParserNode).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 ParserNode).element - }, - DeltaOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, resource: IterationNode, elementalType: IterationNode): ElementalType | null { - return (elementalType as ParserNode).element - }, - DeltaOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, elementalType: IterationNode): ElementalType | null { - return (elementalType as ParserNode).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 ParserNode).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`) - } -}) - -parser.addAttribute("affinity", { - absorbAffinity(): Affinity.Absorbs { - return Affinity.Absorbs - }, - immuneAffinity(): Affinity.Immune { - return Affinity.Immune - }, - resistAffinity(): Affinity.Resistant { - return Affinity.Resistant - }, - vulnerableAffinity(): Affinity.Vulnerable { - return Affinity.Vulnerable - }, - normalAffinity(): Affinity.Normal { - return Affinity.Normal - }, - affinity(affinityNode: NonterminalNode): Affinity { - return (affinityNode as ParserNode).affinity - }, - DeltaOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, elementalType: IterationNode): Affinity { - return (affinity as ParserNode).affinity - }, - DeltaOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, resource: IterationNode, elementalType: IterationNode): Affinity { - return (affinity as ParserNode).affinity - }, - DeltaOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, elementalType: IterationNode): Affinity { - return (affinity as ParserNode).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 ParserNode).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`) - } -}) - -parser.addAttribute("sign", { - deltaOperator(oper: NonterminalNode): NumberSign { - return (oper as ParserNode).sign - }, - plus(): NumberSign.Positive { - return NumberSign.Positive - }, - minus(): NumberSign.Negative { - return NumberSign.Negative - }, - StatusOrItemDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, deltaOperator: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): NumberSign { - return (deltaOperator as ParserNode).sign - }, - StatusOrItemCounterDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, delta: NonterminalNode): NumberSign { - return (delta as ParserNode).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`) - } -}) - -parser.addAttribute("numberValue", { - integer(digits: IterationNode): number { - return parseInt(digits.sourceString) - }, - Delta(sign: NonterminalNode, integer: NonterminalNode): number { - const number = (integer as ParserNode).numberValue - switch ((sign as ParserNode).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 ParserNode).numberValue - }, - DeltaOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, resource: IterationNode, elementalType: IterationNode): number { - return (delta as ParserNode).numberValue - }, - DeltaOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, elementalType: IterationNode): number { - return (delta as ParserNode).numberValue - }, - MaxValue(separator: NonterminalNode, integer: NonterminalNode): number { - return (integer as ParserNode).numberValue; - }, - SetValueOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, integer: NonterminalNode): number { - return (integer as ParserNode).numberValue; - }, - SetValueOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, integer: NonterminalNode, resource: NonterminalNode): number { - return (integer as ParserNode).numberValue; - }, - SetValueOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, integer: NonterminalNode): number { - return (integer as ParserNode).numberValue; - }, - SetMaxOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, maxvalue: NonterminalNode): number { - return (maxvalue as ParserNode).numberValue; - }, - SetMaxOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, maxvalue: NonterminalNode, resource: NonterminalNode): number { - return (maxvalue as ParserNode).numberValue; - }, - SetMaxOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, maxvalue: NonterminalNode): number { - return (maxvalue as ParserNode).numberValue; - }, - StatusOrItemCounterUnwrapped(x: IterationNode, integer: NonterminalNode): number { - return (integer as ParserNode).numberValue - }, - StatusOrItemCounterWrapped(lParen: TerminalNode, counter: NonterminalNode, rParen: TerminalNode): number { - return (counter as ParserNode).numberValue - }, - StatusOrItemDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, deltaOperator: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): number { - return (counter as ParserNode).numberValue - }, - StatusOrItemCounterDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, delta: NonterminalNode): number { - return (delta as ParserNode).numberValue - }, - StatusOrItemCounterSetOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): number { - return (counter as ParserNode).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`) - } -}) - -parser.addAttribute("identifier", { - identifier(): string { - return this.sourceString - }, - StatusOrItemDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, deltaOperator: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): string { - return (identifier as ParserNode).identifier - }, - StatusOrItemCounterDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, delta: NonterminalNode): string { - return (identifier as ParserNode).identifier - }, - StatusOrItemCounterSetOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): string { - return (identifier as ParserNode).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`) - } -}) - -parser.addAttribute("resource", { - BpResource(): MeteredResource.Blood { - return MeteredResource.Blood - }, - FabulaResource(): UnmeteredResource.Fabula { - return UnmeteredResource.Fabula; - }, - HpResource(): MeteredResource.Health { - return MeteredResource.Health; - }, - IpResource(): MeteredResource.Items { - return MeteredResource.Items; - }, - LevelResource(): UnmeteredResource.Level { - return UnmeteredResource.Level; - }, - MaterialsResource(): UnmeteredResource.Materials { - return UnmeteredResource.Materials; - }, - MeteredResource(res: NonterminalNode): MeteredResource { - const r = (res as ParserNode).resource; - switch (r) { - case MeteredResource.Health: - case MeteredResource.Magic: - case MeteredResource.Items: - case MeteredResource.Experience: - case MeteredResource.Zero: - case MeteredResource.Turns: - case MeteredResource.Segments: - case MeteredResource.Blood: - return r - default: - throw Error("unexpected unmetered resource in metered resource node") - } - }, - MoneyResource(): UnmeteredResource.Zenit { - return UnmeteredResource.Zenit; - }, - MpResource(): MeteredResource.Magic { - return MeteredResource.Magic; - }, - Resource(res: NonterminalNode): Resource { - const r = (res as ParserNode).resource; - if (r === null) { - throw Error("unexpected null resource in resource node") - } - return r - }, - SpResource(type: NonterminalNode): UnmeteredResource.Special|UnmeteredResource.Fabula|UnmeteredResource.Ultima { - const r = (type as ParserNode).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(): UnmeteredResource.Special { - return UnmeteredResource.Special; - }, - TpResource(): MeteredResource.Turns { - return MeteredResource.Turns; - }, - UltimaResource(): Resource { - return UnmeteredResource.Ultima; - }, - UnmeteredResource(res: NonterminalNode): UnmeteredResource { - const r = (res as ParserNode).resource; - switch (r) { - case UnmeteredResource.Fabula: - case UnmeteredResource.Ultima: - case UnmeteredResource.Zenit: - case UnmeteredResource.Materials: - case UnmeteredResource.Special: - case UnmeteredResource.Level: - return r - default: - throw Error("unexpected metered resource in unmetered resource node") - } - }, - XpResource(): MeteredResource.Experience { - return MeteredResource.Experience; - }, - ZpResource(): 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 ParserNode).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 ParserNode).resource - }, - DeltaOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, elementalType: IterationNode): Resource { - const r = (resource as ParserNode).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 ParserNode).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 ParserNode).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 ParserNode).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 ParserNode).resource; - switch (r) { - case MeteredResource.Health: - case MeteredResource.Magic: - case MeteredResource.Items: - case MeteredResource.Experience: - case MeteredResource.Zero: - case MeteredResource.Turns: - case MeteredResource.Segments: - case MeteredResource.Blood: - return r - default: - throw Error("unexpected unmetered resource in metered resource node") - } - }, - SetMaxOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, maxvalue: NonterminalNode, resource: NonterminalNode): MeteredResource { - const r = (resource as ParserNode).resource; - switch (r) { - case MeteredResource.Health: - case MeteredResource.Magic: - case MeteredResource.Items: - case MeteredResource.Experience: - case MeteredResource.Zero: - case MeteredResource.Turns: - case MeteredResource.Segments: - case MeteredResource.Blood: - return r - default: - throw Error("unexpected unmetered resource in metered resource node") - } - }, - SetMaxOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, maxvalue: NonterminalNode): MeteredResource { - const r = (resource as ParserNode).resource; - switch (r) { - case MeteredResource.Health: - case MeteredResource.Magic: - case MeteredResource.Items: - case MeteredResource.Experience: - case MeteredResource.Zero: - case MeteredResource.Turns: - case MeteredResource.Segments: - case MeteredResource.Blood: - return r - default: - throw Error("unexpected unmetered resource in metered resource node") - } - }, - - _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 ParserNode).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 ParserNode).resource; - switch (r) { - case MeteredResource.Health: - case MeteredResource.Magic: - case MeteredResource.Items: - case MeteredResource.Experience: - case MeteredResource.Zero: - case MeteredResource.Turns: - case MeteredResource.Segments: - case MeteredResource.Blood: - return r - default: - throw Error("unexpected unmetered resource in metered resource node") - } - }, - SetMeteredOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, value: NonterminalNode, resource: NonterminalNode): MeteredResource { - const r = (resource as ParserNode).resource; - switch (r) { - case MeteredResource.Health: - case MeteredResource.Magic: - case MeteredResource.Items: - case MeteredResource.Experience: - case MeteredResource.Zero: - case MeteredResource.Turns: - case MeteredResource.Segments: - case MeteredResource.Blood: - return r - default: - throw Error("unexpected unmetered resource in metered resource node") - } - }, - SetMeteredOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, value: NonterminalNode): MeteredResource { - const r = (resource as ParserNode).resource; - switch (r) { - case MeteredResource.Health: - case MeteredResource.Magic: - case MeteredResource.Items: - case MeteredResource.Experience: - case MeteredResource.Zero: - case MeteredResource.Turns: - case MeteredResource.Segments: - case MeteredResource.Blood: - return r - default: - throw Error("unexpected unmetered resource in metered resource node") - } - }, - ClearOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, nul: NonterminalNode): Resource { - const r = (resource as ParserNode).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 ParserNode).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 ParserNode).resource - if (r === null) { - throw Error("unexpected null resource in required resource node") - } - return r - }, -}) - -parser.addAttribute("operands", { - CompleteOperation(silence: IterationNode, operation: NonterminalNode, terminator: NonterminalNode): Operands { - return (operation as ParserNode).operands - }, - Operation(operation: NonterminalNode): Operands { - return (operation as ParserNode).operands - }, - ClearOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, nul: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - ClearOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, nul: NonterminalNode, resource: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - ClearOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, nul: NonterminalNode): Operands { - return (operands as ParserNode).operands; - }, - DeltaOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, element: IterationNode): Operands { - return (operands as ParserNode).operands; - }, - DeltaOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, resource: IterationNode, element: IterationNode): Operands { - return (operands as ParserNode).operands; - }, - DeltaOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, delta: NonterminalNode, affinity: IterationNode, element: IterationNode): Operands { - return (operands as ParserNode).operands; - }, - FailOperation(operands: NonterminalNode, colon: NonterminalNode, fail: NonterminalNode): Operands { - return (operands as ParserNode).operands; - }, - PrintOperation(): Set { - return Operands() - }, - SetMaxOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, max: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - SetMaxOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, max: NonterminalNode, resource: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - SetMaxOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, max: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - SetMeteredOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, meter: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - SetMeteredOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, meter: NonterminalNode, resource: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - SetMeteredOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, meter: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - SetSourceOperation(source: NonterminalNode, operands: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - SetTargetOperation(target: NonterminalNode, operands: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - SetValueOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, value: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - SetValueOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, value: NonterminalNode, resource: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - SetValueOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, value: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - StatusOrItemCounterDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, statusOrItem: NonterminalNode, delta: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - StatusOrItemCounterSetOperation(operands: NonterminalNode, colon: NonterminalNode, statusOrItem: NonterminalNode, value: NonterminalNode): Operands { - return (operands as ParserNode).operands - }, - StatusOrItemDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, statusOrItem: NonterminalNode, counter: IterationNode): Operands { - return (operands as ParserNode).operands - }, - Operands(arg0: NonterminalNode): Operands { - return (arg0.asIteration() as ParserNode).operands - }, - _iter(...children: readonly Node[]): Operands { - return OperandsFrom(children.map((child) => (child as ParserNode).operands)) - }, - operand(oper: NonterminalNode): Operands { - return (oper as ParserNode).operands - }, - identifier(): Set { - return Operands((this as ParserNode).identifier) - }, - target(): Set { - return Operands(Target) - }, - source(): Set { - return Operands(Source); - }, - ["null"](): Set { - return Operands() - }, - _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`) - }, -}) - -parser.addAttribute("blockTargets", { - Block(start: NonterminalNode, terminator: NonterminalNode, code: NonterminalNode, end: NonterminalNode, terminator2: NonterminalNode): Operands { - return (start as ParserNode).blockTargets - }, - BlockStart(silenced: IterationNode, begin: NonterminalNode, source: IterationNode, target: IterationNode, colon: NonterminalNode, text: NonterminalNode): Operands { - return (target as ParserNode).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`) - }, -}) - -parser.addAttribute("blockSources", { - Block(start: NonterminalNode, terminator: NonterminalNode, code: NonterminalNode, end: NonterminalNode, terminator2: NonterminalNode): Operands { - return (start as ParserNode).blockSources - }, - BlockStart(silenced: IterationNode, begin: NonterminalNode, source: IterationNode, target: IterationNode, colon: NonterminalNode, text: NonterminalNode): Operands { - return (source as ParserNode).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`) - }, -}) - -parser.addAttribute("silenced", { - Block(start: NonterminalNode, terminator: NonterminalNode, code: NonterminalNode, end: NonterminalNode, terminator2: NonterminalNode): boolean { - return (start as ParserNode).silenced - }, - BlockStart(silenced: IterationNode, begin: NonterminalNode, source: IterationNode, target: IterationNode, colon: NonterminalNode, text: NonterminalNode): boolean { - return (target as ParserNode).silenced - }, - CompleteOperation(silenced: IterationNode, operation: NonterminalNode, terminator: NonterminalNode): boolean { - return (silenced as ParserNode).silenced - }, - silentOperator(): 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 ParserNode).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`) - } -}) - -parser.addAttribute("currentValue", { - SetValueOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, integer: NonterminalNode): number { - return (integer as ParserNode).numberValue; - }, - SetValueOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, integer: NonterminalNode, resource: NonterminalNode): number { - return (integer as ParserNode).numberValue; - }, - SetValueOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, integer: NonterminalNode): number { - return (integer as ParserNode).numberValue; - }, - MeteredValue(current: NonterminalNode, max: NonterminalNode): number { - return (current as ParserNode).numberValue - }, - SetMeteredOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, value: NonterminalNode): number { - return (value as ParserNode).currentValue; - }, - SetMeteredOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, value: NonterminalNode, resource: NonterminalNode): number { - return (value as ParserNode).currentValue; - }, - SetMeteredOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, value: NonterminalNode): number { - return (value as ParserNode).currentValue; - }, - StatusOrItemDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, deltaOperator: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): number { - return (counter as ParserNode).numberValue - }, - StatusOrItemCounterSetOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): number { - return (counter as ParserNode).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`) - } -}) - -parser.addAttribute("maxValue", { - SetMaxOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, maxvalue: NonterminalNode): number { - return (maxvalue as ParserNode).numberValue; - }, - SetMaxOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, maxvalue: NonterminalNode, resource: NonterminalNode): number { - return (maxvalue as ParserNode).numberValue; - }, - SetMaxOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, maxvalue: NonterminalNode): number { - return (maxvalue as ParserNode).numberValue; - }, - MeteredValue(current: NonterminalNode, max: NonterminalNode): number { - return (max as ParserNode).numberValue - }, - SetMeteredOperation(operands: NonterminalNode, space: NonterminalNode, resource: NonterminalNode, colon: NonterminalNode, value: NonterminalNode): number { - return (value as ParserNode).maxValue; - }, - SetMeteredOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, value: NonterminalNode, resource: NonterminalNode): number { - return (value as ParserNode).maxValue; - }, - SetMeteredOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, value: NonterminalNode): number { - return (value as ParserNode).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`) - } -}) - -parser.addAttribute("failReason", { - avoid(): FailReason.Avoid { - return FailReason.Avoid - }, - dodge(): FailReason.Dodge { - return FailReason.Dodge - }, - miss(): FailReason.Miss { - return FailReason.Miss - }, - resist(): FailReason.Resist { - return FailReason.Resist - }, - fail(): FailReason.Fail { - return FailReason.Fail - }, - block(): FailReason.Block { - return FailReason.Block - }, - parry(): FailReason.Parry { - return FailReason.Parry - }, - FailReason(reason: NonterminalNode): FailReason { - return (reason as ParserNode).failReason - }, - FailOperation(operands: NonterminalNode, colon: NonterminalNode, reason: NonterminalNode): FailReason { - return (reason as ParserNode).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`) - } -}) - -parser.addAttribute("side", { - characterSide(side: NonterminalNode): CharacterSide { - return (side as ParserNode).side - }, - enemySide(): CharacterSide.Enemy { - return CharacterSide.Enemy - }, - allySide(): 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`) - } -}) \ No newline at end of file diff --git a/src/index.css b/src/index.css index 9389aa9..fd01bd9 100644 --- a/src/index.css +++ b/src/index.css @@ -11,6 +11,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + font-size: 15px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } diff --git a/src/model/Character.ts b/src/model/Character.ts index 06877c7..05b2bbc 100644 --- a/src/model/Character.ts +++ b/src/model/Character.ts @@ -1,4 +1,11 @@ import {isDefined} from "../types/type_check"; +import { + createResourceManipulator, + MeteredResource, + Resource, + ResourceManipulator, + UnmeteredResource +} from "./Resources"; export enum CharacterHealth { Full = "Full", @@ -77,7 +84,7 @@ export function healthToBounds(health: CharacterHealth | undefined): string { return `${minPercentage ?? ""}${minPercentage !== null && maxPercentage !== null ? "-" : ""}${maxPercentage ?? ""}%` } -export function hpToHealth(hp: number | undefined, maxHp: number | undefined): CharacterHealth | undefined { +export function hpToHealth({hp, maxHp}: {hp?: number, maxHp?: number}): CharacterHealth | undefined { if (!isDefined(hp) || !isDefined(maxHp) || maxHp <= 0) { return } @@ -149,6 +156,13 @@ export function turnStateToDescription(state: CharacterTurnState): string { return CharacterTurnStates[state].description } +export function isZpReady({zp, maxZp}: {zp?: number, maxZp?: number}): boolean|undefined { + if (typeof zp === "undefined" || typeof maxZp === "undefined") { + return + } + return zp === maxZp +} + export enum SPType { UltimaPoints = "Ultima", FabulaPoints = "Fabula", @@ -175,7 +189,7 @@ export function spTypeToDescription(sp: SPType): string { export interface StatusEffectInstance { readonly id: string - readonly count?: number + readonly count: number } export enum CharacterSide { @@ -186,9 +200,11 @@ export enum CharacterSide { export interface Character { readonly id: string readonly side?: CharacterSide + readonly minion?: boolean readonly portraitUrl?: string readonly name?: string readonly altName?: string + readonly description?: string readonly level?: number readonly xp?: number readonly maxXp?: number @@ -212,6 +228,8 @@ export interface Character { readonly canAct?: boolean readonly statuses?: readonly StatusEffectInstance[] readonly order?: number + readonly zenit?: number + readonly materials?: number readonly privacy?: CharacterPrivacy } @@ -341,6 +359,123 @@ export const CharacterPrivacySettings = { } } as const satisfies {readonly [value in CharacterPrivacy]: CharacterPrivacySetting} +const CharacterResourceValue = { + [MeteredResource.Blood]: "bp", + [MeteredResource.Experience]: "xp", + [MeteredResource.Health]: "hp", + [MeteredResource.Items]: "ip", + [MeteredResource.Magic]: "mp", + [MeteredResource.Segments]: undefined, + [MeteredResource.Turns]: "turnsLeft", + [MeteredResource.Zero]: "zp", + [UnmeteredResource.Order]: "order", + [UnmeteredResource.Level]: "level", + [UnmeteredResource.Zenit]: "zenit", + [UnmeteredResource.Fabula]: "sp", + [UnmeteredResource.Ultima]: "sp", + [UnmeteredResource.Special]: "sp", + [UnmeteredResource.Materials]: "materials", +} as const satisfies {[key in Resource]: keyof Character|undefined} + +const CharacterResourceMax = { + [MeteredResource.Blood]: "maxBp", + [MeteredResource.Experience]: "maxXp", + [MeteredResource.Health]: "maxHp", + [MeteredResource.Items]: "maxIp", + [MeteredResource.Magic]: "maxMp", + [MeteredResource.Segments]: undefined, + [MeteredResource.Turns]: "turnsTotal", + [MeteredResource.Zero]: "maxZp", + [UnmeteredResource.Materials]: undefined, + [UnmeteredResource.Fabula]: undefined, + [UnmeteredResource.Ultima]: undefined, + [UnmeteredResource.Special]: undefined, + [UnmeteredResource.Zenit]: undefined, + [UnmeteredResource.Order]: undefined, + [UnmeteredResource.Level]: undefined, +} as const satisfies {[key in MeteredResource]: keyof Character|undefined} & {[key in UnmeteredResource]: undefined} + +const baseCharacterResource = + createResourceManipulator< + Exclude, + Character, Resource>(CharacterResourceValue, CharacterResourceMax) + +export const CharacterResources: ResourceManipulator = { + ...baseCharacterResource, + getValue(object: Character, resource: Resource): number | undefined { + if ((resource === UnmeteredResource.Fabula && object.spType !== SPType.FabulaPoints) + || (resource === UnmeteredResource.Ultima && object.spType !== SPType.UltimaPoints)) { + return undefined + } + return baseCharacterResource.getValue(object, resource) + }, + applyDelta(object: Character, resource: Resource, delta: number): Character { + if ((resource === UnmeteredResource.Fabula && object.spType !== SPType.FabulaPoints) + || (resource === UnmeteredResource.Ultima && object.spType !== SPType.UltimaPoints)) { + return object + } + if ((resource === MeteredResource.Experience)) { + const oldExp = this.getValue(object, resource) + const maxExp = this.getMax(object, resource) + if (typeof maxExp === "undefined" || typeof oldExp === "undefined" || maxExp < 1) { + return object + } + let levelDiff = 0 + let newExp = oldExp + delta + while (newExp > maxExp) { + levelDiff += 1 + newExp -= maxExp + } + while (newExp < 0) { + levelDiff -= 1 + newExp += maxExp + } + return this.setValue(this.applyDelta(object, UnmeteredResource.Level, levelDiff), MeteredResource.Experience, newExp) + } + const updated = baseCharacterResource.applyDelta(object, resource, delta) + if (resource === MeteredResource.Health) { + return {...updated, health: hpToHealth(updated)} + } else { + return updated + } + }, + setValue(object: Character, resource: Resource, value: number): Character { + const updated = baseCharacterResource.setValue(object, resource, value) + switch (resource) { + case MeteredResource.Health: + return {...updated, health: hpToHealth(updated)} + case UnmeteredResource.Fabula: + return {...updated, spType: SPType.FabulaPoints} + case UnmeteredResource.Ultima: + return {...updated, spType: SPType.UltimaPoints} + default: + return updated + } + }, + setMetered(object: Character, resource: Resource, value: number, max: number): Character { + if (resource === MeteredResource.Experience && value === max) { + return baseCharacterResource.applyDelta( + baseCharacterResource.setMetered(object, resource, 0, max), + UnmeteredResource.Level, 1) + } else { + const result = baseCharacterResource.setMetered(object, resource, value, max) + if (resource === MeteredResource.Health) { + return {...result, health: hpToHealth(result)} + } else { + return result + } + } + }, + setMax(object: Character, resource: Resource, max: number): Character { + const result = baseCharacterResource.setMax(object, resource, max) + if (resource === MeteredResource.Health) { + return {...result, health: hpToHealth(result)} + } else { + return result + } + }, +} + export function applyCharacterPrivacy(character: Character): Character|null { const privacySettings = CharacterPrivacySettings[character.privacy ?? CharacterPrivacy.Hidden] if (!privacySettings.showCharacter) { @@ -399,4 +534,58 @@ export function applyCharacterPrivacy(character: Character): Character|null { delete out.maxXp } return out +} + +export const CharacterStatuses = { + addStatus(c: Character, id: string, stacks: number): Character { + if (!c.statuses || c.statuses.some((s) => s.id === id)) { + return c + } else { + return this.setStatus(c, id, stacks) + } + }, + setStatus(c: Character, id: string, stacks: number): Character { + if (!c.statuses) { + return c + } + if (c.statuses.some((s) => s.id === id)) { + return { + ...c, + statuses: c.statuses.map((s) => s.id === id ? {...s, count: Math.max(0, stacks)} : s), + } + } else { + return { + ...c, + statuses: [...c.statuses, {id: id, count: Math.max(0, stacks)}] + } + } + }, + applyStatusDelta(c: Character, id: string, delta: number): Character { + if (!c.statuses) { + return c + } + const status = c.statuses.find((s) => s.id === id) + if (!status) { + if (delta > 0) { + return this.addStatus(c, id, delta) + } else { + return c + } + } + if (status.count + delta > 0) { + return this.setStatus(c, id, status.count + delta) + } else { + return this.removeStatus(c, id) + } + }, + removeStatus(c: Character, id: string): Character { + if (!c.statuses || !c.statuses.some((s) => s.id === id)) { + return c + } else { + return { + ...c, + statuses: c.statuses.filter((s) => s.id !== id) + } + } + } } \ No newline at end of file diff --git a/src/model/Clock.ts b/src/model/Clock.ts new file mode 100644 index 0000000..de154d1 --- /dev/null +++ b/src/model/Clock.ts @@ -0,0 +1,62 @@ +import { + createResourceManipulator, + MeteredResource, + Resource, + ResourceManipulator, + UnmeteredResource +} from "./Resources"; + +export enum ClockMode { + HEROES_FILL = "fill", + HEROES_EMPTY = "empty", +} + +export interface Clock { + readonly id: string + readonly text: string + readonly segments: number + readonly filled: number + readonly mode: ClockMode + readonly order: number +} + +const ClockResourceValue = { + [MeteredResource.Blood]: undefined, + [MeteredResource.Experience]: undefined, + [MeteredResource.Health]: undefined, + [MeteredResource.Items]: undefined, + [MeteredResource.Magic]: undefined, + [MeteredResource.Segments]: "filled", + [MeteredResource.Turns]: undefined, + [MeteredResource.Zero]: undefined, + [UnmeteredResource.Materials]: undefined, + [UnmeteredResource.Fabula]: undefined, + [UnmeteredResource.Ultima]: undefined, + [UnmeteredResource.Special]: undefined, + [UnmeteredResource.Zenit]: undefined, + [UnmeteredResource.Order]: "order", + [UnmeteredResource.Level]: undefined, +} as const satisfies {[key in Resource]: keyof Clock|undefined} + +const ClockResourceMax = { + [MeteredResource.Blood]: undefined, + [MeteredResource.Experience]: undefined, + [MeteredResource.Health]: undefined, + [MeteredResource.Items]: undefined, + [MeteredResource.Magic]: undefined, + [MeteredResource.Segments]: "segments", + [MeteredResource.Turns]: undefined, + [MeteredResource.Zero]: undefined, + [UnmeteredResource.Materials]: undefined, + [UnmeteredResource.Fabula]: undefined, + [UnmeteredResource.Ultima]: undefined, + [UnmeteredResource.Special]: undefined, + [UnmeteredResource.Zenit]: undefined, + [UnmeteredResource.Order]: undefined, + [UnmeteredResource.Level]: undefined, +} as const satisfies {[key in MeteredResource]: keyof Clock|undefined} & {[key in UnmeteredResource]: undefined} + +export const ClockResources: ResourceManipulator = + createResourceManipulator< + Exclude, + Clock, Resource>(ClockResourceValue, ClockResourceMax) \ No newline at end of file diff --git a/src/model/GameState.ts b/src/model/GameState.ts index e5d42dc..a5ce881 100644 --- a/src/model/GameState.ts +++ b/src/model/GameState.ts @@ -1,17 +1,5 @@ import {Character, CharacterSide, SPType} from "./Character"; - -export enum ClockMode { - HEROES_FILL = "fill", - HEROES_EMPTY = "empty", -} - -export interface Clock { - readonly id: string - readonly text: string - readonly segments: number - readonly filled: number - readonly mode: ClockMode -} +import {Clock} from "./Clock"; export interface SessionState { readonly usedSp: {readonly [key in SPType]?: number} @@ -33,6 +21,7 @@ export interface BaseTimerState { readonly type: TimerDirection readonly id: string readonly text: string + readonly order?: number } export interface StoppedTimerState extends BaseTimerState { @@ -68,18 +57,46 @@ export interface GameState { readonly timers: readonly TimerState[] } -export function getClockById(state: GameState, id: string): Clock|undefined { - return state.clocks.find((clock) => clock.id === id) +export interface BaseIdentifierLookupResult { + readonly type: string } - -export function getCharacterById(state: GameState, id: string): Character|undefined { - return state.characters.find((character) => character.id === id) +export interface NullLookupResult extends BaseIdentifierLookupResult { + readonly type: "null" } - -export function getStatusById(state: GameState, id: string): StatusEffect|undefined { - return state.statuses.find((status) => status.id === id) +export interface ClockLookupResult extends BaseIdentifierLookupResult { + readonly type: "clock" + readonly clock: Clock } - -export function getTimerById(state: GameState, id: string): TimerState|undefined { - return state.timers.find((timer) => timer.id === id) +export interface CharacterLookupResult extends BaseIdentifierLookupResult { + readonly type: "character" + readonly character: Character +} +export interface StatusLookupResult extends BaseIdentifierLookupResult { + readonly type: "status" + readonly status: StatusEffect +} +export interface TimerLookupResult extends BaseIdentifierLookupResult { + readonly type: "timer" + readonly timer: TimerState +} +export type IdentifierLookupResult = NullLookupResult|ClockLookupResult|CharacterLookupResult|StatusLookupResult|TimerLookupResult + +export function lookupIdentifier(state: GameState, identifier: string): IdentifierLookupResult { + const character = state.characters.find((c) => c.id === identifier) + if (character) { + return {type: "character", character} + } + const clock = state.clocks.find((c) => c.id === identifier) + if (clock) { + return {type: "clock", clock} + } + const status = state.statuses.find((s) => s.id === identifier) + if (status) { + return {type: "status", status} + } + const timer = state.timers.find((t) => t.id === identifier) + if (timer) { + return {type: "timer", timer} + } + return {type: "null"} } \ No newline at end of file diff --git a/src/model/Messages.ts b/src/model/Messages.ts index fdfead2..fb45f0f 100644 --- a/src/model/Messages.ts +++ b/src/model/Messages.ts @@ -1,4 +1,4 @@ -import {GameState} from "./GameState"; +import {GameState, IdentifierLookupResult, lookupIdentifier} from "./GameState"; export enum ElementalType { Fire = "fire", @@ -22,32 +22,19 @@ export enum Affinity { Vulnerable = "vulnerable", } +export const AffinityDisplay = { + [Affinity.Absorbs]: "??", + [Affinity.Immune]: ".", + [Affinity.Resistant]: "...", + [Affinity.Normal]: "", + [Affinity.Vulnerable]: "!!", +} as const satisfies {[affinity in Affinity]: string} + export enum NumberSign { Positive = "+", Negative = "-", } -export enum MeteredResource { - Experience = "EXP", - Health = "HP", - Magic = "MP", - Items = "IP", - Zero = "ZP", - Blood = "BP", - Turns = "TP", - Segments = "Segments", -} -export enum UnmeteredResource { - Fabula = "FP", - Ultima = "UP", - Special = "SP", - Level = "Level", - Materials = "Materials", - Zenit = "Zenit", -} - -export type Resource = MeteredResource|UnmeteredResource - export enum FailReason { Avoid = "avoid", Dodge = "dodge", @@ -62,22 +49,49 @@ export const Target: unique symbol = Symbol("target"); export const Source: unique symbol = Symbol("source"); export type Operands = Set; -export function Operands(...items: readonly T[]): Set { +export function OperandItems(...items: readonly T[]): Set { return new Set(items) } -export function OperandsFrom(children: readonly Set[]): Set { +export function OperandSets(children: readonly Set[]): Set { return new Set([...children.flatMap((child) => [...child.values()])]) } -export interface ParseContext { +export interface EvaluationContext { readonly timestamp: number readonly source: readonly string[] readonly target: readonly string[] readonly game: GameState } -export interface MarkdownContext extends ParseContext {} +export function evaluateOperands(operands: Operands, ctx: EvaluationContext): Set { + const result = new Set() + for (const operand of operands) { + if (operand === Source) { + for (const sourceItem of ctx.source) { + result.add(sourceItem) + } + } else if (operand === Target) { + for (const targetItem of ctx.target) { + result.add(targetItem) + } + } else { + result.add(operand) + } + } + return result +} + +export function lookupOperands(operands: Operands, ctx: EvaluationContext): Map { + const set = evaluateOperands(operands, ctx) + const result = new Map() + for (const operand of set) { + result.set(operand, lookupIdentifier(ctx.game, operand)) + } + return result +} + +export interface MarkdownContext extends EvaluationContext {} export interface MarkdownOutput extends MarkdownContext { - readonly output: string + readonly output: string|null } \ No newline at end of file diff --git a/src/model/Resources.ts b/src/model/Resources.ts new file mode 100644 index 0000000..b6ec811 --- /dev/null +++ b/src/model/Resources.ts @@ -0,0 +1,230 @@ +import {Affinity, AffinityDisplay} from "./Messages"; + +export enum MeteredResource { + Experience = "EXP", + Health = "HP", + Magic = "MP", + Items = "IP", + Zero = "Zero Charge", + Blood = "BP", + Turns = "Turns", + Segments = "Segments", +} + +export function isMeteredResource(r: Resource): r is MeteredResource { + return Object.values(MeteredResource).includes(r) +} + +export enum UnmeteredResource { + Fabula = "FP", + Ultima = "UP", + Special = "SP", + Level = "Levels", + Materials = "Zenit of Materials", + Zenit = "Zenit", + Order = "Order", +} + +export function formatResourceDelta(r: Resource, a: Affinity, n: number): string { + const delta = `${n > 0 ? "+" : n === 0 ? "±" : ""}${n.toFixed(0)}${AffinityDisplay[a]}` + if (Math.abs(n) === 1) { + switch (r) { + case MeteredResource.Turns: + return `${delta} Turn` + case MeteredResource.Segments: + return `${delta} Segment` + case UnmeteredResource.Level: + return `${delta} Level` + } + } + return `${delta} ${r}` +} + +export function formatMeteredResource(r: MeteredResource, n: number, mx: number): string { + const value = `${n}/${mx}` + if (n === 1 && mx === 1) { + switch (r) { + case MeteredResource.Turns: + return `${value} Turn` + case MeteredResource.Segments: + return `${value} Segment` + } + } + return `${value} ${r}` +} + +export function formatResourceValue(r: Resource, n: number): string { + const value = `${n}` + if (r === UnmeteredResource.Level) { + return `Lv. ${value}` + } + if (n === 1) { + switch (r) { + case MeteredResource.Turns: + return `${value} Turn` + case MeteredResource.Segments: + return `${value} Segment` + } + } + return `${value} ${r}` +} + +export function formatResourceMax(r: MeteredResource, n: number): string { + const value = `${n}` + if (n === 1) { + switch (r) { + case MeteredResource.Turns: + return `Max ${value} Turn` + case MeteredResource.Segments: + return `Max ${value} Segment` + } + } + return `Max ${value} ${r}` +} + +export function isUnmeteredResource(r: Resource): r is UnmeteredResource { + return Object.values(UnmeteredResource).includes(r) +} + +export type Resource = MeteredResource|UnmeteredResource + +export function getResourceValue< + KeySetT extends string|number|symbol, ObjectT extends {[key in KeySetT]?: number}, + ResourceT extends string|number|symbol> +(valueMap: {[key in ResourceT]: KeySetT|undefined}, object: ObjectT, resource: ResourceT): number|undefined { + const field = valueMap[resource] + if (typeof field === "undefined") { + return undefined + } + return object[field] +} + +export function setResourceValue< + KeySetT extends string|number|symbol, ObjectT extends {[key in KeySetT]?: number}, + ResourceT extends string|number|symbol> +(valueMap: {[key in ResourceT]: KeySetT|undefined}, maxMap: {[key in ResourceT]: KeySetT|undefined}, object: ObjectT, resource: ResourceT, value: number): ObjectT { + const valueField = valueMap[resource] + const maxField = maxMap[resource] + + if (typeof valueField === "undefined") { + return object + } + + if (typeof maxField !== "undefined") { + const max = getResourceValue(maxMap, object, resource) + if (typeof max === "undefined") { + return object + } + return setMeteredResource(valueMap, maxMap, object, resource, value, max) + } + + const effectiveValue = Math.max(0, value) + return { + ...object, + [valueField as KeySetT]: effectiveValue, + } +} +export function applyResourceDelta< + KeySetT extends string|number|symbol, ObjectT extends {[key in KeySetT]?: number}, + ResourceT extends string|number|symbol> +(valueMap: {[key in ResourceT]: KeySetT|undefined}, maxMap: {[key in ResourceT]: KeySetT|undefined}, object: ObjectT, resource: ResourceT, delta: number): ObjectT { + const value = getResourceValue(valueMap, object, resource) + if (typeof value === "undefined") { + return object + } + + return setResourceValue(valueMap, maxMap, object, resource, value + delta) +} + +export function setResourceMax< + KeySetT extends string|number|symbol, ObjectT extends {[key in KeySetT]?: number}, + ResourceT extends string|number|symbol> +(valueMap: {[key in ResourceT]: KeySetT|undefined}, maxMap: {[key in ResourceT]: KeySetT|undefined}, object: ObjectT, resource: ResourceT, max: number): ObjectT { + const valueField = valueMap[resource] + const maxField = maxMap[resource] + + if (typeof valueField === "undefined" || typeof maxField === "undefined") { + return object + } + const value = getResourceValue(valueMap, object, resource) + if (typeof value === "undefined") { + return object + } + return setMeteredResource(valueMap, maxMap, object, resource, value, max) +} + +export function setMeteredResource< + KeySetT extends string|number|symbol, ObjectT extends {[key in KeySetT]?: number}, + ResourceT extends string|number|symbol> +(valueMap: {[key in ResourceT]: KeySetT|undefined}, maxMap: {[key in ResourceT]: KeySetT|undefined}, object: ObjectT, resource: ResourceT, value: number, max: number): ObjectT { + const valueField = valueMap[resource] + const maxField = maxMap[resource] + + if (typeof valueField === "undefined" || typeof maxField === "undefined") { + return object + } + const effectiveMax = Math.max(0, max) + const effectiveValue = Math.max(0, Math.min(value, effectiveMax)) + return { + ...object, + [valueField as KeySetT]: effectiveValue, + [maxField as KeySetT]: effectiveMax, + } +} + +export function clearResource< + KeySetT extends string|number|symbol, ObjectT extends {[key in KeySetT]?: number}, + ResourceT extends string|number|symbol> +(valueMap: {[key in ResourceT]: KeySetT|undefined}, maxMap: {[key in ResourceT]: KeySetT|undefined}, object: ObjectT, resource: ResourceT): ObjectT { + const valueField = valueMap[resource] + const maxField = maxMap[resource] + + const isValidValue = typeof valueField !== "undefined" && object.hasOwnProperty(valueField) + const isValidMax = typeof maxField !== "undefined" && object.hasOwnProperty(maxField) + + const result: { -readonly [key in keyof ObjectT]: ObjectT[key] } = {...object} + if (isValidValue) { + delete result[valueField as KeySetT] + } + if (isValidMax) { + delete result[maxField as KeySetT] + } + return result as ObjectT +} + +export interface ResourceManipulator { + getValue(object: ObjectT, resource: ResourceT): number|undefined + getMax(object: ObjectT, resource: ResourceT): number|undefined + setValue(object: ObjectT, resource: ResourceT, value: number): ObjectT + setMax(object: ObjectT, resource: ResourceT, max: number): ObjectT + setMetered(object: ObjectT, resource: ResourceT, value: number, max: number): ObjectT + applyDelta(object: ObjectT, resource: ResourceT, delta: number): ObjectT + clear(object: ObjectT, resource: ResourceT): ObjectT +} + +export function createResourceManipulator(valueMap: {[key in ResourceT]: KeySetT|undefined}, maxMap: {[key in ResourceT]: KeySetT|undefined}): ResourceManipulator { + return { + getValue(object: ObjectT, resource: ResourceT): number | undefined { + return getResourceValue(valueMap, object, resource) + }, + applyDelta(object: ObjectT, resource: ResourceT, delta: number): ObjectT { + return applyResourceDelta(valueMap, maxMap, object, resource, delta) + }, + getMax(object: ObjectT, resource: ResourceT): number | undefined { + return getResourceValue(maxMap, object, resource) + }, + setMax(object: ObjectT, resource: ResourceT, max: number): ObjectT { + return setResourceMax(valueMap, maxMap, object, resource, max) + }, + setMetered(object: ObjectT, resource: ResourceT, value: number, max: number): ObjectT { + return setMeteredResource(valueMap, maxMap, object, resource, value, max); + }, + setValue(object: ObjectT, resource: ResourceT, value: number): ObjectT { + return setResourceValue(valueMap, maxMap, object, resource, value); + }, + clear(object: ObjectT, resource: ResourceT): ObjectT { + return clearResource(valueMap, maxMap, object, resource) + } + } +} diff --git a/src/ui/App.css b/src/ui/App.css index 68f6bd9..27edb8f 100644 --- a/src/ui/App.css +++ b/src/ui/App.css @@ -2,11 +2,12 @@ color: white; position: sticky; text-align: center; - padding: 2px 0 5px; - text-shadow: 0 0 5px black; + font-size: 2em; + padding: 0.05em 0 0.125em; + text-shadow: 0 0 0.125em black; align-self: stretch; user-select: none; - border-bottom: 1px solid; + border-bottom: 0.05em solid; top: 0; z-index: 90; } @@ -17,15 +18,15 @@ .totalFPSpent, .totalUPSpent { color: white; - line-height: 60px; - -webkit-text-stroke: 2px black; - text-shadow: 2px 2px 2px black; - font-size: 45px; - letter-spacing: -3px; + line-height: 1.333; + -webkit-text-stroke: 0.0444em black; + text-shadow: 0.0444em 0.0444em 0.0444em black; + font-size: 2.25em; + letter-spacing: -0.06667em; text-align: center; font-weight: bold; - height: 60px; - width: 60px; + height: 1.333em; + width: 1.333em; background-repeat: no-repeat; background-position: center; user-select: none; @@ -42,13 +43,13 @@ .totalSPSpent { color: white; - line-height: 60px; - text-shadow: 1px 1px 3px black; - font-size: 30px; + line-height: 2; + text-shadow: 0.0333em 0.0333em 0.1em black; + font-size: 1.5em; text-align: center; font-weight: bold; - height: 60px; - width: 200px; + height: 2em; + width: 6.6667em; white-space: nowrap; user-select: none; flex: 0 0 auto; @@ -66,7 +67,7 @@ .enemy-head.inactive { background: linear-gradient(to right, transparent 0%, maroon 50%, transparent 100%); - border-bottom: 1px solid maroon; + border-bottom-color: maroon; } .ally-head { @@ -76,7 +77,7 @@ .enemy-head { background: linear-gradient(to right, transparent 0%, maroon 10%, red 50%, maroon 90%, transparent 100%); - border-bottom: 1px solid red; + border-bottom-color: red; } .ally-head.active::before, .ally-head.active::after { diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 9eab966..9001931 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import './App.css'; import {Col, Container, Row, Stack} from "react-bootstrap"; import {CharacterStatus} from "./CharacterStatus"; @@ -7,6 +7,7 @@ import OverlayTrigger from "react-bootstrap/OverlayTrigger"; import Tooltip from "react-bootstrap/Tooltip"; import {TurnTimer} from "./TurnTimer"; import {CharacterSide} from "../model/Character"; +import {evaluate} from "../grammar/interpreter"; function useJson(url: string): T | null { const [data, setData] = useState(null); @@ -27,7 +28,34 @@ function useJson(url: string): T | null { } function App() { - const state = useJson("/api/current.json") + const origState = useJson("/api/current.json") + const state = useMemo(() => { + try { + if (origState !== null) { + return evaluate(origState, Date.now(), + String.raw`Begin &echo @prandia: Lunch + &: 69/420 MP + &: -20 MP + @ HP: 35/70 + @ HP: -30... water + @: ZP 5/6 + @: ZP +1 + &: 20 HP + gale, calor,gravitas HP: 3 + &: ZP 5 + &: ZP /7 + &: /99 IP + & HP: /69 + aelica IP: - + athetyz: - MP + linnet: HP - + &: Lv - + End`) + } + } catch (ex) { + console.log(ex) + } + }, [origState]) const startTime = Date.now() const endTime = Date.now() + 2 * 60 * 1000 return diff --git a/src/ui/CharacterStatus.css b/src/ui/CharacterStatus.css index 0d02eee..4440902 100644 --- a/src/ui/CharacterStatus.css +++ b/src/ui/CharacterStatus.css @@ -1,28 +1,33 @@ .characterStatus { - height: 150px; - width: 545px; + height: 7.5em; + width: 27.25em; position: relative; box-sizing: content-box; } +.characterStatus.minion { + margin-left: 6.8125em; + font-size: 0.80em; +} + .characterHeader { position: absolute; - left: 205px; - bottom: 55px; + left: 10.25em; + bottom: 2.75em; z-index: 4; } .characterLevel { display: inline; color: white; - -webkit-text-stroke: 1px rgba(0, 0, 0, 0.2); - text-shadow: 0 0 2px black; - margin-right: 0.25em; + -webkit-text-stroke: 0.05em rgba(0, 0, 0, 0.2); + text-shadow: 0 0 0.1em black; + margin-right: 0.2em; user-select: none; } .characterLevelLabel { - font-size: smaller; + font-size: 0.65em; font-variant: small-caps; user-select: none; } @@ -32,21 +37,21 @@ color: white; font-family: sans-serif; font-weight: bold; - font-size: 30px; + font-size: 1.5em; text-align: left; - -webkit-text-stroke: 1px rgba(0, 0, 0, 0.5); - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); + -webkit-text-stroke: 0.0333em rgba(0, 0, 0, 0.5); + text-shadow: 0.0333em 0.0333em 0.0667em rgba(0, 0, 0, 0.5); user-select: none; } .characterHpBar, .characterMpBar, .characterIpBar { - box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5); - border: 2px solid black; - transform: skewX(-30deg) translateX(15px); - border-radius: 5px; + box-shadow: 0.1em 0.1em 0.1em rgba(0, 0, 0, 0.5); + border: 0.1em solid black; + transform: skewX(-30deg) translateX(0.75em); + border-radius: 0.25em; position: absolute; - width: calc(100% - 30px); - margin: 2px 2px 8px 5px; + width: calc(100% - 1.5em); + margin: 0.1em 0.1em 0.4em 0.25em; bottom: 0; box-sizing: content-box; } @@ -57,8 +62,6 @@ font-weight: bold; font-style: italic; text-align: right; - -webkit-text-stroke: 1px black; - text-shadow: 2px 2px rgba(0, 0, 0, 0.5); position: absolute; bottom: 0; pointer-events: none; @@ -67,69 +70,70 @@ .characterHp { position: absolute; - height: 60px; - left: 157px; + height: 3em; + left: 7.85em; right: 0; - bottom: 28px; + bottom: 1.4em; overflow: visible; z-index: 1; box-sizing: content-box; } .characterHpBar { - height: 25px; -} - -.characterHpValue, .characterHealthText { - right: 5px; - transition: color 0.3s ease-in; + height: 1.25em; } .characterHpValue { - font-size: 60px; + font-size: 3em; + right: 0.0833em; + -webkit-text-stroke: 0.01667em black; + text-shadow: 0.0333em 0.0333em rgba(0, 0, 0, 0.5); + transition: color 0.3s ease-in; } .characterHealthText { - font-size: 30px; - right: 5px; - bottom: 15px; + font-size: 1.5em; + right: 0.1667em; + bottom: 0.5em; transition: color 0.3s ease-in; } .characterMp, .characterIp { position: absolute; bottom: 0; - height: 40px; + height: 2em; box-sizing: content-box; } .characterMp { - left: 140px; - right: 155px; + left: 7em; + right: 7.75em; z-index: 3; } .characterIp { - width: 150px; - right: 20px; + width: 7.5em; + right: 1em; z-index: 2; } .characterMpBar, .characterIpBar { - height: 20px; + height: 1em; } .characterMpValue, .characterIpValue { - font-size: 40px; - right: 10px; + font-size: 2em; + -webkit-text-stroke: 0.025em black; + text-shadow: 0.05em 0.05em rgba(0, 0, 0, 0.5); + right: 0.25em; } .characterPortrait { position: absolute; - top: 10px; - bottom: 15px; - left: 90px; - width: 125px; + top: 0.5em; + bottom: 0.75em; + left: 4.5em; + width: 6.25em; background-repeat: no-repeat; background-position: center; background-size: cover; @@ -139,10 +143,10 @@ .characterZeroGauge { position: absolute; - top: 5px; - bottom: 10px; - left: 50px; - width: 65px; + top: 0.25em; + bottom: 0.5em; + left: 2.5em; + width: 3.75em; } .characterZeroBar, .characterZeroBarBack, .characterZeroBarPulse{ position: absolute; @@ -152,7 +156,7 @@ right: 0; background-repeat: no-repeat; background-position: left bottom; - background-size: auto 135px; + background-size: auto 6.75em; z-index: 0; } @@ -165,10 +169,11 @@ @keyframes zeroBarPulse { from { - opacity: 0; + opacity: 75%; + transform: scaleX(100%) scaleY(100%) translateX(1%); } 50% { - opacity: 80%; + opacity: 60%; transform: scaleX(120%) scaleY(110%) translateX(-3%); } to { @@ -186,43 +191,43 @@ } .characterZeroBarPulse.active { - animation: 1s ease-out infinite zeroBarPulse; + animation: 0.75s ease-out infinite zeroBarPulse; pointer-events: none; } .characterKOBar { position: absolute; - top: 43px; - bottom: 68px; - left: 90px; - width: 125px; + top: 1.592em; + bottom: 2.518em; + left: 3.333em; + width: 4.629em; background-color: black; z-index: 0; transform: rotate(-20deg); text-align: center; - font-size: 27px; - line-height: 42px; + font-size: 1.35em; + line-height: 1.5; font-weight: bold; color: white; user-select: none; - border-radius: 15px 0; + border-radius: 0.555em 0; } .characterTurns { position: absolute; - top: 5px; - left: 5px; - width: 40px; - height: 40px; - font-size: 40px; + top: 0.125em; + left: 0.125em; + width: 1em; + height: 1em; + font-size: 2em; font-weight: bold; - -webkit-text-stroke: 2px black; - text-shadow: 2px 2px 2px black; - line-height: 40px; + -webkit-text-stroke: 0.05em black; + text-shadow: 0.05em 0.05em 0.05em black; + line-height: 1; text-align: center; box-sizing: border-box; - border: 2px solid black; - border-radius: 9px 0; + border: 0.05em solid black; + border-radius: 0.225em 0; color: white; transition: background-color 0.3s; user-select: none; @@ -256,52 +261,39 @@ background-color: paleturquoise; } -.characterSp { +.characterSp, .characterBp { position: absolute; color: white; - line-height: 40px; - -webkit-text-stroke: 2px black; - text-shadow: 2px 2px 2px black; - font-size: 30px; - letter-spacing: -3px; + line-height: 1.333; + -webkit-text-stroke: 0.06667em black; + text-shadow: 0.06667em 0.06667em 0.06667em black; + font-size: 1.5em; + letter-spacing: -0.1em; text-align: center; font-weight: bold; - top: 50px; - left: 5px; - width: 40px; - height: 40px; user-select: none; + left: 0.16667em; + width: 1.3333em; + height: 1.3333em; + opacity: 50%; + transition: opacity 0.3s ease-out; } -.characterSpFabula { - background: url("fabula-points.svg"); +.characterBp { + top: 3.16667em; + background: url("blood-points.svg") } -.characterSpUltima { - background: url("ultima-points.svg"); +.characterSp { + top: 1.6667em; } -.characterBp { - position: absolute; - color: white; - line-height: 40px; - -webkit-text-stroke: 2px black; - text-shadow: 2px 2px 2px black; - font-size: 30px; - letter-spacing: -3px; - text-align: center; - font-weight: bold; - top: 95px; - left: 5px; - width: 40px; - height: 40px; - user-select: none; - background: url("blood-points.svg") +.characterSpFabula { + background: url("fabula-points.svg"); } -.characterSp, .characterBp { - opacity: 50%; - transition: opacity 0.3s ease-out; +.characterSpUltima { + background: url("ultima-points.svg"); } .characterSp:hover, .characterBp:hover { @@ -310,9 +302,9 @@ .characterStatuses { position: absolute; - top: 5px; - right: 5px; - left: 225px; + top: 0.25em; + right: 0.25em; + left: 11.25em; display: flex; flex-direction: row; flex-wrap: nowrap; @@ -321,22 +313,22 @@ .characterStatusIcon { position: relative; - flex: 0 0 36px; - width: 36px; - height: 48px; + flex: 0 0 1.8em; + width: 1.8em; + height: 2.4em; background-size: contain; background-repeat: no-repeat; - margin-left: 5px; + margin-left: 0.25em; overflow: visible; } .characterStatusIconCountBadge { display: block; color: white; - text-shadow: 2px 2px 0 black; - -webkit-text-stroke: 1px black; - font-size: 25px; - letter-spacing: -3px; + text-shadow: 0.08em 0.08em 0 black; + -webkit-text-stroke: 0.04em black; + font-size: 1.25em; + letter-spacing: -0.15em; text-align: center; font-weight: bold; position: absolute; @@ -351,9 +343,9 @@ } .characterStatusCount, .characterHelpValue { - margin-left: 5px; + margin-left: 0.3125em; font-style: italic; - font-size: smaller; + font-size: 0.8rem; } .characterStatusHeader, .characterHelpHeader { @@ -362,7 +354,7 @@ .characterStatusDescription, .characterHelpDescription { text-align: left; - font-size: smaller; + font-size: 0.8em; } .characterStatusCount::before, .characterHelpValue::before { diff --git a/src/ui/CharacterStatus.tsx b/src/ui/CharacterStatus.tsx index 7fcd91d..648ede0 100644 --- a/src/ui/CharacterStatus.tsx +++ b/src/ui/CharacterStatus.tsx @@ -76,7 +76,7 @@ const ipBarStyle: SpringyValueInterpolatables = { } export function CharacterStatus({character, statuses, active}: {character: Character, statuses: readonly StatusEffect[], active: boolean}): ReactElement { - const {name, altName, level, health, koText} = character + const {name, altName, minion, level, health, koText} = character const {hp, maxHp} = character const effectiveMaxHp = maxHp ?? 100 @@ -87,19 +87,15 @@ export function CharacterStatus({character, statuses, active}: {character: Chara starting: 0, flash: effectiveHp * 2 <= effectiveMaxHp && effectiveHp > 0, }) - const logged = function(v:T): T { - console.log(v) - return v - } const {hpText, hpTextStyleInterpolated, hpBarStyleInterpolated} = useMemo(() => { if ((isDefined(hp) && isDefined(maxHp)) || isDefined(health)) { return { hpText: isDefined(hp) - ? to([hpRecentSpring], recentValue => `${Math.round(logged(recentValue))}`) + ? to([hpRecentSpring], recentValue => `${Math.round(recentValue)}`) : "", hpBarStyleInterpolated: evaluateResourceBarStyles(hpBarStyle, hpInterpolate), hpTextStyleInterpolated: { - color: to([hpRecentSpring], recentValue => healthToColor(hpToHealth(recentValue, maxHp))) + color: to([hpRecentSpring], recentValue => healthToColor(hpToHealth({hp: recentValue, maxHp}))) } } } else { @@ -279,7 +275,7 @@ export function CharacterStatus({character, statuses, active}: {character: Chara const characterStatuses = character.statuses ?? [] const effectiveStatuses = characterStatuses.map((statusInstance) => ({ - ...statuses.find(s => s.id === statusInstance.id), count: statusInstance.count})) + ...statuses.find(s => s.id === statusInstance.id) ?? {id: statusInstance.id, name: statusInstance.id, description: "Unrecognized status effect.", iconUrl: undefined}, count: statusInstance.count})) const hpTooltip =
@@ -312,7 +308,7 @@ export function CharacterStatus({character, statuses, active}: {character: Chara - return
+ return
{isDefined(maxZp) && isDefined(zp) && @@ -412,18 +408,18 @@ export function CharacterStatus({character, statuses, active}: {character: Chara } {isDefined(effectiveStatuses) && effectiveStatuses.length > 0 &&
- {effectiveStatuses.map(({id, count, description, iconUrl}) => + {effectiveStatuses.map(({id, name, count, description, iconUrl}) =>
{name} - {isDefined(count) && {count}}
+ {isDefined(count) && count > 0 && {count}}
{isDefined(description) &&
- {isDefined(count) ? description.replaceAll("%c%", count.toFixed(0)) : description} + {count > 0 ? description.replaceAll("%c%", count.toFixed(0)) : description}
} } placement={"bottom"}> -
{count}
+
{count > 0 && {count}}
)}
} diff --git a/tsconfig.json b/tsconfig.json index d48d3f4..3e8041d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,11 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, + "alwaysStrict": true, + "strictNullChecks": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true,