interpreter handles rendering up to halfway

main
Mari 1 year ago
parent 8f13574ee7
commit e721129d16
  1. 164
      public/api/current.json
  2. 155
      src/grammar/grammar.ohm
  3. 1528
      src/grammar/interpreter.ts
  4. 849
      src/grammar/parser.ts
  5. 1
      src/index.css
  6. 193
      src/model/Character.ts
  7. 62
      src/model/Clock.ts
  8. 65
      src/model/GameState.ts
  9. 68
      src/model/Messages.ts
  10. 230
      src/model/Resources.ts
  11. 35
      src/ui/App.css
  12. 32
      src/ui/App.tsx
  13. 224
      src/ui/CharacterStatus.css
  14. 22
      src/ui/CharacterStatus.tsx
  15. 3
      tsconfig.json

@ -29,8 +29,17 @@
"spType": "Fabula", "spType": "Fabula",
"portraitUrl": "/portraits/aelica.png", "portraitUrl": "/portraits/aelica.png",
"turnsTotal": 1, "turnsTotal": 1,
"turnsLeft": 1 "turnsLeft": 1,
"statuses": []
}, },
{
"id": "flow",
"side": "ally",
"minion": true,
"name": "Flow",
"portraitUrl": "/portraits/flow.png",
"statuses": []
},
{ {
"id": "athetyz", "id": "athetyz",
"side": "ally", "side": "ally",
@ -49,14 +58,22 @@
"spType": "Fabula", "spType": "Fabula",
"portraitUrl": "/portraits/athetyz.png", "portraitUrl": "/portraits/athetyz.png",
"turnsTotal": 1, "turnsTotal": 1,
"turnsLeft": 0 "turnsLeft": 0,
"statuses": []
},
{
"id": "galvelle",
"side": "ally",
"minion": true,
"name": "Galvelle",
"statuses": []
}, },
{ {
"id": "echo", "id": "echo",
"side": "ally", "side": "ally",
"name": "Echo", "name": "Echo",
"level": 32, "level": 32,
"hp": 67, "hp": 1,
"maxHp": 67, "maxHp": 67,
"mp": 62, "mp": 62,
"maxMp": 62, "maxMp": 62,
@ -69,7 +86,23 @@
"spType": "Fabula", "spType": "Fabula",
"portraitUrl": "/portraits/echo.png", "portraitUrl": "/portraits/echo.png",
"turnsTotal": 1, "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", "id": "gravitas",
@ -89,7 +122,16 @@
"spType": "Fabula", "spType": "Fabula",
"portraitUrl": "/portraits/gravitas.png", "portraitUrl": "/portraits/gravitas.png",
"turnsTotal": 1, "turnsTotal": 1,
"turnsLeft": 1 "turnsLeft": 1,
"statuses": []
},
{
"id": "calor",
"side": "ally",
"minion": true,
"name": "Calor",
"portraitUrl": "/portraits/calor.png",
"statuses": []
}, },
{ {
"id": "linnet", "id": "linnet",
@ -109,14 +151,23 @@
"spType": "Fabula", "spType": "Fabula",
"portraitUrl": "/portraits/linnet.png", "portraitUrl": "/portraits/linnet.png",
"turnsTotal": 1, "turnsTotal": 1,
"turnsLeft": 1 "turnsLeft": 1,
"statuses": []
},
{
"id": "terra",
"side": "ally",
"minion": true,
"name": "Terra",
"portraitUrl": "/portraits/terra.png",
"statuses": []
}, },
{ {
"id": "prandia", "id": "prandia",
"side": "ally", "side": "ally",
"name": "Prandia", "name": "Prandia",
"level": 32, "level": 32,
"hp": 90, "hp": 37,
"maxHp": 90, "maxHp": 90,
"koText": "rip lmao", "koText": "rip lmao",
"mp": 0, "mp": 0,
@ -131,67 +182,15 @@
"portraitUrl": "/portraits/prandia.png", "portraitUrl": "/portraits/prandia.png",
"turnsTotal": 1, "turnsTotal": 1,
"turnsLeft": 1, "turnsLeft": 1,
"statuses": [ "statuses": []
{ },
"id": "digested", {
"name": "Digested", "id": "selia",
"count": 3, "side": "ally",
"description": "Is currently being melted down for sustenance." "minion": true,
}, "name": "Sélia",
{ "portraitUrl": "/portraits/selia.png",
"id": "digested2", "statuses": []
"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."
}]
}, },
{ {
"id": "werespider", "id": "werespider",
@ -202,7 +201,34 @@
"sp": 5, "sp": 5,
"turnsLeft": 3, "turnsLeft": 3,
"turnsTotal": 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."
}]
} }

