@jjlmoya/utils-games-development 1.63.0 → 1.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/damageFormulaLab/bibliography.astro +24 -0
- package/src/tool/damageFormulaLab/bibliography.ts +10 -0
- package/src/tool/damageFormulaLab/component.astro +178 -0
- package/src/tool/damageFormulaLab/controller.ts +235 -0
- package/src/tool/damageFormulaLab/damage-formula-lab.css +644 -0
- package/src/tool/damageFormulaLab/dom-views.ts +193 -0
- package/src/tool/damageFormulaLab/entry.ts +28 -0
- package/src/tool/damageFormulaLab/evaluator.ts +84 -0
- package/src/tool/damageFormulaLab/expression.ts +191 -0
- package/src/tool/damageFormulaLab/i18n/de.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/en.ts +231 -0
- package/src/tool/damageFormulaLab/i18n/es.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/fr.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/id.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/it.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/ja.ts +182 -0
- package/src/tool/damageFormulaLab/i18n/ko.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/nl.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/pl.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/pt.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/ru.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/sv.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/tr.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/zh.ts +181 -0
- package/src/tool/damageFormulaLab/index.ts +11 -0
- package/src/tool/damageFormulaLab/logic.test.ts +138 -0
- package/src/tool/damageFormulaLab/logic.ts +138 -0
- package/src/tool/damageFormulaLab/model.ts +151 -0
- package/src/tool/damageFormulaLab/seo.astro +13 -0
- package/src/tool/damageFormulaLab/storage.ts +62 -0
- package/src/tool/damageFormulaLab/transfers.ts +89 -0
- package/src/tool/damageFormulaLab/ui.ts +62 -0
- package/src/tools.ts +2 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CombatSettings,
|
|
3
|
+
FormulaVariables,
|
|
4
|
+
ModifierOrder,
|
|
5
|
+
RoundingMode,
|
|
6
|
+
SweepRange,
|
|
7
|
+
} from "./logic";
|
|
8
|
+
|
|
9
|
+
export interface LabConfig {
|
|
10
|
+
version: 1;
|
|
11
|
+
formulaA: string;
|
|
12
|
+
formulaB: string;
|
|
13
|
+
variables: FormulaVariables;
|
|
14
|
+
settings: CombatSettings;
|
|
15
|
+
attackRange: SweepRange;
|
|
16
|
+
defenseRange: SweepRange;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const defaultConfig: LabConfig = {
|
|
20
|
+
version: 1,
|
|
21
|
+
formulaA: "max(1, attack * power - defense)",
|
|
22
|
+
formulaB: "max(1, attack * power * 100 / (100 + defense))",
|
|
23
|
+
variables: {
|
|
24
|
+
attack: 80,
|
|
25
|
+
defense: 35,
|
|
26
|
+
level: 20,
|
|
27
|
+
power: 1.4,
|
|
28
|
+
resistance: 15,
|
|
29
|
+
flat: 3,
|
|
30
|
+
criticalChance: 20,
|
|
31
|
+
criticalMultiplier: 1.75,
|
|
32
|
+
},
|
|
33
|
+
settings: {
|
|
34
|
+
health: 450,
|
|
35
|
+
attacksPerSecond: 1.5,
|
|
36
|
+
rounding: "round",
|
|
37
|
+
modifierOrder: "resistance-first",
|
|
38
|
+
},
|
|
39
|
+
attackRange: { min: 40, max: 160, steps: 17 },
|
|
40
|
+
defenseRange: { min: 10, max: 90, steps: 9 },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const formulaPresets = {
|
|
44
|
+
linear: "max(1, attack * power - defense)",
|
|
45
|
+
ratio: "max(1, attack * power * 100 / (100 + defense))",
|
|
46
|
+
level:
|
|
47
|
+
"max(1, ((2 * level + 10) / 250) * (attack / max(1, defense)) * power + 2)",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const scenarios: Record<
|
|
51
|
+
string,
|
|
52
|
+
Partial<FormulaVariables> & { health: number }
|
|
53
|
+
> = {
|
|
54
|
+
skirmisher: { defense: 20, resistance: 5, health: 220 },
|
|
55
|
+
guardian: { defense: 70, resistance: 25, health: 700 },
|
|
56
|
+
boss: { defense: 110, resistance: 35, health: 2200 },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function finite(value: unknown, fallback: number): number {
|
|
60
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function rounding(value: unknown): RoundingMode {
|
|
64
|
+
if (value === "none" || value === "floor" || value === "ceil") return value;
|
|
65
|
+
return "round";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function order(value: unknown): ModifierOrder {
|
|
69
|
+
return value === "flat-first" ? "flat-first" : "resistance-first";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function variables(value: unknown): FormulaVariables {
|
|
73
|
+
const source =
|
|
74
|
+
typeof value === "object" && value
|
|
75
|
+
? (value as Partial<FormulaVariables>)
|
|
76
|
+
: {};
|
|
77
|
+
const base = defaultConfig.variables;
|
|
78
|
+
return {
|
|
79
|
+
attack: finite(source.attack, base.attack),
|
|
80
|
+
defense: finite(source.defense, base.defense),
|
|
81
|
+
level: finite(source.level, base.level),
|
|
82
|
+
power: finite(source.power, base.power),
|
|
83
|
+
resistance: finite(source.resistance, base.resistance),
|
|
84
|
+
flat: finite(source.flat, base.flat),
|
|
85
|
+
criticalChance: finite(source.criticalChance, base.criticalChance),
|
|
86
|
+
criticalMultiplier: finite(
|
|
87
|
+
source.criticalMultiplier,
|
|
88
|
+
base.criticalMultiplier,
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function range(value: unknown, fallback: SweepRange): SweepRange {
|
|
94
|
+
const source =
|
|
95
|
+
typeof value === "object" && value ? (value as Partial<SweepRange>) : {};
|
|
96
|
+
return {
|
|
97
|
+
min: finite(source.min, fallback.min),
|
|
98
|
+
max: finite(source.max, fallback.max),
|
|
99
|
+
steps: finite(source.steps, fallback.steps),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function combatSettings(value: unknown): CombatSettings {
|
|
104
|
+
const source =
|
|
105
|
+
typeof value === "object" && value
|
|
106
|
+
? (value as Partial<CombatSettings>)
|
|
107
|
+
: {};
|
|
108
|
+
return {
|
|
109
|
+
health: finite(source.health, defaultConfig.settings.health),
|
|
110
|
+
attacksPerSecond: finite(
|
|
111
|
+
source.attacksPerSecond,
|
|
112
|
+
defaultConfig.settings.attacksPerSecond,
|
|
113
|
+
),
|
|
114
|
+
rounding: rounding(source.rounding),
|
|
115
|
+
modifierOrder: order(source.modifierOrder),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function parseConfig(value: unknown): LabConfig {
|
|
120
|
+
if (typeof value !== "object" || !value)
|
|
121
|
+
throw new Error("Invalid configuration");
|
|
122
|
+
const source = value as Partial<LabConfig>;
|
|
123
|
+
return {
|
|
124
|
+
version: 1,
|
|
125
|
+
formulaA:
|
|
126
|
+
typeof source.formulaA === "string"
|
|
127
|
+
? source.formulaA
|
|
128
|
+
: defaultConfig.formulaA,
|
|
129
|
+
formulaB:
|
|
130
|
+
typeof source.formulaB === "string"
|
|
131
|
+
? source.formulaB
|
|
132
|
+
: defaultConfig.formulaB,
|
|
133
|
+
variables: variables(source.variables),
|
|
134
|
+
settings: combatSettings(source.settings),
|
|
135
|
+
attackRange: range(source.attackRange, defaultConfig.attackRange),
|
|
136
|
+
defenseRange: range(source.defenseRange, defaultConfig.defenseRange),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function configFromHash(hash: string): LabConfig | undefined {
|
|
141
|
+
try {
|
|
142
|
+
if (!hash.startsWith("#damage=")) return undefined;
|
|
143
|
+
return parseConfig(JSON.parse(decodeURIComponent(hash.slice(8))));
|
|
144
|
+
} catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function configToHash(config: LabConfig): string {
|
|
150
|
+
return `#damage=${encodeURIComponent(JSON.stringify(config))}`;
|
|
151
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import type { SEOSection } from '../../types';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
locale?: string;
|
|
7
|
+
sections?: SEOSection[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en', sections = [] } = Astro.props;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<SEORenderer content={{ locale, sections }} />
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ModifierOrder, RoundingMode } from "./logic";
|
|
2
|
+
|
|
3
|
+
export interface LabPreferences {
|
|
4
|
+
formulaA: string;
|
|
5
|
+
formulaB: string;
|
|
6
|
+
rounding: RoundingMode;
|
|
7
|
+
modifierOrder: ModifierOrder;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY = "jjlmoya-damage-formula-lab";
|
|
11
|
+
|
|
12
|
+
export const defaultPreferences: LabPreferences = {
|
|
13
|
+
formulaA: "max(1, attack * power - defense)",
|
|
14
|
+
formulaB: "max(1, attack * power * 100 / (100 + defense))",
|
|
15
|
+
rounding: "round",
|
|
16
|
+
modifierOrder: "resistance-first",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function isRounding(value: unknown): value is RoundingMode {
|
|
20
|
+
return (
|
|
21
|
+
value === "none" ||
|
|
22
|
+
value === "floor" ||
|
|
23
|
+
value === "round" ||
|
|
24
|
+
value === "ceil"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isOrder(value: unknown): value is ModifierOrder {
|
|
29
|
+
return value === "resistance-first" || value === "flat-first";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function loadPreferences(): LabPreferences {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(
|
|
35
|
+
localStorage.getItem(STORAGE_KEY) ?? "",
|
|
36
|
+
) as Partial<LabPreferences>;
|
|
37
|
+
return {
|
|
38
|
+
formulaA:
|
|
39
|
+
typeof parsed.formulaA === "string"
|
|
40
|
+
? parsed.formulaA
|
|
41
|
+
: defaultPreferences.formulaA,
|
|
42
|
+
formulaB:
|
|
43
|
+
typeof parsed.formulaB === "string"
|
|
44
|
+
? parsed.formulaB
|
|
45
|
+
: defaultPreferences.formulaB,
|
|
46
|
+
rounding: isRounding(parsed.rounding)
|
|
47
|
+
? parsed.rounding
|
|
48
|
+
: defaultPreferences.rounding,
|
|
49
|
+
modifierOrder: isOrder(parsed.modifierOrder)
|
|
50
|
+
? parsed.modifierOrder
|
|
51
|
+
: defaultPreferences.modifierOrder,
|
|
52
|
+
};
|
|
53
|
+
} catch {
|
|
54
|
+
return { ...defaultPreferences };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function savePreferences(preferences: LabPreferences): void {
|
|
59
|
+
try {
|
|
60
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { buildCurve } from "./logic";
|
|
2
|
+
import { configToHash } from "./model";
|
|
3
|
+
import type { LabConfig } from "./model";
|
|
4
|
+
|
|
5
|
+
export function download(name: string, type: string, body: BlobPart): void {
|
|
6
|
+
const link = document.createElement("a");
|
|
7
|
+
link.href = URL.createObjectURL(new Blob([body], { type }));
|
|
8
|
+
link.download = name;
|
|
9
|
+
link.click();
|
|
10
|
+
URL.revokeObjectURL(link.href);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function curveRows(config: LabConfig): Array<Array<number | undefined>> {
|
|
14
|
+
const a = buildCurve(
|
|
15
|
+
config.formulaA,
|
|
16
|
+
config.variables,
|
|
17
|
+
config.settings,
|
|
18
|
+
config.attackRange,
|
|
19
|
+
);
|
|
20
|
+
const b = buildCurve(
|
|
21
|
+
config.formulaB,
|
|
22
|
+
config.variables,
|
|
23
|
+
config.settings,
|
|
24
|
+
config.attackRange,
|
|
25
|
+
);
|
|
26
|
+
return a.map((point, index) => [
|
|
27
|
+
point.attack,
|
|
28
|
+
point.defense,
|
|
29
|
+
point.roundedDamage,
|
|
30
|
+
point.hitsToKill,
|
|
31
|
+
point.timeToKill,
|
|
32
|
+
b[index]?.roundedDamage,
|
|
33
|
+
b[index]?.hitsToKill,
|
|
34
|
+
b[index]?.timeToKill,
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function exportCsv(config: LabConfig): void {
|
|
39
|
+
const header = [
|
|
40
|
+
"attack",
|
|
41
|
+
"defense",
|
|
42
|
+
"formula_a_damage",
|
|
43
|
+
"formula_a_hits",
|
|
44
|
+
"formula_a_ttk",
|
|
45
|
+
"formula_b_damage",
|
|
46
|
+
"formula_b_hits",
|
|
47
|
+
"formula_b_ttk",
|
|
48
|
+
];
|
|
49
|
+
const body = [header, ...curveRows(config)]
|
|
50
|
+
.map((row) => row.join(","))
|
|
51
|
+
.join("\n");
|
|
52
|
+
download("damage-formula-sweep.csv", "text/csv", body);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function exportJson(config: LabConfig): void {
|
|
56
|
+
download(
|
|
57
|
+
"damage-formula-lab.json",
|
|
58
|
+
"application/json",
|
|
59
|
+
JSON.stringify(config, null, 2),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function drawPng(image: HTMLImageElement): void {
|
|
64
|
+
const canvas = document.createElement("canvas");
|
|
65
|
+
canvas.width = 1200;
|
|
66
|
+
canvas.height = 700;
|
|
67
|
+
const context = canvas.getContext("2d");
|
|
68
|
+
if (!context) return;
|
|
69
|
+
context.fillStyle = "#f7f4ee";
|
|
70
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
71
|
+
context.drawImage(image, 70, 70, 1060, 560);
|
|
72
|
+
canvas.toBlob((blob) => {
|
|
73
|
+
if (blob) download("damage-formula-curve.png", "image/png", blob);
|
|
74
|
+
}, "image/png");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function exportChartPng(root: HTMLElement): void {
|
|
78
|
+
const svg = root.querySelector<SVGSVGElement>("[data-chart]");
|
|
79
|
+
if (!svg) return;
|
|
80
|
+
const source = new XMLSerializer().serializeToString(svg);
|
|
81
|
+
const image = new Image();
|
|
82
|
+
image.onload = () => drawPng(image);
|
|
83
|
+
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function copyShareLink(config: LabConfig): Promise<void> {
|
|
87
|
+
const url = `${location.origin}${location.pathname}${configToHash(config)}`;
|
|
88
|
+
await navigator.clipboard.writeText(url);
|
|
89
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface DamageFormulaLabUI extends Record<string, string> {
|
|
2
|
+
onboarding: string;
|
|
3
|
+
localNote: string;
|
|
4
|
+
formulaDeck: string;
|
|
5
|
+
formulaALabel: string;
|
|
6
|
+
formulaBLabel: string;
|
|
7
|
+
formulaHint: string;
|
|
8
|
+
formulaFunctions: string;
|
|
9
|
+
presetLinear: string;
|
|
10
|
+
presetRatio: string;
|
|
11
|
+
presetLevel: string;
|
|
12
|
+
combatInputs: string;
|
|
13
|
+
attackLabel: string;
|
|
14
|
+
defenseLabel: string;
|
|
15
|
+
levelLabel: string;
|
|
16
|
+
powerLabel: string;
|
|
17
|
+
resistanceLabel: string;
|
|
18
|
+
flatLabel: string;
|
|
19
|
+
criticalChanceLabel: string;
|
|
20
|
+
criticalMultiplierLabel: string;
|
|
21
|
+
healthLabel: string;
|
|
22
|
+
cadenceLabel: string;
|
|
23
|
+
roundingLabel: string;
|
|
24
|
+
roundingNone: string;
|
|
25
|
+
roundingFloor: string;
|
|
26
|
+
roundingRound: string;
|
|
27
|
+
roundingCeil: string;
|
|
28
|
+
orderLabel: string;
|
|
29
|
+
resistanceFirst: string;
|
|
30
|
+
flatFirst: string;
|
|
31
|
+
runLabel: string;
|
|
32
|
+
resultDamage: string;
|
|
33
|
+
resultHits: string;
|
|
34
|
+
resultTtk: string;
|
|
35
|
+
resultDifference: string;
|
|
36
|
+
formulaAName: string;
|
|
37
|
+
formulaBName: string;
|
|
38
|
+
curveTitle: string;
|
|
39
|
+
curveCaption: string;
|
|
40
|
+
heatmapTitle: string;
|
|
41
|
+
heatmapCaption: string;
|
|
42
|
+
attackAxis: string;
|
|
43
|
+
defenseAxis: string;
|
|
44
|
+
scenariosTitle: string;
|
|
45
|
+
scenarioSkirmisher: string;
|
|
46
|
+
scenarioGuardian: string;
|
|
47
|
+
scenarioBoss: string;
|
|
48
|
+
scenarioCustom: string;
|
|
49
|
+
diagnosticsTitle: string;
|
|
50
|
+
statusBalanced: string;
|
|
51
|
+
exportTitle: string;
|
|
52
|
+
copyLink: string;
|
|
53
|
+
exportCsv: string;
|
|
54
|
+
exportJson: string;
|
|
55
|
+
importJson: string;
|
|
56
|
+
exportPng: string;
|
|
57
|
+
reset: string;
|
|
58
|
+
privacyDisclosure: string;
|
|
59
|
+
limitationDisclosure: string;
|
|
60
|
+
importError: string;
|
|
61
|
+
copiedStatus: string;
|
|
62
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { RETRO_SFX_GENERATOR_TOOL } from './tool/retroSfxGenerator';
|
|
|
11
11
|
import { PIXEL_ART_PALETTE_SWAPPER_TOOL } from './tool/pixelArtPaletteSwapper';
|
|
12
12
|
import { GAME_UI_ACCESSIBILITY_TESTER_TOOL } from './tool/gameUIAccessibilityTester';
|
|
13
13
|
import { HITBOX_HURTBOX_ANIMATOR_TOOL } from './tool/hitboxHurtboxAnimator';
|
|
14
|
+
import { DAMAGE_FORMULA_LAB_TOOL } from './tool/damageFormulaLab';
|
|
14
15
|
|
|
15
16
|
export const ALL_TOOLS: ToolDefinition[] = [
|
|
16
17
|
STEAM_CAPSULE_GENERATOR_TOOL,
|
|
@@ -24,4 +25,5 @@ export const ALL_TOOLS: ToolDefinition[] = [
|
|
|
24
25
|
PIXEL_ART_PALETTE_SWAPPER_TOOL,
|
|
25
26
|
GAME_UI_ACCESSIBILITY_TESTER_TOOL,
|
|
26
27
|
HITBOX_HURTBOX_ANIMATOR_TOOL,
|
|
28
|
+
DAMAGE_FORMULA_LAB_TOOL,
|
|
27
29
|
];
|