RN for OpenHarmony英雄联盟助手App实战:伤害计算实现

案例开源地址:https://atomgit.com/nutpi/rn_openharmony_lol
在英雄联盟中,了解技能的伤害计算方式对于提升游戏水平很有帮助。一个技能的伤害通常由基础伤害加上属性加成组成,比如"100 + 60% AP"表示基础伤害 100,加上法术强度的 60%。
这篇文章我们来实现一个伤害计算器,让用户可以输入各项参数,实时计算出最终伤害。这是一个典型的表单输入 + 实时计算场景,我们会重点讨论受控组件、数值输入处理、以及实时计算的实现方式。
伤害计算的基本公式
在实现之前,先了解一下英雄联盟中技能伤害的计算方式:
总伤害 = 基础伤害 + (AP × AP加成比例) + (AD × AD加成比例)
举个例子,阿狸的 Q 技能"欺诈宝珠":
- 基础伤害:80/115/150/185/220(根据技能等级)
- AP 加成:70%
如果阿狸有 300 点法术强度,Q 技能 5 级时的伤害是:
220 + 300 × 0.7 = 220 + 210 = 430
当然实际游戏中还要考虑敌人的魔法抗性、穿透等因素,但基础的伤害计算就是这个公式。理解这个公式对于选择出装、评估技能强度都很有帮助。
状态设计与初始化
import React, {useState} from 'react';
import {View, Text, ScrollView, TextInput, StyleSheet} from 'react-native';
import {colors} from '../../styles/colors';
export function DamageCalcPage() {
const [baseDamage, setBaseDamage] = useState('100');
const [ap, setAp] = useState('0');
const [ad, setAd] = useState('0');
const [apRatio, setApRatio] = useState('0.6');
const [adRatio, setAdRatio] = useState('0');
这个组件定义了五个状态变量,分别对应伤害计算公式中的五个参数:
| 状态变量 | 含义 | 默认值 | 说明 |
|---|---|---|---|
| baseDamage | 基础伤害 | ‘100’ | 技能的固定伤害部分 |
| ap | 法术强度 | ‘0’ | 英雄的 AP 属性 |
| apRatio | AP 加成比例 | ‘0.6’ | 技能描述中的 AP 系数 |
| ad | 攻击力 | ‘0’ | 英雄的 AD 属性 |
| adRatio | AD 加成比例 | ‘0’ | 技能描述中的 AD 系数 |
为什么用字符串类型而不是数字类型?
这是一个很重要的设计决策。TextInput 组件的 value 属性必须是字符串,如果用数字类型存储,会遇到几个问题:
- 类型转换麻烦:显示时要
String(num),计算时要Number(str),来回转换容易出错 - 空值处理复杂:用户清空输入框时,空字符串转数字会变成 NaN 或 0,行为不可预测
- 格式丢失:用户输入 “0.60”,转成数字再转回来会变成 “0.6”
用字符串存储可以保留用户输入的原始格式,只在需要计算时才转换成数字。
默认值的选择逻辑:
默认值不是随便选的,而是经过考虑的:
- 基础伤害 100:一个常见的技能基础伤害值,让用户看到有意义的初始结果
- AP 加成 0.6:很多法师技能的 AP 加成在 50%-80% 之间,0.6 是一个典型值
- AD 和 AD 加成默认 0:法师技能通常没有 AD 加成,默认为 0 符合大多数场景
这些默认值让用户打开页面就能看到一个合理的计算结果(100 + 0 × 0.6 + 0 × 0 = 100),而不是全是 0 或者 NaN。
实时计算的实现
const totalDamage = parseFloat(baseDamage || '0')
+ parseFloat(ap || '0') * parseFloat(apRatio || '0')
+ parseFloat(ad || '0') * parseFloat(adRatio || '0');
这行代码是整个计算器的核心逻辑,它实现了伤害公式:基础伤害 + AP × AP比例 + AD × AD比例。
parseFloat 函数的行为:
parseFloat 是 JavaScript 内置函数,用于把字符串转换成浮点数。它的行为比 Number() 更宽容:
parseFloat('100') // → 100
parseFloat('100.5') // → 100.5
parseFloat('100abc') // → 100(忽略后面的非数字字符)
parseFloat('') // → NaN
parseFloat('abc') // → NaN
空值保护机制:
baseDamage || '0' 这个写法是空值保护。当用户清空输入框时,状态会变成空字符串 '',而 parseFloat('') 返回 NaN,会导致整个计算结果变成 NaN。
用 || 运算符可以在值为空字符串(falsy 值)时返回 ‘0’,确保计算不会出错:
'' || '0' // → '0'
'100' || '0' // → '100'
为什么不用 useMemo 优化?
有些开发者可能会想用 useMemo 来缓存计算结果:
const totalDamage = useMemo(() => {
return parseFloat(baseDamage || '0') + ...;
}, [baseDamage, ap, apRatio, ad, adRatio]);
但在这个场景下没有必要。原因是:
- 计算非常简单,只是几个数字的加减乘除,性能开销可以忽略
- 每次输入变化都需要重新计算,useMemo 的缓存几乎不会命中
- 增加 useMemo 反而会增加代码复杂度和内存开销
只有当计算逻辑复杂、或者依赖的数据量大时,才需要用 useMemo 优化。
页面结构与布局
return (
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
<Text style={styles.title}>伤害计算器</Text>
<Text style={styles.subtitle}>计算技能伤害</Text>
<View style={styles.inputGroup}>
<Text style={styles.label}>基础伤害</Text>
<TextInput
style={styles.input}
value={baseDamage}
onChangeText={setBaseDamage}
keyboardType="numeric"
placeholderTextColor={colors.textMuted}
/>
</View>
为什么用 ScrollView 包裹?
虽然当前内容不多,但用 ScrollView 包裹有几个好处:
- 小屏幕适配:在屏幕较小的设备上,内容可能超出一屏,需要滚动查看
- 键盘适配:键盘弹出时,ScrollView 可以自动调整,确保当前输入框可见
- 一致性:和其他页面保持相同的结构,便于维护
输入组的结构设计:
每个输入项由标签 + 输入框组成,用 inputGroup 容器包裹。这种结构清晰明了,用户一眼就能看出每个输入框的用途。
受控组件模式详解
<TextInput
style={styles.input}
value={baseDamage}
onChangeText={setBaseDamage}
keyboardType="numeric"
placeholderTextColor={colors.textMuted}
/>
这是 React 中典型的受控组件(Controlled Component)模式。
什么是受控组件?
受控组件是指表单元素的值完全由 React 状态控制。数据流是单向的:
用户输入 → onChangeText 触发 → setBaseDamage 更新状态 → 组件重新渲染 → 输入框显示新值
受控组件 vs 非受控组件:
| 特性 | 受控组件 | 非受控组件 |
|---|---|---|
| 值的存储位置 | React state | DOM 元素内部 |
| 获取值的方式 | 直接读取 state | 通过 ref 获取 |
| 实时验证 | ✓ 容易实现 | ✗ 需要额外处理 |
| 实时计算 | ✓ 自动触发 | ✗ 需要手动触发 |
在这个场景下,受控组件是最佳选择,因为我们需要实时计算——每次输入变化都要重新计算伤害值。
keyboardType=“numeric” 的作用:
这个属性告诉系统弹出数字键盘而不是全键盘:
- iOS:显示纯数字键盘
- Android:显示带小数点的数字键盘
这样用户输入数字更方便,也减少了输入错误的可能。
placeholderTextColor 的必要性:
在深色主题下,默认的占位符颜色可能看不清。显式设置 placeholderTextColor={colors.textMuted} 确保占位符在任何主题下都清晰可见。
完整的输入表单
<View style={styles.inputGroup}>
<Text style={styles.label}>法术强度 (AP)</Text>
<TextInput
style={styles.input}
value={ap}
onChangeText={setAp}
keyboardType="numeric"
placeholderTextColor={colors.textMuted}
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>AP 加成比例</Text>
<TextInput
style={styles.input}
value={apRatio}
onChangeText={setApRatio}
keyboardType="numeric"
placeholderTextColor={colors.textMuted}
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>攻击力 (AD)</Text>
<TextInput
style={styles.input}
value={ad}
onChangeText={setAd}
keyboardType="numeric"
placeholderTextColor={colors.textMuted}
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>AD 加成比例</Text>
<TextInput
style={styles.input}
value={adRatio}
onChangeText={setAdRatio}
keyboardType="numeric"
placeholderTextColor={colors.textMuted}
/>
</View>
输入项的排列顺序:
输入项按逻辑分组排列,而不是随意堆放:
- 基础伤害(独立项,最重要)
- AP 相关(法术强度、AP 加成比例)
- AD 相关(攻击力、AD 加成比例)
这种顺序符合用户的思维模式:先输入基础值,再输入各种加成。相关的输入项放在一起,用户不需要来回跳跃。
标签的命名规范:
标签使用中文名称 + 英文缩写的格式,比如"法术强度 (AP)"。这样做的好处是:
- 照顾新手:不熟悉英文缩写的用户也能理解
- 方便老手:熟悉游戏的用户能快速识别 AP、AD 等术语
- 节省空间:比纯中文"法术强度"或纯英文"Ability Power"都更简洁
结果展示区域
<View style={styles.resultCard}>
<Text style={styles.resultLabel}>总伤害</Text>
<Text style={styles.resultValue}>{totalDamage.toFixed(0)}</Text>
</View>
</ScrollView>
);
}
toFixed(0) 的作用:
toFixed(n) 方法把数字格式化为指定小数位数的字符串,会进行四舍五入:
123.456.toFixed(0) // → "123"
123.567.toFixed(0) // → "124"
123.456.toFixed(2) // → "123.46"
游戏中的伤害都是整数,所以用 toFixed(0) 去掉小数部分。
结果卡片的视觉设计:
结果卡片和输入区域有明显的视觉区分,让用户一眼就能找到计算结果:
| 设计元素 | 输入区域 | 结果卡片 |
|---|---|---|
| 内边距 | 12px | 24px |
| 边框宽度 | 1px | 2px |
| 边框颜色 | 普通边框色 | 金色 |
| 对齐方式 | 左对齐 | 居中 |
| 字号 | 16px | 48px |
这些差异让结果成为页面的视觉焦点,用户扫一眼就能看到计算结果。
样式设计详解
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
padding: 16
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: colors.textPrimary,
marginBottom: 4
},
subtitle: {
fontSize: 14,
color: colors.textSecondary,
marginBottom: 20
},
inputGroup: {
marginBottom: 16
},
label: {
fontSize: 14,
color: colors.textSecondary,
marginBottom: 6
},
input: {
backgroundColor: colors.backgroundCard,
borderRadius: 8,
padding: 12,
fontSize: 16,
color: colors.textPrimary,
borderWidth: 1,
borderColor: colors.border
},
resultCard: {
backgroundColor: colors.backgroundCard,
borderRadius: 12,
padding: 24,
alignItems: 'center',
marginTop: 20,
borderWidth: 2,
borderColor: colors.borderGold
},
resultLabel: {
fontSize: 16,
color: colors.textSecondary,
marginBottom: 8
},
resultValue: {
fontSize: 48,
fontWeight: 'bold',
color: colors.textGold
},
});
输入框样式的设计考量:
backgroundColor: colors.backgroundCard:和页面背景色有区分,让用户明确知道这是可输入区域borderRadius: 8:圆角让输入框更柔和,符合现代 UI 设计趋势padding: 12:足够的内边距,让文字不会贴着边缘,提升可读性fontSize: 16:适中的字号,在移动端方便阅读和输入borderWidth: 1:细边框增加层次感,但不会太抢眼
结果卡片样式的设计考量:
borderWidth: 2:比输入框更粗的边框,突出重要性borderColor: colors.borderGold:金色边框,和游戏风格一致,也暗示"这是重要信息"fontSize: 48:超大字号,让结果一目了然alignItems: 'center':居中对齐,让结果成为视觉中心
输入验证的实现思路
当前实现没有做输入验证,用户可以输入任何内容。在生产环境中,可能需要添加验证逻辑。
限制输入范围:
const handleApChange = (text: string) => {
// 允许空字符串(用户正在清空输入)
if (text === '') {
setAp(text);
return;
}
const num = parseFloat(text);
// 只接受 0-2000 之间的有效数字
if (!isNaN(num) && num >= 0 && num <= 2000) {
setAp(text);
}
// 超出范围的输入会被忽略,输入框保持原值
};
这个函数只允许输入 0-2000 之间的数字,超出范围的输入会被静默忽略。2000 是一个合理的上限,正常游戏中很难达到这么高的 AP。
格式化输入(只允许数字和小数点):
const handleRatioChange = (text: string) => {
// 只保留数字和小数点
const cleaned = text.replace(/[^0-9.]/g, '');
// 处理多个小数点的情况:只保留第一个
const parts = cleaned.split('.');
const formatted = parts.length > 2
? parts[0] + '.' + parts.slice(1).join('')
: cleaned;
setApRatio(formatted);
};
这个函数确保用户只能输入有效的数字格式,自动过滤掉字母和特殊字符。
显示错误提示:
const [error, setError] = useState('');
const handleApChange = (text: string) => {
setAp(text);
const num = parseFloat(text);
if (text !== '' && isNaN(num)) {
setError('请输入有效的数字');
} else if (num < 0) {
setError('数值不能为负');
} else if (num > 2000) {
setError('数值不能超过 2000');
} else {
setError('');
}
};
// 在 UI 中显示错误
{error !== '' && (
<Text style={styles.errorText}>{error}</Text>
)}
这种方式允许用户输入任何内容,但会显示错误提示。用户体验比静默忽略更好,因为用户知道发生了什么。
扩展:添加护甲/魔抗计算
实际伤害还要考虑敌人的防御属性。英雄联盟的减伤公式是:
减伤比例 = 护甲 / (100 + 护甲)
实际伤害 = 原始伤害 × (1 - 减伤比例)
比如敌人有 100 护甲:
减伤比例 = 100 / (100 + 100) = 50%
实际伤害 = 原始伤害 × 50%
实现代码:
const [armor, setArmor] = useState('0');
const [magicResist, setMagicResist] = useState('0');
const [damageType, setDamageType] = useState<'physical' | 'magic'>('magic');
// 计算减伤比例
const getReduction = (resist: number) => {
if (resist >= 0) {
return resist / (100 + resist);
} else {
// 负抗性会增加伤害(穿透过多时可能出现)
return resist / (100 - resist);
}
};
// 根据伤害类型选择对应的抗性
const resistance = damageType === 'physical'
? parseFloat(armor || '0')
: parseFloat(magicResist || '0');
const reduction = getReduction(resistance);
const actualDamage = totalDamage * (1 - reduction);
扩展:添加穿透计算
穿透可以减少敌人的有效护甲/魔抗。穿透分为固定穿透和百分比穿透,计算顺序是:先百分比,后固定。
const [flatPen, setFlatPen] = useState('0'); // 固定穿透
const [percentPen, setPercentPen] = useState('0'); // 百分比穿透
// 计算有效抗性
const calculateEffectiveResist = (baseResist: number) => {
const percentPenValue = parseFloat(percentPen || '0') / 100;
const flatPenValue = parseFloat(flatPen || '0');
// 先计算百分比穿透
const afterPercent = baseResist * (1 - percentPenValue);
// 再计算固定穿透,最低为 0
const afterFlat = Math.max(0, afterPercent - flatPenValue);
return afterFlat;
};
const effectiveResist = calculateEffectiveResist(resistance);
const actualDamage = totalDamage * (1 - getReduction(effectiveResist));
扩展:预设技能数据
可以添加一些常用技能的预设,让用户快速填充参数:
const skillPresets = [
{name: '阿狸 Q', baseDamage: '220', apRatio: '0.7', adRatio: '0'},
{name: '劫 Q', baseDamage: '230', apRatio: '0', adRatio: '1.0'},
{name: '拉克丝 R', baseDamage: '500', apRatio: '1.2', adRatio: '0'},
{name: '盖伦 E', baseDamage: '180', apRatio: '0', adRatio: '0.36'},
];
const applyPreset = (preset: typeof skillPresets[0]) => {
setBaseDamage(preset.baseDamage);
setApRatio(preset.apRatio);
setAdRatio(preset.adRatio);
};
用户点击预设按钮,就能一键填充对应技能的参数,省去手动输入的麻烦。
扩展:计算历史记录
保存用户的计算历史,方便对比不同配置的伤害:
interface HistoryRecord {
params: {
baseDamage: string;
ap: string;
apRatio: string;
ad: string;
adRatio: string;
};
result: number;
timestamp: number;
}
const [history, setHistory] = useState<HistoryRecord[]>([]);
const saveToHistory = () => {
const record: HistoryRecord = {
params: {baseDamage, ap, apRatio, ad, adRatio},
result: totalDamage,
timestamp: Date.now(),
};
// 新记录放在最前面,最多保留 10 条
setHistory(prev => [record, ...prev].slice(0, 10));
};
用户可以点击历史记录恢复之前的参数,方便对比不同出装方案的伤害差异。
键盘处理优化
在移动端,键盘弹出可能会遮挡输入框。可以用 KeyboardAvoidingView 处理:
import {KeyboardAvoidingView, Platform} from 'react-native';
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{flex: 1}}
>
<ScrollView>
{/* 内容 */}
</ScrollView>
</KeyboardAvoidingView>
KeyboardAvoidingView 会在键盘弹出时自动调整布局,确保当前输入框始终可见。behavior 属性在 iOS 和 Android 上需要不同的值才能获得最佳效果。
小结
伤害计算器展示了表单输入页面的典型实现模式:
- 受控组件:用 state 管理输入值,通过 value 和 onChangeText 保持同步
- 实时计算:每次渲染时根据当前状态计算结果,无需手动触发"计算"按钮
- 数值处理:用 parseFloat 转换字符串,用
|| '0'处理空值 - 视觉层次:输入区域和结果区域有明显的视觉区分,结果是页面焦点
这种模式可以复用到其他计算工具,比如经验计算器、金币效率计算器、冷却缩减计算器等。
下一篇我们来实现克制关系页面,展示英雄之间的克制信息。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)