@ -1,69 +1,85 @@
FabulaDSL { FabulaDSL {
// CodeSegment: evaluate, renderMarkdown
CodeSegment = (Block | CompleteOperation | EmptyLines)* CodeSegment = (Block | CompleteOperation | EmptyLines)*
// identifier: identifier, operands, textValue
identifier (an identifier) = identifierStart identifierContinue+ identifier (an identifier) = identifierStart identifierContinue+
identifierStart = "_" | "$" | letter identifierStart = "_" | "$" | letter
identifierContinue = identifierStart | digit identifierContinue = identifierStart | digit
newline = "\n" newline = "\n"
// EmptyLines: evaluate (empty), renderMarkdown (empty)
EmptyLines = operationTerminator+ EmptyLines = operationTerminator+
wordSep = ~newline space wordSep = ~newline space
spaces := (wordSep|blockComment)* spaces := (wordSep|blockComment)*
// target: operands
target = "@" target = "@"
// source: operands
source = "&" source = "&"
null = "-" null = "-"
colon = ":" colon = ":"
// operand: operands
operand (an operand) = identifier | target | source operand (an operand) = identifier | target | source
comma = "," comma = spaces "," spaces
// Operands: operands
Operands (operands) = nonemptyListOf<operand, comma> Operands (operands) = nonemptyListOf<operand, comma>
// segments: resource
segments = caseInsensitive<"segments">|caseInsensitive<"sections"> segments = caseInsensitive<"segments">|caseInsensitive<"sections">
|caseInsensitive<"segment">|caseInsensitive<"section"> |caseInsensitive<"segment">|caseInsensitive<"section">
|caseInsensitive<"ticks">|caseInsensitive<"tick"> |caseInsensitive<"ticks">|caseInsensitive<"tick">
|caseInsensitive<"clock"> |caseInsensitive<"steps">|caseInsensitive<"step">
SegmentResource = segments
points = caseInsensitive<"points">|caseInsensitive<"pts">|caseInsensitive<"power"> points = caseInsensitive<"points">|caseInsensitive<"pts">|caseInsensitive<"power">
experience = caseInsensitive<"experience"> experience = caseInsensitive<"experience">
xp = caseInsensitive<"XP">|caseInsensitive<"EXP"> xp = caseInsensitive<"XP">|caseInsensitive<"EXP">
ExperienceFull = experience points ExperienceFull = experience points
// XpResource: resource
XpResource = ExperienceFull|experience|xp XpResource = ExperienceFull|experience|xp
health = caseInsensitive<"health">|caseInsensitive<"life">|caseInsensitive<"hit"> health = caseInsensitive<"health">|caseInsensitive<"life">|caseInsensitive<"hit">
HealthFull = health points HealthFull = health points
hp = caseInsensitive<"HP"> hp = caseInsensitive<"HP">
// HpResource: resource
HpResource = HealthFull|health|hp HpResource = HealthFull|health|hp
mind = caseInsensitive<"mind">|caseInsensitive<"magic">|caseInsensitive<"mana"> mind = caseInsensitive<"mind">|caseInsensitive<"magic">|caseInsensitive<"mana">
MindFull = mind points MindFull = mind points
mp = caseInsensitive<"MP"> mp = caseInsensitive<"MP">
// MpResource: resource
MpResource = MindFull|mind|mp MpResource = MindFull|mind|mp
item = caseInsensitive<"items">|caseInsensitive<"item">|caseInsensitive<"inventory"> item = caseInsensitive<"items">|caseInsensitive<"item">|caseInsensitive<"inventory">
ItemFull = item points ItemFull = item points
ip = caseInsensitive<"IP"> ip = caseInsensitive<"IP">
// IpResource: resource
IpResource = ItemFull|item|ip IpResource = ItemFull|item|ip
zero = caseInsensitive<"zero"> zero = caseInsensitive<"zero">
charge = points|caseInsensitive<"charge">|segments charge = points|caseInsensitive<"charge">|segments
ZeroFull = zero charge ZeroFull = zero charge
zp = caseInsensitive<"ZP">|caseInsensitive<"ZC"> zp = caseInsensitive<"ZP">|caseInsensitive<"ZC">
// ZpResource: resource
ZpResource = ZeroFull|zero|zp ZpResource = ZeroFull|zero|zp
blood = caseInsensitive<"blood">|caseInsensitive<"grave"> blood = caseInsensitive<"blood">|caseInsensitive<"grave">
BloodFull = blood points BloodFull = blood points
bp = caseInsensitive<"BP"> bp = caseInsensitive<"BP">
// BpResource: resource
BpResource = BloodFull|blood|bp BpResource = BloodFull|blood|bp
turns = caseInsensitive<"turns">|caseInsensitive<"turn">|caseInsensitive<"actions">|caseInsensitive<"action"> turns = caseInsensitive<"turns">|caseInsensitive<"turn">|caseInsensitive<"actions">|caseInsensitive<"action">
TurnsFull = turns points TurnsFull = turns points
tp = caseInsensitive<"TP">|caseInsensitive<"AP"> tp = caseInsensitive<"TP">|caseInsensitive<"AP">
// TpResource: resource
TpResource = TurnsFull|turns|tp 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 |IpResource|ZpResource|BpResource|TpResource
fabula = caseInsensitive<"Fabula"> fabula = caseInsensitive<"Fabula">
@ -75,28 +91,41 @@ FabulaDSL {
FabulaFull = fabula points FabulaFull = fabula points
UltimaFull = ultima points UltimaFull = ultima points
SpecialFull = special points SpecialFull = special points
// FabulaResource, UltimaResource, SpecialResource, SpResource: resource
FabulaResource = FabulaFull|fabula|fp FabulaResource = FabulaFull|fabula|fp
UltimaResource = UltimaFull|ultima|up UltimaResource = UltimaFull|ultima|up
SpecialResource = SpecialFull|special|sp SpecialResource = SpecialFull|special|sp
SpResource = FabulaResource|UltimaResource|SpecialResource SpResource = FabulaResource|UltimaResource|SpecialResource
// LevelResource: resource
LevelResource = caseInsensitive<"Level">|caseInsensitive<"lvl">|caseInsensitive<"lv"> LevelResource = caseInsensitive<"Level">|caseInsensitive<"lvl">|caseInsensitive<"lv">
money = caseInsensitive<"Zenit">|caseInsensitive<"z">|caseInsensitive<"gold">|caseInsensitive<"gil">|caseInsensitive<"gp"> money = caseInsensitive<"Zenit">|caseInsensitive<"z">|caseInsensitive<"gold">|caseInsensitive<"gil">|caseInsensitive<"gp">
// MoneyResource: resource
MoneyResource = money MoneyResource = money
materials = caseInsensitive<"materials">|caseInsensitive<"mats"> materials = caseInsensitive<"materials">|caseInsensitive<"mats">
MaterialsFull = money wordSep materials MaterialsFull = money #wordSep materials
// MaterialsResource: resource
MaterialsResource = MaterialsFull|materials 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 Resource (the name of a resource) = MeteredResource|UnmeteredResource
// plus, minus, deltaOperator: sign
plus = "+" plus = "+"
minus = "-" minus = "-"
deltaOperator = plus|minus deltaOperator = plus|minus
// integer: numberValue
integer (an integer) = digit+ integer (an integer) = digit+
// delta: numberValue, sign
Delta (a delta) = deltaOperator integer Delta (a delta) = deltaOperator integer
// elements: element
fireElement = caseInsensitive<"fire">|caseInsensitive<"flame">|caseInsensitive<"burn"> fireElement = caseInsensitive<"fire">|caseInsensitive<"flame">|caseInsensitive<"burn">
waterElement = caseInsensitive<"water">|caseInsensitive<"poison">|caseInsensitive<"acid"> waterElement = caseInsensitive<"water">|caseInsensitive<"poison">|caseInsensitive<"acid">
lightningElement = caseInsensitive<"lightning">|caseInsensitive<"zap">|caseInsensitive<"electricity"> lightningElement = caseInsensitive<"lightning">|caseInsensitive<"zap">|caseInsensitive<"electricity">
@ -116,6 +145,7 @@ FabulaDSL {
|lightElement|darkElement |lightElement|darkElement
|physicalElement|nonElement|healingElement |physicalElement|nonElement|healingElement
// affinities: affinity
absorbAffinity = "??"|caseInsensitive<"drained">|caseInsensitive<"drains">|caseInsensitive<"drain"> absorbAffinity = "??"|caseInsensitive<"drained">|caseInsensitive<"drains">|caseInsensitive<"drain">
|caseInsensitive<"absorbed">|caseInsensitive<"absorbs">|caseInsensitive<"absorb"> |caseInsensitive<"absorbed">|caseInsensitive<"absorbs">|caseInsensitive<"absorb">
immuneAffinity = "."|caseInsensitive<"immunity">|caseInsensitive<"blocked">|caseInsensitive<"blocks">|caseInsensitive<"immune">|caseInsensitive<"block"> 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 affinity (an affinity indicator) = vulnerableAffinity|resistAffinity|immuneAffinity|absorbAffinity|normalAffinity
meteredSeparator = ~lineCommentStart ~blockCommentStart "/" meteredSeparator = ~lineCommentStart ~blockCommentStart "/"
MeteredValue = integer MaxValue // MaxValue: numberValue, maxValue
MaxValue = meteredSeparator integer 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? DeltaOperationAlternate = Operands colon Delta affinity? Resource? elementalType?
DeltaOperationAlternate2 = Operands colon Resource Delta affinity? 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 SetMeteredOperationAlternate = Operands colon MeteredValue MeteredResource
SetMeteredOperationAlternate2 = Operands colon MeteredResource MeteredValue 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 SetValueOperationAlternate = Operands colon integer Resource
SetValueOperationAlternate2 = Operands colon Resource integer 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 SetMaxOperationAlternate = Operands colon MaxValue MeteredResource
SetMaxOperationAlternate2 = Operands colon MeteredResource MaxValue 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 ClearOperationAlternate = Operands colon null Resource
ClearOperationAlternate2 = Operands colon Resource null ClearOperationAlternate2 = Operands colon Resource null
// TODO: continue implementation list from here
StatusOrItemCounterUnwrapped = "x"? integer StatusOrItemCounterUnwrapped = "x"? integer
StatusOrItemCounterWrapped = "(" StatusOrItemCounterUnwrapped ")" StatusOrItemCounterWrapped = "(" StatusOrItemCounterUnwrapped ")"
StatusOrItemCounter = 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 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 notNewlineOrComment = ~newline ~lineCommentStart ~blockCommentStart any
textToEndOfLine = notNewlineOrComment+ textToEndOfLine = notNewlineOrComment+
@ -159,12 +206,13 @@ FabulaDSL {
avoid = caseInsensitive<"avoided">|caseInsensitive<"avoids">|caseInsensitive<"avoid"> avoid = caseInsensitive<"avoided">|caseInsensitive<"avoids">|caseInsensitive<"avoid">
dodge = caseInsensitive<"dodged">|caseInsensitive<"dodges">|caseInsensitive<"dodge"> dodge = caseInsensitive<"dodged">|caseInsensitive<"dodges">|caseInsensitive<"dodge">
immune = caseInsensitive<"immune">
miss = caseInsensitive<"missed">|caseInsensitive<"misses">|caseInsensitive<"miss"> miss = caseInsensitive<"missed">|caseInsensitive<"misses">|caseInsensitive<"miss">
resist = caseInsensitive<"resisted">|caseInsensitive<"resists">|caseInsensitive<"resist"> resist = caseInsensitive<"resisted">|caseInsensitive<"resists">|caseInsensitive<"resist">
fail = caseInsensitive<"failed">|caseInsensitive<"fails">|caseInsensitive<"fail"> fail = caseInsensitive<"failed">|caseInsensitive<"fails">|caseInsensitive<"fail">
block = caseInsensitive<"blocked">|caseInsensitive<"blocks">|caseInsensitive<"block"> block = caseInsensitive<"blocked">|caseInsensitive<"blocks">|caseInsensitive<"block">
parry = caseInsensitive<"parried">|caseInsensitive<"parries">|caseInsensitive<"parry"> 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 FailOperation = Operands colon FailReason
allySide = caseInsensitive<"allies">|caseInsensitive<"ally"> allySide = caseInsensitive<"allies">|caseInsensitive<"ally">
@ -177,6 +225,77 @@ FabulaDSL {
|caseInsensitive<"opponents">|caseInsensitive<"opponent">|caseInsensitive<"opposed"> |caseInsensitive<"opponents">|caseInsensitive<"opponent">|caseInsensitive<"opposed">
characterSide = allySide|enemySide 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 = ">" printOperator = ">"
PrintOperation = printOperator textToEndOfLine PrintOperation = printOperator textToEndOfLine
PrintOperationWithOperands = Operands colon printOperator textToEndOfLine PrintOperationWithOperands = Operands colon printOperator textToEndOfLine
@ -186,7 +305,8 @@ FabulaDSL {
| SetValueOperation | SetValueOperationAlternate | SetValueOperationAlternate2 | SetValueOperation | SetValueOperationAlternate | SetValueOperationAlternate2
| SetMaxOperation | SetMaxOperationAlternate | SetMaxOperationAlternate2 | SetMaxOperation | SetMaxOperationAlternate | SetMaxOperationAlternate2
| ClearOperation | ClearOperationAlternate | ClearOperationAlternate2 | ClearOperation | ClearOperationAlternate | ClearOperationAlternate2
| StatusOrItemDeltaOperation | StatusOrItemCounterDeltaOperation | StatusOrItemCounterSetOperation | StatusOrItemAddOperation | StatusOrItemRemoveOperation | StatusOrItemCounterSetOperation
| StatusOrItemCounterDeltaOperation | StatusOrItemCounterDeltaOperationAlternate
| SetTargetOperation | SetSourceOperation | FailOperation | PrintOperationWithOperands | PrintOperation | SetTargetOperation | SetSourceOperation | FailOperation | PrintOperationWithOperands | PrintOperation
silentOperator = "~" silentOperator = "~"
@ -207,7 +327,8 @@ FabulaDSL {
beginKeyword = caseInsensitive<"begin"> beginKeyword = caseInsensitive<"begin">
endKeyword = caseInsensitive<"end"> endKeyword = caseInsensitive<"end">
BlockStart = silentOperator? beginKeyword SetSourceOperation? SetTargetOperation? colon textToEndOfLine BlockTitle = colon textToEndOfLine
BlockStart = silentOperator? beginKeyword SetSourceOperation? SetTargetOperation? BlockTitle?
BlockEnd = endKeyword textToEndOfLine? BlockEnd = endKeyword textToEndOfLine?
Block = BlockStart operationTerminator CodeSegment BlockEnd operationTerminatorOrEnd Block = BlockStart operationTerminator CodeSegment BlockEnd operationTerminatorOrEnd

File diff suppressed because it is too large Load Diff

@ -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<ElementalType|null>("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>("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<NumberSign>("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<number>("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<string>("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|null>("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>("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<never> {
return Operands<never>()
},
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<string> {
return Operands((this as ParserNode).identifier)
},
target(): Set<typeof Target> {
return Operands(Target)
},
source(): Set<typeof Source> {
return Operands(Source);
},
["null"](): Set<never> {
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<Operands>("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<Operands>("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<boolean>("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<number>("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<number>("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>("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<CharacterSide>("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`)
}
})

@ -11,6 +11,7 @@ body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif; sans-serif;
font-size: 15px;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }

@ -1,4 +1,11 @@
import {isDefined} from "../types/type_check"; import {isDefined} from "../types/type_check";
import {
createResourceManipulator,
MeteredResource,
Resource,
ResourceManipulator,
UnmeteredResource
} from "./Resources";
export enum CharacterHealth { export enum CharacterHealth {
Full = "Full", Full = "Full",
@ -77,7 +84,7 @@ export function healthToBounds(health: CharacterHealth | undefined): string {
return `${minPercentage ?? ""}${minPercentage !== null && maxPercentage !== null ? "-" : ""}${maxPercentage ?? ""}%` 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) { if (!isDefined(hp) || !isDefined(maxHp) || maxHp <= 0) {
return return
} }
@ -149,6 +156,13 @@ export function turnStateToDescription(state: CharacterTurnState): string {
return CharacterTurnStates[state].description 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 { export enum SPType {
UltimaPoints = "Ultima", UltimaPoints = "Ultima",
FabulaPoints = "Fabula", FabulaPoints = "Fabula",
@ -175,7 +189,7 @@ export function spTypeToDescription(sp: SPType): string {
export interface StatusEffectInstance { export interface StatusEffectInstance {
readonly id: string readonly id: string
readonly count?: number readonly count: number
} }
export enum CharacterSide { export enum CharacterSide {
@ -186,9 +200,11 @@ export enum CharacterSide {
export interface Character { export interface Character {
readonly id: string readonly id: string
readonly side?: CharacterSide readonly side?: CharacterSide
readonly minion?: boolean
readonly portraitUrl?: string readonly portraitUrl?: string
readonly name?: string readonly name?: string
readonly altName?: string readonly altName?: string
readonly description?: string
readonly level?: number readonly level?: number
readonly xp?: number readonly xp?: number
readonly maxXp?: number readonly maxXp?: number
@ -212,6 +228,8 @@ export interface Character {
readonly canAct?: boolean readonly canAct?: boolean
readonly statuses?: readonly StatusEffectInstance[] readonly statuses?: readonly StatusEffectInstance[]
readonly order?: number readonly order?: number
readonly zenit?: number
readonly materials?: number
readonly privacy?: CharacterPrivacy readonly privacy?: CharacterPrivacy
} }
@ -341,6 +359,123 @@ export const CharacterPrivacySettings = {
} }
} as const satisfies {readonly [value in CharacterPrivacy]: CharacterPrivacySetting} } 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<typeof CharacterResourceMax[Resource] | typeof CharacterResourceValue[Resource], undefined>,
Character, Resource>(CharacterResourceValue, CharacterResourceMax)
export const CharacterResources: ResourceManipulator<Character, Resource> = {
...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 { export function applyCharacterPrivacy(character: Character): Character|null {
const privacySettings = CharacterPrivacySettings[character.privacy ?? CharacterPrivacy.Hidden] const privacySettings = CharacterPrivacySettings[character.privacy ?? CharacterPrivacy.Hidden]
if (!privacySettings.showCharacter) { if (!privacySettings.showCharacter) {
@ -400,3 +535,57 @@ export function applyCharacterPrivacy(character: Character): Character|null {
} }
return out 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)
}
}
}
}

@ -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<Clock, Resource> =
createResourceManipulator<
Exclude<typeof ClockResourceMax[Resource]|typeof ClockResourceValue[Resource], undefined>,
Clock, Resource>(ClockResourceValue, ClockResourceMax)

@ -1,17 +1,5 @@
import {Character, CharacterSide, SPType} from "./Character"; import {Character, CharacterSide, SPType} from "./Character";
import {Clock} from "./Clock";
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
}
export interface SessionState { export interface SessionState {
readonly usedSp: {readonly [key in SPType]?: number} readonly usedSp: {readonly [key in SPType]?: number}
@ -33,6 +21,7 @@ export interface BaseTimerState {
readonly type: TimerDirection readonly type: TimerDirection
readonly id: string readonly id: string
readonly text: string readonly text: string
readonly order?: number
} }
export interface StoppedTimerState extends BaseTimerState { export interface StoppedTimerState extends BaseTimerState {
@ -68,18 +57,46 @@ export interface GameState {
readonly timers: readonly TimerState[] readonly timers: readonly TimerState[]
} }
export function getClockById(state: GameState, id: string): Clock|undefined { export interface BaseIdentifierLookupResult {
return state.clocks.find((clock) => clock.id === id) readonly type: string
} }
export interface NullLookupResult extends BaseIdentifierLookupResult {
export function getCharacterById(state: GameState, id: string): Character|undefined { readonly type: "null"
return state.characters.find((character) => character.id === id)
} }
export interface ClockLookupResult extends BaseIdentifierLookupResult {
export function getStatusById(state: GameState, id: string): StatusEffect|undefined { readonly type: "clock"
return state.statuses.find((status) => status.id === id) readonly clock: Clock
} }
export interface CharacterLookupResult extends BaseIdentifierLookupResult {
export function getTimerById(state: GameState, id: string): TimerState|undefined { readonly type: "character"
return state.timers.find((timer) => timer.id === id) 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"}
} }

@ -1,4 +1,4 @@
import {GameState} from "./GameState"; import {GameState, IdentifierLookupResult, lookupIdentifier} from "./GameState";
export enum ElementalType { export enum ElementalType {
Fire = "fire", Fire = "fire",
@ -22,32 +22,19 @@ export enum Affinity {
Vulnerable = "vulnerable", 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 { export enum NumberSign {
Positive = "+", Positive = "+",
Negative = "-", 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 { export enum FailReason {
Avoid = "avoid", Avoid = "avoid",
Dodge = "dodge", Dodge = "dodge",
@ -62,22 +49,49 @@ export const Target: unique symbol = Symbol("target");
export const Source: unique symbol = Symbol("source"); export const Source: unique symbol = Symbol("source");
export type Operands = Set<string|typeof Target|typeof Source>; export type Operands = Set<string|typeof Target|typeof Source>;
export function Operands<T extends string|typeof Target|typeof Source>(...items: readonly T[]): Set<T> { export function OperandItems<T extends string|typeof Target|typeof Source>(...items: readonly T[]): Set<T> {
return new Set<T>(items) return new Set<T>(items)
} }
export function OperandsFrom<T extends string|typeof Target|typeof Source>(children: readonly Set<T>[]): Set<T> { export function OperandSets<T extends string|typeof Target|typeof Source>(children: readonly Set<T>[]): Set<T> {
return new Set<T>([...children.flatMap((child) => [...child.values()])]) return new Set<T>([...children.flatMap((child) => [...child.values()])])
} }
export interface ParseContext { export interface EvaluationContext {
readonly timestamp: number readonly timestamp: number
readonly source: readonly string[] readonly source: readonly string[]
readonly target: readonly string[] readonly target: readonly string[]
readonly game: GameState readonly game: GameState
} }
export interface MarkdownContext extends ParseContext {} export function evaluateOperands(operands: Operands, ctx: EvaluationContext): Set<string> {
const result = new Set<string>()
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<string, IdentifierLookupResult> {
const set = evaluateOperands(operands, ctx)
const result = new Map<string, IdentifierLookupResult>()
for (const operand of set) {
result.set(operand, lookupIdentifier(ctx.game, operand))
}
return result
}
export interface MarkdownContext extends EvaluationContext {}
export interface MarkdownOutput extends MarkdownContext { export interface MarkdownOutput extends MarkdownContext {
readonly output: string readonly output: string|null
} }

@ -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<Resource>(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<Resource>(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<ObjectT, ResourceT> {
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<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}): ResourceManipulator<ObjectT, ResourceT> {
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)
}
}
}

@ -2,11 +2,12 @@
color: white; color: white;
position: sticky; position: sticky;
text-align: center; text-align: center;
padding: 2px 0 5px; font-size: 2em;
text-shadow: 0 0 5px black; padding: 0.05em 0 0.125em;
text-shadow: 0 0 0.125em black;
align-self: stretch; align-self: stretch;
user-select: none; user-select: none;
border-bottom: 1px solid; border-bottom: 0.05em solid;
top: 0; top: 0;
z-index: 90; z-index: 90;
} }
@ -17,15 +18,15 @@
.totalFPSpent, .totalUPSpent { .totalFPSpent, .totalUPSpent {
color: white; color: white;
line-height: 60px; line-height: 1.333;
-webkit-text-stroke: 2px black; -webkit-text-stroke: 0.0444em black;
text-shadow: 2px 2px 2px black; text-shadow: 0.0444em 0.0444em 0.0444em black;
font-size: 45px; font-size: 2.25em;
letter-spacing: -3px; letter-spacing: -0.06667em;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
height: 60px; height: 1.333em;
width: 60px; width: 1.333em;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
user-select: none; user-select: none;
@ -42,13 +43,13 @@
.totalSPSpent { .totalSPSpent {
color: white; color: white;
line-height: 60px; line-height: 2;
text-shadow: 1px 1px 3px black; text-shadow: 0.0333em 0.0333em 0.1em black;
font-size: 30px; font-size: 1.5em;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
height: 60px; height: 2em;
width: 200px; width: 6.6667em;
white-space: nowrap; white-space: nowrap;
user-select: none; user-select: none;
flex: 0 0 auto; flex: 0 0 auto;
@ -66,7 +67,7 @@
.enemy-head.inactive { .enemy-head.inactive {
background: linear-gradient(to right, transparent 0%, maroon 50%, transparent 100%); background: linear-gradient(to right, transparent 0%, maroon 50%, transparent 100%);
border-bottom: 1px solid maroon; border-bottom-color: maroon;
} }
.ally-head { .ally-head {
@ -76,7 +77,7 @@
.enemy-head { .enemy-head {
background: linear-gradient(to right, transparent 0%, maroon 10%, red 50%, maroon 90%, transparent 100%); 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 { .ally-head.active::before, .ally-head.active::after {

@ -1,4 +1,4 @@
import React, {useEffect, useState} from 'react'; import React, {useEffect, useMemo, useState} from 'react';
import './App.css'; import './App.css';
import {Col, Container, Row, Stack} from "react-bootstrap"; import {Col, Container, Row, Stack} from "react-bootstrap";
import {CharacterStatus} from "./CharacterStatus"; import {CharacterStatus} from "./CharacterStatus";
@ -7,6 +7,7 @@ import OverlayTrigger from "react-bootstrap/OverlayTrigger";
import Tooltip from "react-bootstrap/Tooltip"; import Tooltip from "react-bootstrap/Tooltip";
import {TurnTimer} from "./TurnTimer"; import {TurnTimer} from "./TurnTimer";
import {CharacterSide} from "../model/Character"; import {CharacterSide} from "../model/Character";
import {evaluate} from "../grammar/interpreter";
function useJson<T>(url: string): T | null { function useJson<T>(url: string): T | null {
const [data, setData] = useState<T | null>(null); const [data, setData] = useState<T | null>(null);
@ -27,7 +28,34 @@ function useJson<T>(url: string): T | null {
} }
function App() { function App() {
const state = useJson<GameState>("/api/current.json") const origState = useJson<GameState>("/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 startTime = Date.now()
const endTime = Date.now() + 2 * 60 * 1000 const endTime = Date.now() + 2 * 60 * 1000
return <React.Fragment> return <React.Fragment>

@ -1,28 +1,33 @@
.characterStatus { .characterStatus {
height: 150px; height: 7.5em;
width: 545px; width: 27.25em;
position: relative; position: relative;
box-sizing: content-box; box-sizing: content-box;
} }
.characterStatus.minion {
margin-left: 6.8125em;
font-size: 0.80em;
}
.characterHeader { .characterHeader {
position: absolute; position: absolute;
left: 205px; left: 10.25em;
bottom: 55px; bottom: 2.75em;
z-index: 4; z-index: 4;
} }
.characterLevel { .characterLevel {
display: inline; display: inline;
color: white; color: white;
-webkit-text-stroke: 1px rgba(0, 0, 0, 0.2); -webkit-text-stroke: 0.05em rgba(0, 0, 0, 0.2);
text-shadow: 0 0 2px black; text-shadow: 0 0 0.1em black;
margin-right: 0.25em; margin-right: 0.2em;
user-select: none; user-select: none;
} }
.characterLevelLabel { .characterLevelLabel {
font-size: smaller; font-size: 0.65em;
font-variant: small-caps; font-variant: small-caps;
user-select: none; user-select: none;
} }
@ -32,21 +37,21 @@
color: white; color: white;
font-family: sans-serif; font-family: sans-serif;
font-weight: bold; font-weight: bold;
font-size: 30px; font-size: 1.5em;
text-align: left; text-align: left;
-webkit-text-stroke: 1px rgba(0, 0, 0, 0.5); -webkit-text-stroke: 0.0333em rgba(0, 0, 0, 0.5);
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); text-shadow: 0.0333em 0.0333em 0.0667em rgba(0, 0, 0, 0.5);
user-select: none; user-select: none;
} }
.characterHpBar, .characterMpBar, .characterIpBar { .characterHpBar, .characterMpBar, .characterIpBar {
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5); box-shadow: 0.1em 0.1em 0.1em rgba(0, 0, 0, 0.5);
border: 2px solid black; border: 0.1em solid black;
transform: skewX(-30deg) translateX(15px); transform: skewX(-30deg) translateX(0.75em);
border-radius: 5px; border-radius: 0.25em;
position: absolute; position: absolute;
width: calc(100% - 30px); width: calc(100% - 1.5em);
margin: 2px 2px 8px 5px; margin: 0.1em 0.1em 0.4em 0.25em;
bottom: 0; bottom: 0;
box-sizing: content-box; box-sizing: content-box;
} }
@ -57,8 +62,6 @@
font-weight: bold; font-weight: bold;
font-style: italic; font-style: italic;
text-align: right; text-align: right;
-webkit-text-stroke: 1px black;
text-shadow: 2px 2px rgba(0, 0, 0, 0.5);
position: absolute; position: absolute;
bottom: 0; bottom: 0;
pointer-events: none; pointer-events: none;
@ -67,69 +70,70 @@
.characterHp { .characterHp {
position: absolute; position: absolute;
height: 60px; height: 3em;
left: 157px; left: 7.85em;
right: 0; right: 0;
bottom: 28px; bottom: 1.4em;
overflow: visible; overflow: visible;
z-index: 1; z-index: 1;
box-sizing: content-box; box-sizing: content-box;
} }
.characterHpBar { .characterHpBar {
height: 25px; height: 1.25em;
}
.characterHpValue, .characterHealthText {
right: 5px;
transition: color 0.3s ease-in;
} }
.characterHpValue { .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 { .characterHealthText {
font-size: 30px; font-size: 1.5em;
right: 5px; right: 0.1667em;
bottom: 15px; bottom: 0.5em;
transition: color 0.3s ease-in; transition: color 0.3s ease-in;
} }
.characterMp, .characterIp { .characterMp, .characterIp {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
height: 40px; height: 2em;
box-sizing: content-box; box-sizing: content-box;
} }
.characterMp { .characterMp {
left: 140px; left: 7em;
right: 155px; right: 7.75em;
z-index: 3; z-index: 3;
} }
.characterIp { .characterIp {
width: 150px; width: 7.5em;
right: 20px; right: 1em;
z-index: 2; z-index: 2;
} }
.characterMpBar, .characterIpBar { .characterMpBar, .characterIpBar {
height: 20px; height: 1em;
} }
.characterMpValue, .characterIpValue { .characterMpValue, .characterIpValue {
font-size: 40px; font-size: 2em;
right: 10px; -webkit-text-stroke: 0.025em black;
text-shadow: 0.05em 0.05em rgba(0, 0, 0, 0.5);
right: 0.25em;
} }
.characterPortrait { .characterPortrait {
position: absolute; position: absolute;
top: 10px; top: 0.5em;
bottom: 15px; bottom: 0.75em;
left: 90px; left: 4.5em;
width: 125px; width: 6.25em;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
background-size: cover; background-size: cover;
@ -139,10 +143,10 @@
.characterZeroGauge { .characterZeroGauge {
position: absolute; position: absolute;
top: 5px; top: 0.25em;
bottom: 10px; bottom: 0.5em;
left: 50px; left: 2.5em;
width: 65px; width: 3.75em;
} }
.characterZeroBar, .characterZeroBarBack, .characterZeroBarPulse{ .characterZeroBar, .characterZeroBarBack, .characterZeroBarPulse{
position: absolute; position: absolute;
@ -152,7 +156,7 @@
right: 0; right: 0;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: left bottom; background-position: left bottom;
background-size: auto 135px; background-size: auto 6.75em;
z-index: 0; z-index: 0;
} }
@ -165,10 +169,11 @@
@keyframes zeroBarPulse { @keyframes zeroBarPulse {
from { from {
opacity: 0; opacity: 75%;
transform: scaleX(100%) scaleY(100%) translateX(1%);
} }
50% { 50% {
opacity: 80%; opacity: 60%;
transform: scaleX(120%) scaleY(110%) translateX(-3%); transform: scaleX(120%) scaleY(110%) translateX(-3%);
} }
to { to {
@ -186,43 +191,43 @@
} }
.characterZeroBarPulse.active { .characterZeroBarPulse.active {
animation: 1s ease-out infinite zeroBarPulse; animation: 0.75s ease-out infinite zeroBarPulse;
pointer-events: none; pointer-events: none;
} }
.characterKOBar { .characterKOBar {
position: absolute; position: absolute;
top: 43px; top: 1.592em;
bottom: 68px; bottom: 2.518em;
left: 90px; left: 3.333em;
width: 125px; width: 4.629em;
background-color: black; background-color: black;
z-index: 0; z-index: 0;
transform: rotate(-20deg); transform: rotate(-20deg);
text-align: center; text-align: center;
font-size: 27px; font-size: 1.35em;
line-height: 42px; line-height: 1.5;
font-weight: bold; font-weight: bold;
color: white; color: white;
user-select: none; user-select: none;
border-radius: 15px 0; border-radius: 0.555em 0;
} }
.characterTurns { .characterTurns {
position: absolute; position: absolute;
top: 5px; top: 0.125em;
left: 5px; left: 0.125em;
width: 40px; width: 1em;
height: 40px; height: 1em;
font-size: 40px; font-size: 2em;
font-weight: bold; font-weight: bold;
-webkit-text-stroke: 2px black; -webkit-text-stroke: 0.05em black;
text-shadow: 2px 2px 2px black; text-shadow: 0.05em 0.05em 0.05em black;
line-height: 40px; line-height: 1;
text-align: center; text-align: center;
box-sizing: border-box; box-sizing: border-box;
border: 2px solid black; border: 0.05em solid black;
border-radius: 9px 0; border-radius: 0.225em 0;
color: white; color: white;
transition: background-color 0.3s; transition: background-color 0.3s;
user-select: none; user-select: none;
@ -256,52 +261,39 @@
background-color: paleturquoise; background-color: paleturquoise;
} }
.characterSp { .characterSp, .characterBp {
position: absolute; position: absolute;
color: white; color: white;
line-height: 40px; line-height: 1.333;
-webkit-text-stroke: 2px black; -webkit-text-stroke: 0.06667em black;
text-shadow: 2px 2px 2px black; text-shadow: 0.06667em 0.06667em 0.06667em black;
font-size: 30px; font-size: 1.5em;
letter-spacing: -3px; letter-spacing: -0.1em;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
top: 50px;
left: 5px;
width: 40px;
height: 40px;
user-select: none; user-select: none;
left: 0.16667em;
width: 1.3333em;
height: 1.3333em;
opacity: 50%;
transition: opacity 0.3s ease-out;
} }
.characterSpFabula { .characterBp {
background: url("fabula-points.svg"); top: 3.16667em;
background: url("blood-points.svg")
} }
.characterSpUltima { .characterSp {
background: url("ultima-points.svg"); top: 1.6667em;
} }
.characterBp { .characterSpFabula {
position: absolute; background: url("fabula-points.svg");
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")
} }
.characterSp, .characterBp { .characterSpUltima {
opacity: 50%; background: url("ultima-points.svg");
transition: opacity 0.3s ease-out;
} }
.characterSp:hover, .characterBp:hover { .characterSp:hover, .characterBp:hover {
@ -310,9 +302,9 @@
.characterStatuses { .characterStatuses {
position: absolute; position: absolute;
top: 5px; top: 0.25em;
right: 5px; right: 0.25em;
left: 225px; left: 11.25em;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -321,22 +313,22 @@
.characterStatusIcon { .characterStatusIcon {
position: relative; position: relative;
flex: 0 0 36px; flex: 0 0 1.8em;
width: 36px; width: 1.8em;
height: 48px; height: 2.4em;
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
margin-left: 5px; margin-left: 0.25em;
overflow: visible; overflow: visible;
} }
.characterStatusIconCountBadge { .characterStatusIconCountBadge {
display: block; display: block;
color: white; color: white;
text-shadow: 2px 2px 0 black; text-shadow: 0.08em 0.08em 0 black;
-webkit-text-stroke: 1px black; -webkit-text-stroke: 0.04em black;
font-size: 25px; font-size: 1.25em;
letter-spacing: -3px; letter-spacing: -0.15em;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
position: absolute; position: absolute;
@ -351,9 +343,9 @@
} }
.characterStatusCount, .characterHelpValue { .characterStatusCount, .characterHelpValue {
margin-left: 5px; margin-left: 0.3125em;
font-style: italic; font-style: italic;
font-size: smaller; font-size: 0.8rem;
} }
.characterStatusHeader, .characterHelpHeader { .characterStatusHeader, .characterHelpHeader {
@ -362,7 +354,7 @@
.characterStatusDescription, .characterHelpDescription { .characterStatusDescription, .characterHelpDescription {
text-align: left; text-align: left;
font-size: smaller; font-size: 0.8em;
} }
.characterStatusCount::before, .characterHelpValue::before { .characterStatusCount::before, .characterHelpValue::before {

@ -76,7 +76,7 @@ const ipBarStyle: SpringyValueInterpolatables<ResourceBarStyles> = {
} }
export function CharacterStatus({character, statuses, active}: {character: Character, statuses: readonly StatusEffect[], active: boolean}): ReactElement { 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 {hp, maxHp} = character
const effectiveMaxHp = maxHp ?? 100 const effectiveMaxHp = maxHp ?? 100
@ -87,19 +87,15 @@ export function CharacterStatus({character, statuses, active}: {character: Chara
starting: 0, starting: 0,
flash: effectiveHp * 2 <= effectiveMaxHp && effectiveHp > 0, flash: effectiveHp * 2 <= effectiveMaxHp && effectiveHp > 0,
}) })
const logged = function<T>(v:T): T {
console.log(v)
return v
}
const {hpText, hpTextStyleInterpolated, hpBarStyleInterpolated} = useMemo(() => { const {hpText, hpTextStyleInterpolated, hpBarStyleInterpolated} = useMemo(() => {
if ((isDefined(hp) && isDefined(maxHp)) || isDefined(health)) { if ((isDefined(hp) && isDefined(maxHp)) || isDefined(health)) {
return { return {
hpText: isDefined(hp) hpText: isDefined(hp)
? to([hpRecentSpring], recentValue => `${Math.round(logged(recentValue))}`) ? to([hpRecentSpring], recentValue => `${Math.round(recentValue)}`)
: "", : "",
hpBarStyleInterpolated: evaluateResourceBarStyles(hpBarStyle, hpInterpolate), hpBarStyleInterpolated: evaluateResourceBarStyles(hpBarStyle, hpInterpolate),
hpTextStyleInterpolated: { hpTextStyleInterpolated: {
color: to([hpRecentSpring], recentValue => healthToColor(hpToHealth(recentValue, maxHp))) color: to([hpRecentSpring], recentValue => healthToColor(hpToHealth({hp: recentValue, maxHp})))
} }
} }
} else { } else {
@ -279,7 +275,7 @@ export function CharacterStatus({character, statuses, active}: {character: Chara
const characterStatuses = character.statuses ?? [] const characterStatuses = character.statuses ?? []
const effectiveStatuses = characterStatuses.map((statusInstance) => ({ 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 = <Tooltip> const hpTooltip = <Tooltip>
<div className={"characterHelpHeader"}> <div className={"characterHelpHeader"}>
@ -312,7 +308,7 @@ export function CharacterStatus({character, statuses, active}: {character: Chara
</Tooltip> </Tooltip>
return <div className="characterStatus"> return <div className={"characterStatus" + (minion ? " minion" : "")}>
<animated.div className={"characterPortrait"} style={characterPortraitStyleInterpolated} /> <animated.div className={"characterPortrait"} style={characterPortraitStyleInterpolated} />
{isDefined(maxZp) && isDefined(zp) && <OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ {isDefined(maxZp) && isDefined(zp) && <OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={
<Tooltip> <Tooltip>
@ -412,18 +408,18 @@ export function CharacterStatus({character, statuses, active}: {character: Chara
</OverlayTrigger>} </OverlayTrigger>}
{isDefined(effectiveStatuses) && effectiveStatuses.length > 0 && {isDefined(effectiveStatuses) && effectiveStatuses.length > 0 &&
<div className={"characterStatuses"}> <div className={"characterStatuses"}>
{effectiveStatuses.map(({id, count, description, iconUrl}) => {effectiveStatuses.map(({id, name, count, description, iconUrl}) =>
<OverlayTrigger key={id} delay={{show: 300, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ <OverlayTrigger key={id} delay={{show: 300, hide: 0}} trigger={["hover", "click", "focus"]} overlay={
<Tooltip> <Tooltip>
<div className={"characterStatusHeader"}> <div className={"characterStatusHeader"}>
<span className={"characterStatusName"}>{name}</span> <span className={"characterStatusName"}>{name}</span>
{isDefined(count) && <span className={"characterStatusCount"}>{count}</span>}</div> {isDefined(count) && count > 0 && <span className={"characterStatusCount"}>{count}</span>}</div>
{isDefined(description) && <div className={"characterStatusDescription"}> {isDefined(description) && <div className={"characterStatusDescription"}>
{isDefined(count) ? description.replaceAll("%c%", count.toFixed(0)) : description} {count > 0 ? description.replaceAll("%c%", count.toFixed(0)) : description}
</div>} </div>}
</Tooltip> </Tooltip>
} placement={"bottom"}> } placement={"bottom"}>
<div className={"characterStatusIcon"} style={{backgroundImage: `url("${iconUrl ?? DefaultStatus}")`}}><span className={"characterStatusIconCountBadge"}>{count}</span></div> <div className={"characterStatusIcon"} style={{backgroundImage: `url("${iconUrl ?? DefaultStatus}")`}}>{count > 0 && <span className={"characterStatusIconCountBadge"}>{count}</span>}</div>
</OverlayTrigger> </OverlayTrigger>
)} )}
</div>} </div>}

@ -11,8 +11,11 @@
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"alwaysStrict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,

Loading…
Cancel
Save