@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,181 @@
|
|
|
1
|
+
import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
import type { DamageFormulaLabUI } from '../ui';
|
|
4
|
+
import { bibliographyEntries } from '../bibliography';
|
|
5
|
+
|
|
6
|
+
const slug = 'oyun-hasar-formulu-hesaplayici-ttk';
|
|
7
|
+
const title = 'Oyun Hasar Formülü Laboratuvarı ve TTK Grafikleri';
|
|
8
|
+
const description = 'Güvenli matematiksel formüllerle oyun hasar kurallarını canlı grafikler, ısı haritaları, yuvarlama kuralları ve Time to Kill (TTK) ile karşılaştırın.';
|
|
9
|
+
|
|
10
|
+
const faq = [
|
|
11
|
+
{
|
|
12
|
+
question: 'Hasar formülü hesaplayıcısı neleri karşılaştırır?',
|
|
13
|
+
answer: 'Aynı savaş değerleri üzerinde iki güvenli matematiksel formülü çalıştırır. Hasar eğrilerini, vuruş eşiklerini, time to kill (TTK) süresini ve direnç sırasını JavaScript çalıştırmadan güvenle test edebilirsiniz.',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
question: 'Hangi değişkenleri ve fonksiyonları kullanabilirim?',
|
|
17
|
+
answer: 'Kullanılabilir değişkenler: attack, defense, level, power, resistance, flat, criticalChance ve criticalMultiplier. Güvenli fonksiyonlar: min, max, clamp, abs, sqrt, pow, floor, round ve ceil.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
question: 'Time to Kill (TTK) nasıl hesaplanır?',
|
|
21
|
+
answer: 'Gerekli vuruş sayısı, hedefin canının yuvarlanmış beklenen hasara bölünmesiyle (yukarı yuvarlama) bulunur. TTK ilk ve son vuruş arasındaki süreyi ölçer: (vuruş - 1) / saniyedeki saldırı sayısı.',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
question: 'Direnç sırası neden sonucu değiştirir?',
|
|
25
|
+
answer: 'Sabit çarpanı yüzde dirençten önce uygulamak o sabit değeri de azaltır. Direncin önce uygulanması ise sonraki sabit çarpanı etkisiz bırakır.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
question: 'Düzgün bir eğri oyunun dengeli olduğu anlamına gelir mi?',
|
|
29
|
+
answer: 'Hayır. Grafik eşik değerleri ve sıfır hasar bölgelerini gösterir, ancak denge oyunun bağlamına, rollere ve oyuncu testlerine bağlıdır.',
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const howTo = [
|
|
34
|
+
{ name: 'İki formül seçin', text: 'Hazır şablonlardan (doğrusal, oran, seviye ölçekli) başlayın veya iki özel formül yazın.' },
|
|
35
|
+
{ name: 'Savaş değerlerini ayarlayın', text: 'Saldırı, savunma, seviye, güç katsayısı, direnç, sabit çarpan, kritik değerleri, can ve saldırı hızını girin.' },
|
|
36
|
+
{ name: 'Motor kurallarını belirleyin', text: 'Hasarın nasıl yuvarlanacağını ve direnç sırasını seçin.' },
|
|
37
|
+
{ name: 'Grafik ve eşikleri inceleyin', text: 'Hasar eğrisini, savunma ısı haritasını, gerekli vuruş sayısını ve uyarıları karşılaştırın.' },
|
|
38
|
+
{ name: 'Deneysel verileri kaydedin', text: 'Paylaşım bağlantısını kopyalayın veya JSON, CSV ve PNG grafiğini indirin.' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const softwareApplication: WithContext<SoftwareApplication> = {
|
|
42
|
+
'@context': 'https://schema.org',
|
|
43
|
+
'@type': 'SoftwareApplication',
|
|
44
|
+
name: title,
|
|
45
|
+
applicationCategory: 'DeveloperApplication',
|
|
46
|
+
operatingSystem: 'Any',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const faqPage: WithContext<FAQPage> = {
|
|
50
|
+
'@context': 'https://schema.org',
|
|
51
|
+
'@type': 'FAQPage',
|
|
52
|
+
mainEntity: faq.map((item) => ({
|
|
53
|
+
'@type': 'Question',
|
|
54
|
+
name: item.question,
|
|
55
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
56
|
+
})),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const howToSchema: WithContext<HowTo> = {
|
|
60
|
+
'@context': 'https://schema.org',
|
|
61
|
+
'@type': 'HowTo',
|
|
62
|
+
name: title,
|
|
63
|
+
step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
67
|
+
slug,
|
|
68
|
+
title,
|
|
69
|
+
description,
|
|
70
|
+
ui: {
|
|
71
|
+
onboarding: 'Mevcut hasar kuralınızı girin, yanına bir alternatif ekleyin ve savaş değerlerini ayarlayın.',
|
|
72
|
+
localNote: 'Özel model. Formüller ve veriler bu tarayıcıda kalır.',
|
|
73
|
+
formulaDeck: 'Formül Bölümü',
|
|
74
|
+
formulaALabel: 'Formül A (Mevcut Model)',
|
|
75
|
+
formulaBLabel: 'Formül B (Alternatif)',
|
|
76
|
+
formulaHint: 'Değişkenler: attack, defense, level, power, resistance, flat, criticalChance, criticalMultiplier',
|
|
77
|
+
formulaFunctions: 'Güvenli fonksiyonlar: min, max, clamp, abs, sqrt, pow, floor, round, ceil',
|
|
78
|
+
presetLinear: 'Doğrusal Koruma',
|
|
79
|
+
presetRatio: 'Orantılı Zırh',
|
|
80
|
+
presetLevel: 'Seviye Ölçekleme',
|
|
81
|
+
combatInputs: 'Savaş Değerleri',
|
|
82
|
+
attackLabel: 'Saldırı',
|
|
83
|
+
defenseLabel: 'Savunma',
|
|
84
|
+
levelLabel: 'Seviye',
|
|
85
|
+
powerLabel: 'Güç Katsayısı',
|
|
86
|
+
resistanceLabel: 'Direnç (%)',
|
|
87
|
+
flatLabel: 'Sabit Çarpan',
|
|
88
|
+
criticalChanceLabel: 'Kritik Şansı (%)',
|
|
89
|
+
criticalMultiplierLabel: 'Kritik Çarpanı',
|
|
90
|
+
healthLabel: 'Hedef Canı',
|
|
91
|
+
cadenceLabel: 'Saniyedeki Saldırı Sayısı',
|
|
92
|
+
roundingLabel: 'Hasar Yuvarlama',
|
|
93
|
+
roundingNone: 'Ondalıkları Korur',
|
|
94
|
+
roundingFloor: 'Aşağı Yuvarla (Floor)',
|
|
95
|
+
roundingRound: 'En Yakın Tam Sayı',
|
|
96
|
+
roundingCeil: 'Yukarı Yuvarla (Ceil)',
|
|
97
|
+
orderLabel: 'Çarpan Sırası',
|
|
98
|
+
resistanceFirst: 'Önce Direnç Sonra Sabit',
|
|
99
|
+
flatFirst: 'Önce Sabit Sonra Direnç',
|
|
100
|
+
runLabel: 'Canlı Etki Karşılaştırması',
|
|
101
|
+
resultDamage: 'Beklenen Hasar',
|
|
102
|
+
resultHits: 'Yenmek İçin Vuruş',
|
|
103
|
+
resultTtk: 'Time to Kill (TTK)',
|
|
104
|
+
resultDifference: 'Hasar Farkı',
|
|
105
|
+
formulaAName: 'Mevcut',
|
|
106
|
+
formulaBName: 'Alternatif',
|
|
107
|
+
curveTitle: 'Saldırı Gelişim Eğrisi',
|
|
108
|
+
curveCaption: 'Savunma sabit tutulurken saldırı değeri yarıdan iki katına kadar taranır.',
|
|
109
|
+
heatmapTitle: 'Baskı Alanı Isı Haritası',
|
|
110
|
+
heatmapCaption: 'Formül A beklenen hasarını saldırı ve savunma kombinasyonlarında gösterir.',
|
|
111
|
+
attackAxis: 'Saldırı sağa doğru artar',
|
|
112
|
+
defenseAxis: 'Savunma aşağı doğru artar',
|
|
113
|
+
scenariosTitle: 'Savaş Profilleri',
|
|
114
|
+
scenarioSkirmisher: 'Avcı',
|
|
115
|
+
scenarioGuardian: 'Muhafız',
|
|
116
|
+
scenarioBoss: 'Patron (Boss)',
|
|
117
|
+
scenarioCustom: 'Mevcut Ayar',
|
|
118
|
+
diagnosticsTitle: 'Eşik ve Uyarı Kontrolü',
|
|
119
|
+
statusBalanced: 'Bu test aralığında olağandışı bir matematiksel sıçrama tespit edilmedi.',
|
|
120
|
+
exportTitle: 'Deneysel Verileri Aktar',
|
|
121
|
+
copyLink: 'Bağlantıyı Kopyala',
|
|
122
|
+
exportCsv: 'CSV İndir',
|
|
123
|
+
exportJson: 'JSON İndir',
|
|
124
|
+
importJson: 'JSON İçe Aktar',
|
|
125
|
+
exportPng: 'PNG Grafiği İndir',
|
|
126
|
+
reset: 'Modeli Sıfırla',
|
|
127
|
+
privacyDisclosure: 'Paylaşım bağlantısı ayarları URL içinde saklar ve sunucuya veri göndermez.',
|
|
128
|
+
limitationDisclosure: 'Beklenen kritik hasar bir ortalamadır, rastgele bir simülasyon değildir.',
|
|
129
|
+
importError: 'Seçilen dosya geçerli bir konfigürasyon değil.',
|
|
130
|
+
copiedStatus: 'Bağlantı panoya kopyalandı.',
|
|
131
|
+
},
|
|
132
|
+
seo: [
|
|
133
|
+
{
|
|
134
|
+
type: 'title',
|
|
135
|
+
level: 2,
|
|
136
|
+
text: 'Oyun motoruna entegre etmeden önce hasar formüllerini test edin',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: 'paragraph',
|
|
140
|
+
html: 'Bir hasar formülü varsayılan değerlerde dengeli görünebilir ancak yüksek seviyelerde bozulabilir. Bu laboratuvar olası eşikleri erkenden tespit etmenizi sağlar.',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'title',
|
|
144
|
+
level: 2,
|
|
145
|
+
text: 'Güvenli ve sınırlandırılmış matematiksel ifade dili',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: 'paragraph',
|
|
149
|
+
html: 'Girdi alanı yalnızca tanımlı değişkenleri ve güvenli fonksiyonları kabul eder, güvensiz kod çalıştırmaz.',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'table',
|
|
153
|
+
headers: ['Metrik', 'Hesaplama', 'Tasarım Sorusu'],
|
|
154
|
+
rows: [
|
|
155
|
+
['Beklanan Hasar', 'Kritik faktör ve direnç dahil temel formül', 'Kural zayıf ve güçlü birimlerde mantıklı çalışıyor mu?'],
|
|
156
|
+
['Yenmek İçin Vuruş', 'Hedef canının yuvarlanmış hasara bölünmesi', 'Fazladan 1 stat puanı bir vuruşu tamamen ortadan kaldırıyor mu?'],
|
|
157
|
+
['Time to Kill (TTK)', 'Vuruş aralığının saldırı hızına bölünmesi', 'Saldırı hızı istenen savaş temposunu oluşturuyor mu?'],
|
|
158
|
+
['Isı Haritası', 'Formül A nın saldırı ve savunma üzerindeki örneklemesi', 'Beklenmeyen eşikler veya ölü bölgeler var mı?'],
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
type: 'title',
|
|
163
|
+
level: 2,
|
|
164
|
+
text: 'Matematiksel verileri oyun tasarımı kararlarından ayırmak',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'paragraph',
|
|
168
|
+
html: 'Düzgün bir grafik oyunun eğlenceli olduğunu tek başına garanti etmez. Laboratuvarı test süreçlerinde doğrulanacak soruları bulmak için kullanın.',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: 'tip',
|
|
172
|
+
title: 'Hasar ve vuruş sayısını daima birlikte inceleyin',
|
|
173
|
+
html: 'Hasardaki küçük bir değişim bir can eşiğini geçerek vuruş sayısını 1 azaltabilir. Hasarı daima TTK ile karşılaştırın.',
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
faq,
|
|
177
|
+
bibliographyTitle: 'Oyunlarda hasar matematiği referansları',
|
|
178
|
+
bibliography: bibliographyEntries,
|
|
179
|
+
howTo,
|
|
180
|
+
schemas: [softwareApplication, faqPage, howToSchema],
|
|
181
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
import type { DamageFormulaLabUI } from '../ui';
|
|
4
|
+
import { bibliographyEntries } from '../bibliography';
|
|
5
|
+
|
|
6
|
+
const slug = 'game-damage-formula-calculator-ttk';
|
|
7
|
+
const title = '游戏伤害公式与 TTK 击杀时间实验室';
|
|
8
|
+
const description = '在安全环境下对比游戏伤害公式,通过实时曲线、攻防热力图、舍入断点、暴击收益及击杀所需时间 (TTK) 进行数值平衡分析。';
|
|
9
|
+
|
|
10
|
+
const faq = [
|
|
11
|
+
{
|
|
12
|
+
question: '伤害公式计算器主要对比哪些数据?',
|
|
13
|
+
answer: '它在同一套战斗变量下安全运行两条数学公式,帮助您对比伤害曲线、击杀所需攻击次数、击杀时间 (TTK)、舍入规则、减伤顺序及攻防热力图,全过程无需执行 JavaScript。',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
question: '支持哪些变量与数学函数?',
|
|
17
|
+
answer: '支持的变量有 attack, defense, level, power, resistance, flat, criticalChance, criticalMultiplier。安全的函数包括 min, max, clamp, abs, sqrt, pow, floor, round, ceil,同时支持加减乘除与括号。',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
question: '击杀时间 (TTK) 是如何计算的?',
|
|
21
|
+
answer: '击杀所需次数为目标血量除以舍入后的期望伤害(向上取整)。TTK 计算的是从首次攻击到致死攻击的时间间隔,即 (击杀次数 - 1) / 每秒攻击次数。若一击必杀则 TTK 为 0 秒。',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
question: '为什么减伤顺序会影响计算结果?',
|
|
25
|
+
answer: '先应用固定减伤再应用百分比抗性,会使固定减伤数值同样受到百分比衰减;而先应用百分比抗性,则后续的固定减伤保持原值。本工具支持两种计算顺序以匹配不同引擎的物理规则。',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
question: '平滑的伤害曲线是否意味着游戏数值绝对平衡?',
|
|
29
|
+
answer: '并非如此。曲线能揭示零伤害死区和陡峭断点,但平衡性取决于玩法目标、角色定位、玩家选择及实机测试。本工具仅提供数值行为分析,不作绝对判决。',
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const howTo = [
|
|
34
|
+
{ name: '选择两条公式', text: '从线性、比例或等级缩放预设开始,或自定义输入公式 A 与公式 B。' },
|
|
35
|
+
{ name: '设定战斗状态', text: '输入攻击力、防御力、等级、系数、抗性百分比、固定修正、暴击参数、目标血量及攻击频率。' },
|
|
36
|
+
{ name: '指定引擎规则', text: '选择伤害舍入方式,并指定百分比抗性与固定修正值的先后计算顺序。' },
|
|
37
|
+
{ name: '分析曲线与断点', text: '对比伤害推移曲线、防御热力图、击杀次数、TTK 及诊断警告。' },
|
|
38
|
+
{ name: '保存实验数据', text: '复制分享链接,或导出 JSON 配置文件、CSV 数据表及 PNG 曲线图。' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const softwareApplication: WithContext<SoftwareApplication> = {
|
|
42
|
+
'@context': 'https://schema.org',
|
|
43
|
+
'@type': 'SoftwareApplication',
|
|
44
|
+
name: title,
|
|
45
|
+
applicationCategory: 'DeveloperApplication',
|
|
46
|
+
operatingSystem: 'Any',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const faqPage: WithContext<FAQPage> = {
|
|
50
|
+
'@context': 'https://schema.org',
|
|
51
|
+
'@type': 'FAQPage',
|
|
52
|
+
mainEntity: faq.map((item) => ({
|
|
53
|
+
'@type': 'Question',
|
|
54
|
+
name: item.question,
|
|
55
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
56
|
+
})),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const howToSchema: WithContext<HowTo> = {
|
|
60
|
+
'@context': 'https://schema.org',
|
|
61
|
+
'@type': 'HowTo',
|
|
62
|
+
name: title,
|
|
63
|
+
step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
67
|
+
slug,
|
|
68
|
+
title,
|
|
69
|
+
description,
|
|
70
|
+
ui: {
|
|
71
|
+
onboarding: '输入您现有的伤害规则,在其旁放置对比公式,调整战斗状态以观察数值在极端情况下的表现。',
|
|
72
|
+
localNote: '私密战斗模型。公式与数据完全在本地浏览器内处理。',
|
|
73
|
+
formulaDeck: '公式实验室 Chamber',
|
|
74
|
+
formulaALabel: '公式 A (当前模型)',
|
|
75
|
+
formulaBLabel: '公式 B (对比模型)',
|
|
76
|
+
formulaHint: '支持变量: attack, defense, level, power, resistance, flat, criticalChance, criticalMultiplier',
|
|
77
|
+
formulaFunctions: '安全函数: min, max, clamp, abs, sqrt, pow, floor, round, ceil',
|
|
78
|
+
presetLinear: '线性减伤',
|
|
79
|
+
presetRatio: '比例护甲',
|
|
80
|
+
presetLevel: '等级缩放',
|
|
81
|
+
combatInputs: '战斗状态参数',
|
|
82
|
+
attackLabel: '攻击力 (Attack)',
|
|
83
|
+
defenseLabel: '防御力 (Defense)',
|
|
84
|
+
levelLabel: '等级 (Level)',
|
|
85
|
+
powerLabel: '技能威力系数',
|
|
86
|
+
resistanceLabel: '抗性百分比 (%)',
|
|
87
|
+
flatLabel: '固定加减修正值',
|
|
88
|
+
criticalChanceLabel: '暴击率 (%)',
|
|
89
|
+
criticalMultiplierLabel: '暴击伤害倍率',
|
|
90
|
+
healthLabel: '目标生命值 (Health)',
|
|
91
|
+
cadenceLabel: '每秒攻击次数 (APS)',
|
|
92
|
+
roundingLabel: '伤害舍入规则',
|
|
93
|
+
roundingNone: '保留小数',
|
|
94
|
+
roundingFloor: '向下取整 (Floor)',
|
|
95
|
+
roundingRound: '四舍五入 (Round)',
|
|
96
|
+
roundingCeil: '向上取整 (Ceil)',
|
|
97
|
+
orderLabel: '修正计算顺序',
|
|
98
|
+
resistanceFirst: '先计算抗性后计算固定值',
|
|
99
|
+
flatFirst: '先计算固定值后计算抗性',
|
|
100
|
+
runLabel: '实时影响对比',
|
|
101
|
+
resultDamage: '期望伤害',
|
|
102
|
+
resultHits: '击杀所需次数',
|
|
103
|
+
resultTtk: '击杀时间 (TTK)',
|
|
104
|
+
resultDifference: '伤害差值',
|
|
105
|
+
formulaAName: '当前公式',
|
|
106
|
+
formulaBName: '对比公式',
|
|
107
|
+
curveTitle: '攻击力推移曲线',
|
|
108
|
+
curveCaption: '在防御力固定的情况下,攻击力从当前值的一半扫掠至两倍。',
|
|
109
|
+
heatmapTitle: '压力场热力图',
|
|
110
|
+
heatmapCaption: '展示公式 A 在不同攻击力与防御力组合下的期望伤害。',
|
|
111
|
+
attackAxis: '攻击力向右递增',
|
|
112
|
+
defenseAxis: '防御力向下递增',
|
|
113
|
+
scenariosTitle: '战斗目标预设',
|
|
114
|
+
scenarioSkirmisher: '游侠 / 刺客',
|
|
115
|
+
scenarioGuardian: '重装战士',
|
|
116
|
+
scenarioBoss: '首领 Boss',
|
|
117
|
+
scenarioCustom: '当前自定义',
|
|
118
|
+
diagnosticsTitle: '断点与风险诊断',
|
|
119
|
+
statusBalanced: '在此扫掠范围内未检测到数值异常突变。',
|
|
120
|
+
exportTitle: '导出实验成果',
|
|
121
|
+
copyLink: '复制分享链接',
|
|
122
|
+
exportCsv: '下载 CSV 数据',
|
|
123
|
+
exportJson: '下载 JSON 配置',
|
|
124
|
+
importJson: '导入 JSON 配置',
|
|
125
|
+
exportPng: '下载 PNG 图形',
|
|
126
|
+
reset: '重置模型',
|
|
127
|
+
privacyDisclosure: '分享链接将配置保存在 URL 哈希中,不会发送至服务器。',
|
|
128
|
+
limitationDisclosure: '期望暴击伤害为统计平均值,非随机抽样模拟。',
|
|
129
|
+
importError: '所选文件不是有效的伤害实验室配置文件。',
|
|
130
|
+
copiedStatus: '分享链接已复制到剪贴板。',
|
|
131
|
+
},
|
|
132
|
+
seo: [
|
|
133
|
+
{
|
|
134
|
+
type: 'title',
|
|
135
|
+
level: 2,
|
|
136
|
+
text: '在代码实装前验证游戏伤害公式',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: 'paragraph',
|
|
140
|
+
html: '一条伤害公式可能在常规数值下表现良好,但在等级或装备极端增长时失效。本实验室能帮助您提前发现零伤害区与数值断层。',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'title',
|
|
144
|
+
level: 2,
|
|
145
|
+
text: '受限且安全的表达式解析',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: 'paragraph',
|
|
149
|
+
html: '公式输入框仅解析预设变量与安全数学函数,杜绝任意 JavaScript 代码执行风险。',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'table',
|
|
153
|
+
headers: ['评估指标', '计算逻辑', '设计思考'],
|
|
154
|
+
rows: [
|
|
155
|
+
['期望伤害', '包含暴击期待、抗性与舍入规则的基础伤害', '公式在强弱单位间是否均具备合理响应?'],
|
|
156
|
+
['击杀次数', '目标生命值除以舍入后的伤害', '增加 1 点属性是否会导致所需攻击次数减少一次?'],
|
|
157
|
+
['击杀时间 (TTK)', '攻击间隔除以每秒攻击频率', '攻击频率是否符合预期的战斗节奏?'],
|
|
158
|
+
['压力热力图', '公式 A 在攻防矩阵上的采样', '是否存在不合理的停滞区或陡峭断层?'],
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
type: 'title',
|
|
163
|
+
level: 2,
|
|
164
|
+
text: '区分算术客观事实与数值平衡主观判断',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'paragraph',
|
|
168
|
+
html: '平滑的曲线并不直接等同于出色的游戏体验。请将本实验室作为发现实测问题与试玩验证的辅助工具。',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: 'tip',
|
|
172
|
+
title: '同时观察伤害数值与击杀次数',
|
|
173
|
+
html: '伤害数值的微小变化可能会跨越血量阈值,从而使所需攻击次数减少一次。请务必结合 TTK 进行综合评估。',
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
faq,
|
|
177
|
+
bibliographyTitle: '游戏伤害数值设计参考资料',
|
|
178
|
+
bibliography: bibliographyEntries,
|
|
179
|
+
howTo,
|
|
180
|
+
schemas: [softwareApplication, faqPage, howToSchema],
|
|
181
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolDefinition } from "../../types";
|
|
2
|
+
import { damageFormulaLab } from "./entry";
|
|
3
|
+
|
|
4
|
+
export * from "./entry";
|
|
5
|
+
|
|
6
|
+
export const DAMAGE_FORMULA_LAB_TOOL: ToolDefinition = {
|
|
7
|
+
entry: damageFormulaLab,
|
|
8
|
+
Component: () => import("./component.astro"),
|
|
9
|
+
SEOComponent: () => import("./seo.astro"),
|
|
10
|
+
BibliographyComponent: () => import("./bibliography.astro"),
|
|
11
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildCurve,
|
|
4
|
+
buildHeatmap,
|
|
5
|
+
evaluateExpression,
|
|
6
|
+
evaluateFormula,
|
|
7
|
+
makeRange,
|
|
8
|
+
roundDamage,
|
|
9
|
+
} from "./logic";
|
|
10
|
+
import type { CombatSettings, FormulaVariables } from "./logic";
|
|
11
|
+
|
|
12
|
+
const variables: FormulaVariables = {
|
|
13
|
+
attack: 80,
|
|
14
|
+
defense: 30,
|
|
15
|
+
level: 12,
|
|
16
|
+
power: 1.5,
|
|
17
|
+
resistance: 20,
|
|
18
|
+
flat: 4,
|
|
19
|
+
criticalChance: 25,
|
|
20
|
+
criticalMultiplier: 2,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const settings: CombatSettings = {
|
|
24
|
+
health: 500,
|
|
25
|
+
attacksPerSecond: 2,
|
|
26
|
+
rounding: "round",
|
|
27
|
+
modifierOrder: "resistance-first",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe("damage formula parser", () => {
|
|
31
|
+
it("evaluates precedence, unary values, exponentiation and variables", () => {
|
|
32
|
+
expect(evaluateExpression("attack + defense * 2", variables)).toBe(140);
|
|
33
|
+
expect(evaluateExpression("-(2 + 3) ^ 2", variables)).toBe(-25);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("evaluates the documented safe functions", () => {
|
|
37
|
+
expect(evaluateExpression("min(9, max(2, 7)) + abs(-3)", variables)).toBe(
|
|
38
|
+
10,
|
|
39
|
+
);
|
|
40
|
+
expect(
|
|
41
|
+
evaluateExpression("floor(2.9) + ceil(2.1) + round(2.5)", variables),
|
|
42
|
+
).toBe(8);
|
|
43
|
+
expect(
|
|
44
|
+
evaluateExpression("sqrt(16) + pow(2, 3) + clamp(20, 0, 10)", variables),
|
|
45
|
+
).toBe(22);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("rejects empty, unknown and non-finite expressions", () => {
|
|
49
|
+
expect(() => evaluateExpression("", variables)).toThrow(
|
|
50
|
+
"Enter a damage formula",
|
|
51
|
+
);
|
|
52
|
+
expect(() => evaluateExpression("mystery + 1", variables)).toThrow(
|
|
53
|
+
"Unknown variable",
|
|
54
|
+
);
|
|
55
|
+
expect(() => evaluateExpression("attack / 0", variables)).toThrow(
|
|
56
|
+
"non-finite",
|
|
57
|
+
);
|
|
58
|
+
expect(() => evaluateExpression("attack; alert(1)", variables)).toThrow(
|
|
59
|
+
"Unsupported character",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("rejects malformed syntax and unsupported functions", () => {
|
|
64
|
+
expect(() => evaluateExpression("(attack + 1", variables)).toThrow(
|
|
65
|
+
"Expected )",
|
|
66
|
+
);
|
|
67
|
+
expect(() => evaluateExpression("noise(1)", variables)).toThrow(
|
|
68
|
+
"Unknown function",
|
|
69
|
+
);
|
|
70
|
+
expect(() => evaluateExpression("attack defense", variables)).toThrow(
|
|
71
|
+
"Unexpected token",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("combat modelling", () => {
|
|
77
|
+
it("calculates expected damage, hit count and time to kill", () => {
|
|
78
|
+
const result = evaluateFormula(
|
|
79
|
+
"attack * power - defense",
|
|
80
|
+
variables,
|
|
81
|
+
settings,
|
|
82
|
+
);
|
|
83
|
+
expect(result.baseDamage).toBe(90);
|
|
84
|
+
expect(result.expectedDamage).toBe(94);
|
|
85
|
+
expect(result.roundedDamage).toBe(94);
|
|
86
|
+
expect(result.hitsToKill).toBe(6);
|
|
87
|
+
expect(result.timeToKill).toBe(2.5);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("makes modifier order and rounding explicit", () => {
|
|
91
|
+
const flatFirst = evaluateFormula("100", variables, {
|
|
92
|
+
...settings,
|
|
93
|
+
modifierOrder: "flat-first",
|
|
94
|
+
rounding: "floor",
|
|
95
|
+
});
|
|
96
|
+
const resistanceFirst = evaluateFormula("100", variables, {
|
|
97
|
+
...settings,
|
|
98
|
+
modifierOrder: "resistance-first",
|
|
99
|
+
rounding: "ceil",
|
|
100
|
+
});
|
|
101
|
+
expect(flatFirst.roundedDamage).toBe(103);
|
|
102
|
+
expect(resistanceFirst.roundedDamage).toBe(104);
|
|
103
|
+
expect(roundDamage(2.4, "none")).toBe(2.4);
|
|
104
|
+
expect(roundDamage(2.4, "floor")).toBe(2);
|
|
105
|
+
expect(roundDamage(2.4, "round")).toBe(2);
|
|
106
|
+
expect(roundDamage(2.4, "ceil")).toBe(3);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("reports unreachable kills for zero damage or cadence", () => {
|
|
110
|
+
expect(evaluateFormula("-100", variables, settings).hitsToKill).toBe(
|
|
111
|
+
Number.POSITIVE_INFINITY,
|
|
112
|
+
);
|
|
113
|
+
expect(
|
|
114
|
+
evaluateFormula("100", variables, { ...settings, attacksPerSecond: 0 })
|
|
115
|
+
.timeToKill,
|
|
116
|
+
).toBe(Number.POSITIVE_INFINITY);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("builds bounded curves and heatmaps from the same evaluator", () => {
|
|
120
|
+
expect(makeRange({ min: 10, max: 20, steps: 1 })).toEqual([10, 20]);
|
|
121
|
+
expect(makeRange({ min: 10, max: 20, steps: 50 })).toHaveLength(25);
|
|
122
|
+
const curve = buildCurve("attack - defense", variables, settings, {
|
|
123
|
+
min: 40,
|
|
124
|
+
max: 60,
|
|
125
|
+
steps: 3,
|
|
126
|
+
});
|
|
127
|
+
const heatmap = buildHeatmap({
|
|
128
|
+
expression: "attack - defense",
|
|
129
|
+
variables,
|
|
130
|
+
settings,
|
|
131
|
+
attackRange: { min: 40, max: 60, steps: 3 },
|
|
132
|
+
defenseRange: { min: 10, max: 20, steps: 2 },
|
|
133
|
+
});
|
|
134
|
+
expect(curve.map((point) => point.attack)).toEqual([40, 50, 60]);
|
|
135
|
+
expect(heatmap).toHaveLength(2);
|
|
136
|
+
expect(heatmap[0]).toHaveLength(3);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { evaluateExpression } from "./expression";
|
|
2
|
+
|
|
3
|
+
export { evaluateExpression } from "./expression";
|
|
4
|
+
|
|
5
|
+
export type RoundingMode = "none" | "floor" | "round" | "ceil";
|
|
6
|
+
export type ModifierOrder = "resistance-first" | "flat-first";
|
|
7
|
+
|
|
8
|
+
export interface FormulaVariables {
|
|
9
|
+
attack: number;
|
|
10
|
+
defense: number;
|
|
11
|
+
level: number;
|
|
12
|
+
power: number;
|
|
13
|
+
resistance: number;
|
|
14
|
+
flat: number;
|
|
15
|
+
criticalChance: number;
|
|
16
|
+
criticalMultiplier: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CombatSettings {
|
|
20
|
+
health: number;
|
|
21
|
+
attacksPerSecond: number;
|
|
22
|
+
rounding: RoundingMode;
|
|
23
|
+
modifierOrder: ModifierOrder;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FormulaResult {
|
|
27
|
+
baseDamage: number;
|
|
28
|
+
expectedDamage: number;
|
|
29
|
+
roundedDamage: number;
|
|
30
|
+
hitsToKill: number;
|
|
31
|
+
timeToKill: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SamplePoint extends FormulaResult {
|
|
35
|
+
attack: number;
|
|
36
|
+
defense: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SweepRange {
|
|
40
|
+
min: number;
|
|
41
|
+
max: number;
|
|
42
|
+
steps: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface HeatmapRequest {
|
|
46
|
+
expression: string;
|
|
47
|
+
variables: FormulaVariables;
|
|
48
|
+
settings: CombatSettings;
|
|
49
|
+
attackRange: SweepRange;
|
|
50
|
+
defenseRange: SweepRange;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function applyModifiers(
|
|
54
|
+
value: number,
|
|
55
|
+
variables: FormulaVariables,
|
|
56
|
+
order: ModifierOrder,
|
|
57
|
+
): number {
|
|
58
|
+
const resistanceMultiplier =
|
|
59
|
+
1 - Math.min(Math.max(variables.resistance, 0), 100) / 100;
|
|
60
|
+
if (order === "flat-first")
|
|
61
|
+
return (value + variables.flat) * resistanceMultiplier;
|
|
62
|
+
return value * resistanceMultiplier + variables.flat;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function roundDamage(value: number, mode: RoundingMode): number {
|
|
66
|
+
if (mode === "floor") return Math.floor(value);
|
|
67
|
+
if (mode === "ceil") return Math.ceil(value);
|
|
68
|
+
if (mode === "round") return Math.round(value);
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function evaluateFormula(
|
|
73
|
+
expression: string,
|
|
74
|
+
variables: FormulaVariables,
|
|
75
|
+
settings: CombatSettings,
|
|
76
|
+
): FormulaResult {
|
|
77
|
+
const baseDamage = evaluateExpression(expression, variables);
|
|
78
|
+
const chance = Math.min(Math.max(variables.criticalChance, 0), 100) / 100;
|
|
79
|
+
const criticalFactor = 1 + chance * (variables.criticalMultiplier - 1);
|
|
80
|
+
const modified = applyModifiers(
|
|
81
|
+
baseDamage * criticalFactor,
|
|
82
|
+
variables,
|
|
83
|
+
settings.modifierOrder,
|
|
84
|
+
);
|
|
85
|
+
const expectedDamage = Math.max(0, modified);
|
|
86
|
+
const roundedDamage = Math.max(
|
|
87
|
+
0,
|
|
88
|
+
roundDamage(expectedDamage, settings.rounding),
|
|
89
|
+
);
|
|
90
|
+
const hitsToKill =
|
|
91
|
+
roundedDamage > 0
|
|
92
|
+
? Math.ceil(settings.health / roundedDamage)
|
|
93
|
+
: Number.POSITIVE_INFINITY;
|
|
94
|
+
const validCadence =
|
|
95
|
+
Number.isFinite(hitsToKill) && settings.attacksPerSecond > 0;
|
|
96
|
+
const timeToKill = validCadence
|
|
97
|
+
? Math.max(0, hitsToKill - 1) / settings.attacksPerSecond
|
|
98
|
+
: Number.POSITIVE_INFINITY;
|
|
99
|
+
return { baseDamage, expectedDamage, roundedDamage, hitsToKill, timeToKill };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function makeRange(range: SweepRange): number[] {
|
|
103
|
+
const steps = Math.min(Math.max(Math.round(range.steps), 2), 25);
|
|
104
|
+
const span = range.max - range.min;
|
|
105
|
+
return Array.from(
|
|
106
|
+
{ length: steps },
|
|
107
|
+
(_, index) => range.min + (span * index) / (steps - 1),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function buildCurve(
|
|
112
|
+
expression: string,
|
|
113
|
+
variables: FormulaVariables,
|
|
114
|
+
settings: CombatSettings,
|
|
115
|
+
range: SweepRange,
|
|
116
|
+
): SamplePoint[] {
|
|
117
|
+
return makeRange(range).map((attack) => ({
|
|
118
|
+
attack,
|
|
119
|
+
defense: variables.defense,
|
|
120
|
+
...evaluateFormula(expression, { ...variables, attack }, settings),
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildHeatmap(request: HeatmapRequest): SamplePoint[][] {
|
|
125
|
+
const { expression, variables, settings, attackRange, defenseRange } =
|
|
126
|
+
request;
|
|
127
|
+
return makeRange(defenseRange).map((defense) =>
|
|
128
|
+
makeRange(attackRange).map((attack) => ({
|
|
129
|
+
attack,
|
|
130
|
+
defense,
|
|
131
|
+
...evaluateFormula(
|
|
132
|
+
expression,
|
|
133
|
+
{ ...variables, attack, defense },
|
|
134
|
+
settings,
|
|
135
|
+
),
|
|
136
|
+
})),
|
|
137
|
+
);
|
|
138
|
+
}
|