feat: expression-based activatable skill parameters
This commit is contained in:
Generated
+4
-4
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@optolith/database-schema": "^0.47.4",
|
||||
"@optolith/database-schema": "^0.49.0",
|
||||
"@types/node": "^25.9.1",
|
||||
"commit-and-tag-version": "^12.7.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
@@ -842,9 +842,9 @@
|
||||
"license": "MPL-2.0"
|
||||
},
|
||||
"node_modules/@optolith/database-schema": {
|
||||
"version": "0.47.4",
|
||||
"resolved": "https://registry.npmjs.org/@optolith/database-schema/-/database-schema-0.47.4.tgz",
|
||||
"integrity": "sha512-FaKuLaOWD7P6CldlMjcTF/K+VWX82xUH86O4Ys8cFV3AyF7GPQEUyfLHrMprtcta/qWYqPqLUSOm1U3lzcEdAQ==",
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@optolith/database-schema/-/database-schema-0.49.0.tgz",
|
||||
"integrity": "sha512-1YqZnPRCTStzb+RzMyR56dqNlniMuBRTt0gWjJc/C/j7VAFxZ8qVXR5FXki8dKXPcNNK6fiu4hlJ34Fmrgp5Vw==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@optolith/database-schema": "^0.47.4",
|
||||
"@optolith/database-schema": "^0.49.0",
|
||||
"@types/node": "^25.9.1",
|
||||
"commit-and-tag-version": "^12.7.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
|
||||
@@ -48,14 +48,19 @@ export const groupFormatter: UnaryFormatter = value => `(${printOperand(value)})
|
||||
/**
|
||||
* Render a math operation as a string, using the provided function to render the values.
|
||||
*/
|
||||
export const renderMathOperation = <T>(
|
||||
export const renderMathOperation = <T, R extends string | number>(
|
||||
operation: MathOperation<T>,
|
||||
renderValue: (value: T) => string,
|
||||
): string => {
|
||||
renderValue: (value: T) => R,
|
||||
renderDirectValue?: (value: T) => R,
|
||||
): R | string => {
|
||||
if (operation.kind === "Value" && renderDirectValue) {
|
||||
return renderDirectValue(operation.Value)
|
||||
}
|
||||
|
||||
const renderWithParenthesis = (
|
||||
op: MathOperation<T>,
|
||||
addParenthesisTo: MathOperation<T>["kind"][] = [],
|
||||
): string => {
|
||||
): R | string => {
|
||||
const rendered = renderMathOperation(op, renderValue)
|
||||
return addParenthesisTo.includes(op.kind) ? groupFormatter(rendered) : rendered
|
||||
}
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
import { Reader } from "@elyukai/utils/reader"
|
||||
import type {
|
||||
CheckResultArithmetic,
|
||||
CheckResultBasedModifier,
|
||||
CheckResultValue,
|
||||
ExpressionBasedParameterValue,
|
||||
} from "@optolith/database-schema/gen"
|
||||
import { mapNullableDefault } from "@optolith/helpers/nullable"
|
||||
import { assertExhaustive } from "@optolith/helpers/typeSafety"
|
||||
import { divisionFormatter, multiplicationFormatter } from "../../mathOperation.js"
|
||||
import { translateR, type StdReader } from "../../reader.js"
|
||||
|
||||
const renderCheckResultBaseValue = (baseValue: CheckResultValue) => {
|
||||
switch (baseValue.kind) {
|
||||
case "QualityLevels":
|
||||
return translateR("QL")
|
||||
case "SkillPoints":
|
||||
return translateR("SP")
|
||||
case "SkillRating":
|
||||
return translateR("SR")
|
||||
default:
|
||||
return assertExhaustive(baseValue)
|
||||
}
|
||||
}
|
||||
import {
|
||||
divisionFormatter,
|
||||
multiplicationFormatter,
|
||||
renderMathOperation,
|
||||
} from "../../mathOperation.js"
|
||||
import { type StdEnv, type StdReader } from "../../reader.js"
|
||||
|
||||
const getArithmeticFormatter = (arithmetic: CheckResultArithmetic) => {
|
||||
switch (arithmetic.kind) {
|
||||
@@ -32,18 +23,6 @@ const getArithmeticFormatter = (arithmetic: CheckResultArithmetic) => {
|
||||
}
|
||||
}
|
||||
|
||||
interface CheckResultBased {
|
||||
/**
|
||||
* The base value that is derived from the check result.
|
||||
*/
|
||||
base: CheckResultValue
|
||||
|
||||
/**
|
||||
* If defined, it modifies the base value.
|
||||
*/
|
||||
modifier?: CheckResultBasedModifier
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the modifier of a check-result-based parameter of an activatable skill to the base value, using the appropriate arithmetic formatter.
|
||||
*/
|
||||
@@ -53,10 +32,25 @@ export const appendCheckResultModifier = (left: string, modifier: CheckResultBas
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value text for a check-result-based parameter of an activatable
|
||||
* skill.
|
||||
* Renders a value that is based on the result of the skill check, which can
|
||||
* either be a simple value or a math operation.
|
||||
*/
|
||||
export const renderCheckResultBasedValue = (value: CheckResultBased): StdReader<string, "t"> =>
|
||||
renderCheckResultBaseValue(value.base).map(base =>
|
||||
mapNullableDefault(value.modifier, modifier => appendCheckResultModifier(base, modifier), base),
|
||||
export const renderExpressionBasedParameterValue = (
|
||||
value: ExpressionBasedParameterValue,
|
||||
): StdReader<string | number, "t"> =>
|
||||
Reader.asks(({ translate }: StdEnv<"t">) =>
|
||||
renderMathOperation(value, expressionValue => {
|
||||
switch (expressionValue.kind) {
|
||||
case "Constant":
|
||||
return expressionValue.Constant
|
||||
case "QualityLevels":
|
||||
return translate("QL")
|
||||
case "SkillPoints":
|
||||
return translate("SP")
|
||||
case "SkillRating":
|
||||
return translate("SR")
|
||||
default:
|
||||
return assertExhaustive(expressionValue)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Reader } from "@elyukai/utils/reader"
|
||||
import type {
|
||||
CastingTimeDuringLovemaking,
|
||||
CheckResultBasedDuration,
|
||||
DurationForSustained,
|
||||
FixedDuration,
|
||||
ExpressionBasedDuration,
|
||||
Immediate,
|
||||
MusicDuration,
|
||||
PermanentDuration,
|
||||
@@ -11,6 +10,7 @@ import type {
|
||||
} from "@optolith/database-schema/gen"
|
||||
import { mapNullable } from "@optolith/helpers/nullable"
|
||||
import { assertExhaustive } from "@optolith/helpers/typeSafety"
|
||||
import { Case } from "../../../../helpers/enums.js"
|
||||
import type { LocaleMap } from "../../../../helpers/translate.js"
|
||||
import {
|
||||
responsiveTextR,
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { replaceTextIfNeeded } from "../../responsiveText.js"
|
||||
import { formatCombinedTimeSpanR, formatTimeSpanR } from "../../units/timeSpan.js"
|
||||
import { MISSING_VALUE } from "../../unknown.js"
|
||||
import { renderCheckResultBasedValue } from "./checkResultBased.js"
|
||||
import { renderExpressionBasedParameterValue } from "./checkResultBased.js"
|
||||
import { wrapAsMaximum, wrapIfMaximum } from "./isMinimumMaximum.js"
|
||||
import { appendInParensIfNotEmpty } from "./parensIf.js"
|
||||
|
||||
@@ -41,21 +41,17 @@ const renderImmediateDuration = (value?: Immediate): StdReader<string, "t" | "tm
|
||||
const renderPermanentDuration = (value: PermanentDuration): StdReader<string, "t" | "tm" | "rts"> =>
|
||||
translateR("Permanent").thenW(text => replaceTextIfNeeded(value.translations, text))
|
||||
|
||||
const renderFixedDuration = (value: FixedDuration): StdReader<string, "t" | "tm" | "rts"> =>
|
||||
formatCombinedTimeSpanR(value)
|
||||
/**
|
||||
* Returns the text for a duration that is based on an expression.
|
||||
*/
|
||||
export const renderExpressionBasedDuration = (
|
||||
value: ExpressionBasedDuration,
|
||||
): StdReader<string, "t" | "tm" | "rts"> =>
|
||||
renderExpressionBasedParameterValue(value.value)
|
||||
.thenW(expressionValue => formatTimeSpanR(value.unit, expressionValue))
|
||||
.then(text => wrapIfMaximum(value.is_maximum, text).map(wrapped => [wrapped, text] as const))
|
||||
.thenW(([wrapped, text]) => replaceTextIfNeeded(value.translations, wrapped, text))
|
||||
|
||||
/**
|
||||
* Renders a duration that is based on the result of the skill check.
|
||||
*/
|
||||
export const renderCheckResultBasedDuration = (
|
||||
value: CheckResultBasedDuration,
|
||||
): StdReader<string, "t" | "tm" | "rts"> =>
|
||||
renderCheckResultBasedValue(value)
|
||||
.thenW(text => formatTimeSpanR(value.unit, text))
|
||||
.then(text => wrapIfMaximum(value.is_maximum, text))
|
||||
|
||||
const renderIndefiniteDuration = (value: {
|
||||
maximum?: OneTimeDuration
|
||||
translations: LocaleMap<{
|
||||
@@ -100,11 +96,11 @@ type OneTimeDuration =
|
||||
}
|
||||
| {
|
||||
kind: "Fixed"
|
||||
Fixed: FixedDuration
|
||||
Fixed: Omit<ExpressionBasedDuration, "value"> & { value: number }
|
||||
}
|
||||
| {
|
||||
kind: "CheckResultBased"
|
||||
CheckResultBased: CheckResultBasedDuration
|
||||
kind: "Expression"
|
||||
Expression: ExpressionBasedDuration
|
||||
}
|
||||
| {
|
||||
kind: "Indefinite"
|
||||
@@ -133,9 +129,12 @@ export const renderOneTimeDuration = (
|
||||
case "Permanent":
|
||||
return renderPermanentDuration(value.Permanent)
|
||||
case "Fixed":
|
||||
return renderFixedDuration(value.Fixed)
|
||||
case "CheckResultBased":
|
||||
return renderCheckResultBasedDuration(value.CheckResultBased)
|
||||
return renderExpressionBasedDuration({
|
||||
...value.Fixed,
|
||||
value: Case("Value", Case("Constant", value.Fixed.value)),
|
||||
})
|
||||
case "Expression":
|
||||
return renderExpressionBasedDuration(value.Expression)
|
||||
case "Indefinite":
|
||||
return renderIndefiniteDuration(value.Indefinite)
|
||||
case "DuringLovemaking":
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Reader } from "@elyukai/utils/reader"
|
||||
import type {
|
||||
CheckResultBasedRange,
|
||||
FixedRange,
|
||||
ExpressionBasedRange,
|
||||
ModifiableRange,
|
||||
Range,
|
||||
RangeValue,
|
||||
@@ -9,6 +8,7 @@ import type {
|
||||
} from "@optolith/database-schema/gen"
|
||||
import { mapNullable } from "@optolith/helpers/nullable"
|
||||
import { assertExhaustive } from "@optolith/helpers/typeSafety"
|
||||
import { Case } from "../../../../helpers/enums.js"
|
||||
import {
|
||||
getInstanceByIdFnR,
|
||||
modifiableBySpeedOptionalR,
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
type StdReader,
|
||||
} from "../../reader.js"
|
||||
import { appendNoteIfNeeded, replaceTextIfNeeded } from "../../responsiveText.js"
|
||||
import { formatCombinedLengthR, formatLengthR } from "../../units/length.js"
|
||||
import { formatLengthR } from "../../units/length.js"
|
||||
import { MISSING_VALUE } from "../../unknown.js"
|
||||
import { renderCheckResultBasedValue } from "./checkResultBased.js"
|
||||
import { renderExpressionBasedParameterValue } from "./checkResultBased.js"
|
||||
import { wrapIfMaximum } from "./isMinimumMaximum.js"
|
||||
import { appendNonModifiableSuffix, ModifiableParameter } from "./nonModifiableSuffix.js"
|
||||
|
||||
@@ -65,14 +65,9 @@ const renderModifiableRange = (value: ModifiableRange) =>
|
||||
.then(text => wrapIfRadius(value.is_radius, text))
|
||||
.then(text => wrapIfMaximum(value.is_maximum, text))
|
||||
|
||||
const renderFixedRange = (value: FixedRange) =>
|
||||
formatCombinedLengthR(value)
|
||||
.then(text => wrapIfRadius(value.is_radius, text))
|
||||
.then(text => wrapIfMaximum(value.is_maximum, text))
|
||||
|
||||
const getCheckResultBasedRangeTranslation = (value: CheckResultBasedRange) =>
|
||||
renderCheckResultBasedValue(value)
|
||||
.thenW(text => formatLengthR(value.unit, text))
|
||||
const renderExpressionBasedRange = (value: ExpressionBasedRange) =>
|
||||
renderExpressionBasedParameterValue(value.value)
|
||||
.thenW(expressionValue => formatLengthR(value.unit, expressionValue))
|
||||
.then(text => wrapIfRadius(value.is_radius, text))
|
||||
.then(text => wrapIfMaximum(value.is_maximum, text))
|
||||
|
||||
@@ -95,11 +90,11 @@ export const renderNonModifiableRange = (
|
||||
}
|
||||
| {
|
||||
kind: "Fixed"
|
||||
Fixed: FixedRange
|
||||
Fixed: Omit<ExpressionBasedRange, "value"> & { value: number }
|
||||
}
|
||||
| {
|
||||
kind: "CheckResultBased"
|
||||
CheckResultBased: CheckResultBasedRange
|
||||
kind: "Expression"
|
||||
Expression: ExpressionBasedRange
|
||||
},
|
||||
shouldAppendNonModifiableSuffix: boolean,
|
||||
): StdReader<string, "t" | "tm" | "rts" | "nms"> => {
|
||||
@@ -116,13 +111,13 @@ export const renderNonModifiableRange = (
|
||||
return translateR("Global")
|
||||
case "Touch":
|
||||
return translateR("Touch").thenW(appendNonModifiableSuffixIfNeeded)
|
||||
case "Fixed": {
|
||||
return renderFixedRange(value.Fixed).thenW(appendNonModifiableSuffixIfNeeded)
|
||||
}
|
||||
case "CheckResultBased":
|
||||
return getCheckResultBasedRangeTranslation(value.CheckResultBased).then(
|
||||
appendNonModifiableSuffixIfNeeded,
|
||||
)
|
||||
case "Fixed":
|
||||
return renderExpressionBasedRange({
|
||||
...value.Fixed,
|
||||
value: Case("Value", Case("Constant", value.Fixed.value)),
|
||||
}).thenW(appendNonModifiableSuffixIfNeeded)
|
||||
case "Expression":
|
||||
return renderExpressionBasedRange(value.Expression).thenW(appendNonModifiableSuffixIfNeeded)
|
||||
default:
|
||||
return assertExhaustive(value)
|
||||
}
|
||||
@@ -142,8 +137,7 @@ export const renderRangeValue = (
|
||||
case "Self":
|
||||
case "Global":
|
||||
case "Touch":
|
||||
case "Fixed":
|
||||
case "CheckResultBased":
|
||||
case "Expression":
|
||||
return renderNonModifiableRange(value, shouldAppendNonModifiableSuffix)
|
||||
default:
|
||||
return assertExhaustive(value)
|
||||
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
renderNonModifiableOneTimeCost,
|
||||
} from "./partial/rated/activatable/cost.js"
|
||||
import {
|
||||
renderCheckResultBasedDuration,
|
||||
renderExpressionBasedDuration,
|
||||
renderMusicDuration,
|
||||
renderOneTimeDuration,
|
||||
renderSustainedDuration,
|
||||
@@ -1503,8 +1503,8 @@ const renderMagicalRuneCraftingTime = (craftingTime: MagicalRuneCraftingTime) =>
|
||||
)
|
||||
|
||||
const renderMagicalRuneDuration = (duration: MagicalRuneDuration) =>
|
||||
renderCheckResultBasedDuration(duration.fast).map2(
|
||||
renderCheckResultBasedDuration(duration.slow),
|
||||
renderExpressionBasedDuration(duration.fast).map2(
|
||||
renderExpressionBasedDuration(duration.slow),
|
||||
(fast, slow) => `${slow} / ${fast}`,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
import type { ExpressionBasedParameterValue } from "@optolith/database-schema/gen"
|
||||
import assert from "node:assert/strict"
|
||||
import { describe, it } from "node:test"
|
||||
import { renderCheckResultBasedValue } from "../../../../../src/entities/partial/rated/activatable/checkResultBased.js"
|
||||
import { renderExpressionBasedParameterValue } from "../../../../../src/entities/partial/rated/activatable/checkResultBased.js"
|
||||
import { Case } from "../../../../../src/helpers/enums.js"
|
||||
import { translateMock } from "../../../../helpers/translate.js"
|
||||
|
||||
describe("getTextForCheckResultBased", () => {
|
||||
it("should return the value text for a check-result-based parameter of an activatable skill", () => {
|
||||
assert.equal(
|
||||
renderCheckResultBasedValue({
|
||||
base: Case("QualityLevels"),
|
||||
}).run({ translate: translateMock }),
|
||||
renderExpressionBasedParameterValue(Case("Value", Case("QualityLevels"))).run({
|
||||
translate: translateMock,
|
||||
}),
|
||||
"QL",
|
||||
)
|
||||
assert.equal(
|
||||
renderCheckResultBasedValue({
|
||||
base: Case("SkillPoints"),
|
||||
}).run({ translate: translateMock }),
|
||||
renderExpressionBasedParameterValue(Case("Value", Case("SkillPoints"))).run({
|
||||
translate: translateMock,
|
||||
}),
|
||||
"SP",
|
||||
)
|
||||
assert.equal(
|
||||
renderCheckResultBasedValue({
|
||||
base: Case("QualityLevels"),
|
||||
modifier: { arithmetic: Case("Divide"), value: 2 },
|
||||
}).run({ translate: translateMock }),
|
||||
renderExpressionBasedParameterValue(
|
||||
Case("Division", [
|
||||
Case("Value", Case("QualityLevels")),
|
||||
Case("Value", Case("Constant", 2)),
|
||||
] as [ExpressionBasedParameterValue, ExpressionBasedParameterValue]),
|
||||
).run({ translate: translateMock }),
|
||||
"QL / 2",
|
||||
)
|
||||
assert.equal(
|
||||
renderCheckResultBasedValue({
|
||||
base: Case("SkillPoints"),
|
||||
modifier: { arithmetic: Case("Multiply"), value: 3 },
|
||||
}).run({ translate: translateMock }),
|
||||
renderExpressionBasedParameterValue(
|
||||
Case("Multiplication", [
|
||||
Case("Value", Case("SkillPoints")),
|
||||
Case("Value", Case("Constant", 3)),
|
||||
] as [ExpressionBasedParameterValue, ExpressionBasedParameterValue]),
|
||||
).run({ translate: translateMock }),
|
||||
"SP × 3",
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user