12技能家族:继承和模板方法
Skill 抽象类、命中率和伤害公式、10 个技能各自做了什么
- 画出技能家族的继承树:ASkill 下面是 ATargetedSkill、ABoostSkill、CWait,再往下是 10 个具体技能
- 说清楚
Skill.use为什么是模板方法(Template Method):先付能量,再生效,顺序谁也改不了,所以打不中也照样扣能量 - 用截图里真实的数值算出命中率和伤害,比如 Nightwing 的 Lance 为什么打 Bubbore 正好是 22 点
- 说出每个具体技能类做了什么:大多数只是把名字、能量、威力或属性交给
super(...),另外 4 个多写了一个方法 - 解释为什么全游戏每种技能只有一个对象(CSkillCatalogue),大家共用它为什么不会出错
- 用英语回答 How would you add a new skill?,并和 Project 1 的 if/else 写法比一比
时间不够就先看:第 3 节(模板方法 use)、第 4 节(命中判定 hits)、第 8 节的第一个动画、第 9 节(怎么加一个新技能)和最后的自测。
上一章(第 11 章)走完了一个回合:CBattle 的 playTurn 走到第 81 行,执行 playerSkill.use(player, enemy, random),再把它返回的一行字加进日志。这一行后面到底发生了什么,就是这一章的内容。
这一章会一直用到几个第 1、2 章讲过的词(详细的讲解在 第 2 章第 7 节),先提醒一下:父类(superclass)和子类(subclass)——子类用 extends 继承父类,父类写好的字段和方法子类自动都有;抽象类(abstract class)——不能 new,只能当父类用;改写(override,有的书叫“重写”)——子类写一个名字、参数都一样的方法,换掉父类的做法。别的新词,用到的地方再解释。
1. 先看屏幕:技能在哪里出现
先看一张真实的战斗截图。你选了 Nightwing,敌人是 Bubbore。第一回合你用了 Pocket Sand,第二回合点了 Lance [4]。这一张是第二回合结束时的样子。屏幕上和技能有关的有 4 块:
1234- 14 个技能按钮
BattleScreen.skillButtons(每个是一个 CGameButton):Nightwing 有 4 个技能,就有 4 个按钮 - 2这一回合新加的 4 行日志。
Player's Nightwing used Lance!是 CBattle.playTurn 第 80 行写的;Hit Bubbore for 22 damage! They started bleeding!是技能对象的use(...)返回的,第 81 行把它加进日志。后两行是敌人 Bubbore 用了 Wait:敌人出手走的也是同一个use(第 88–89 行) - 3Bubbore 的 HP 条:195 → 151(第 5 节一步一步算)
- 4你的能量 19 / 27:Lance 花了 4 点,用之前是 23
这 4 块是三个地方做的。按钮是画面 CBattleScreen 造的;日志里 “Player's Nightwing used Lance!” 这一行是 CBattle 写的;但是技能到底做了什么——打中没有、扣多少血、加了什么效果、日志里那句结果——全是技能对象自己的代码算出来的。CBattle 只负责叫它干活,根本不知道它是哪一个技能。这一章就是把技能对象打开来看。
按钮上的字从哪来
CBattleScreen 造按钮的时候,问每个技能对象要两样东西:名字和能量消耗。代码和截图用同一套编号:
181 /* One button per skill, showing its energy cost, e.g. "Bite [1]". */182 private VBox createSkillButtons(List<Skill> skills) {183 VBox buttonColumn = new VBox(SKILL_GAP);184 buttonColumn.setLayoutX(ACTIONS_X);185 buttonColumn.setLayoutY(SKILLS_Y);186 187 Font font = UiStyle.loadGameFont(SKILL_FONT_SIZE);1881 for (Skill skill : skills) {1892 GameButton button = new GameButton(skill.getName() + " [" + skill.getEnergyCost()190 + "]", font, BUTTON_RADIUS);191 button.setPrefSize(SKILL_WIDTH, SKILL_HEIGHT);192 button.addPressEffect();1933 button.setOnAction(event -> useSkill(skill));194 skillButtons.add(button);195 buttonColumn.getChildren().add(button);196 }197 return buttonColumn;198 }
- 1循环 4 次:Nightwing 的技能列表里有 4 个技能
- 2按钮上的字 = 名字 + “ [” + 能量 + “]”:Pocket Sand [4]
- 3每个按钮记住自己的技能对象:点 Wait [-2] 就是用 Wait 那个对象
- 标注 1,第 188 行:
for (Skill skill : skills)是 for-each 循环,把 Nightwing 的技能一个一个拿出来,每个造一个按钮。skills的类型是List<Skill>:里面其实是 Lance、Aim、Pocket Sand、Wait 四个不同类的对象,但这里一律当成 ASkill 来用。 - 标注 2,第 189–190 行:按钮上的字是
skill.getName() + " [" + skill.getEnergyCost() + "]"。这两个方法都写在父类 ASkill 里,所以不管是哪个技能,都能这样问。 - 标注 3,第 193 行:
button.setOnAction(event -> useSkill(skill)):点这个按钮,就拿这个按钮自己的技能对象去调用useSkill。->是 Lambda 表达式(第 2 章第 8 节)。useSkill(第 117–120 行)只有两行:battle.playTurn(skill),然后update()刷新整个画面。
点下去以后:谁调用谁
下面是点 Lance [4] 之后的调用顺序。读法见 第 1 章第 3 节:每根竖虚线是一个对象,从上往下是时间,实线箭头 = 调用,虚线箭头 = 返回,绕回自己 = 调用自己的方法。
最关键的是第 4 步 use(...),也就是 CBattle 的第 81 行:log.add(playerSkill.use(player, enemy, random))。
playerSkill的类型写的是 ASkill。运行时它指着的是一个 CLance 对象,所以跑的是 Lance 这一家的代码。换成点 Wait,同一行代码就跑 Wait 的代码。这就是多态(polymorphism):CBattle 里没有任何 “如果是 Lance 就……” 的判断。use返回一个String,就是日志里缩进的那一行结果。技能不碰屏幕:它只改两个 CFighter,再把一句话交回去。屏幕由 CBattleScreen 的update()刷新。- 敌人出手是第 89 行:
enemySkill.use(enemy, player, random)。同一个方法,参数倒过来。谁用技能,谁就是第一个参数user,对手是第二个参数target。
2. 技能家谱:一棵四层的继承树
skill 包里有 15 个类。除了 CSkillCatalogue(第 7 节讲),另外 14 个连成一棵继承树。这张图是横着画的:最左边是根 ASkill,越往右越具体(14 个框竖着排放不下)。老师打印的 UML 是父类在上、子类在下,箭头的意思完全一样:
怎么读这张图:
- 每个框是一个类。空心三角箭头从子类指向父类,意思是 extends(继承)。比如 CLance → AAttackSkill → ATargetedSkill → ASkill:Lance 是一种 AttackSkill,也是一种 TargetedSkill,也是一种 Skill(is-a 关系)。所以 Lance 对象能放进
List<Skill>,也能交给playTurn(Skill playerSkill)。 - 圆圈里是 A 的 4 个是抽象类:ASkill、ATargetedSkill、AAttackSkill、ABoostSkill。它们不能 new,只是把一家人共同的代码写一次。
- 圆圈里是 C 的 10 个是具体类(concrete class),正好是游戏里的 10 个技能。它们都在树枝的最末端,没有子类。
为什么要分这么多层?因为同一家的技能有相同的地方,相同的代码只写一次。每往下一层,只写这一层和别人不一样的部分:
| 类 | 父类 | 这一层加了什么 | 留给子类写的 |
|---|---|---|---|
| ASkill | (根) | 名字、能量消耗,和固定顺序的 use() | applyEffect |
| ATargetedSkill | Skill | 命中判定 hits(),打不中的那句话 | applyHit |
| AAttackSkill | TargetedSkill | 威力 power,打中就算伤害 | (没有了) |
| ABoostSkill | Skill | 加哪个属性 stat,随机加 1–10 点 | (没有了) |
| CBite、CTackle | AttackSkill | 只有两个数字:能量、威力 | — |
| CLance | AttackSkill | 两个数字,加上“打中后流血” | — |
| CPocketSand、CStun | TargetedSkill | 打中后加致盲 / 眩晕 | — |
| CAim、CBunker、CNimble、CSwole | BoostSkill | 只选一个属性 EStat | — |
| CWait | Skill | 负的能量消耗,随机一句话 | — |
越往下要写的越少。10 个具体类里,6 个只有一个构造器(Bite、Tackle、Aim、Bunker、Nimble、Swole),另外 4 个也只多一个方法(Lance、Pocket Sand、Stun 多一个 applyHit,Wait 多一个 applyEffect)。最长的 CLance 和 CWait 也才 23 行。
老师打印的 UML 上是什么样
UML 图的完整读法在 第 3 章(第 2 节讲类名的斜体和圆圈字母,第 5–7 节讲名字前的小图标、下划线和箭头),这里只提醒图上的几个记号:


3. Skill:每个技能都一样的部分(模板方法 use)
先看根 ASkill。整个文件只有 37 行:
1package au.edu.unimelb.swen.oop.mokepon.skill;2 3import au.edu.unimelb.swen.oop.mokepon.battle.Fighter;4import java.util.Random;5 6/* Every skill has a name and an energy cost, but each kind of skill has its own effect.7 * To add a new skill, write a new subclass instead of changing the existing ones.8 */9public abstract class Skill {101 private final String name;11 private final int energyCost;12 13 /* A negative energy cost means the skill restores energy. */142 protected Skill(String name, int energyCost) {15 this.name = name;16 this.energyCost = energyCost;17 }18 19 public String getName() {20 return name;21 }22 23 public int getEnergyCost() {24 return energyCost;25 }26 27 /* Template Method: pay the energy cost first (even if an attack then misses),28 * then apply the effect. Returns the message for the battle log.29 */303 public final String use(Fighter user, Fighter target, Random random) {31 user.spendEnergy(energyCost);32 return applyEffect(user, target, random);33 }34 35 /* Each subclass decides what the skill actually does. */364 protected abstract String applyEffect(Fighter user, Fighter target, Random random);37}- 标注 1,第 10–11 行:两个字段。
name是名字(比如 "Lance"),energyCost是能量消耗(Lance 是 4)。它们是 private:只有 ASkill 自己能直接用,子类和别的类要通过getName()、getEnergyCost()(第 19–25 行)来读。它们也是 final:构造器里赋值一次,以后再也不能改。 - 标注 2,第 14 行:构造器是 protected 的。它是留给子类用的:子类在自己的构造器里写
super(name, energyCost),就会运行它(第 5 节画了这条链)。第 13 行的注释说:能量消耗是负数,就表示这个技能会回能量(Wait 是 −2)。 - 标注 3,第 30–33 行:
use,所有技能都用它出手。只有两行。第 31 行先让用技能的人付能量:user.spendEnergy(energyCost)。第 32 行再执行技能的效果applyEffect(...),并把效果返回的那句话原样交给 CBattle。方法前面写着final:任何子类都不能改写use。第三个参数Random random是 CBattle 传进来的:CBattle 第 19 行只有一个random字段,整场战斗的随机数都从它来,所以技能自己不 newRandom。(CBattle 第 30–31 行的构造器还能接收外面给的 Random,比如固定种子,结果就能重复;游戏里实际走的是第 25–26 行,自己new Random()。) - 标注 4,第 36 行:
applyEffect是抽象方法(abstract method)。只有方法头,后面直接是分号,没有{ }。ASkill 只规定“每个技能都有一个效果”,效果是什么,交给子类写。
模板方法:顺序写死在父类,内容交给子类
把 use 画出来是这样:
这就是模板方法模式(Template Method):父类用一个 final 方法把步骤的顺序定死(这里是 use:① 付能量 → ② 生效),其中一步留给子类写(这里是 applyEffect)。applyEffect 是子类必须写的抽象步骤,英文叫 primitive operation(abstract step);父类已经给了默认做法、子类想改才改的那种步骤,英文更常叫钩子方法(hook method),比如 AStatusEffect 的 getMissChance() 默认返回 0,只有 CBlinded 改写(第 13 章)。三家子类各写各的 applyEffect:
- ATargetedSkill:先判断打没打中,打中了才做事(第 4 节)。
- ABoostSkill:给自己的一项属性加 1–10 点(第 6 节)。
- CWait:什么都不做,随机说一句话(第 6 节)。
为什么这样设计?
- 付能量这件事,10 个技能完全一样。写在 ASkill 里一次,子类就不用管,也不会有哪个技能忘了扣能量。
final保证顺序永远是“先付能量,再做事”。没有哪个子类能改成“先看打没打中,打中了才扣能量”。- CBattle 只用到 ASkill 里写好的方法:
use(第 81、89 行)、getName(第 80、88 行),还有第 74 行player.canUse(playerSkill)里面读的getEnergyCost。它从来不看技能是哪个子类。所以以后加多少个技能,CBattle 都不用改(第 9 节)。
真实结果:打不中,能量也照扣
第 31 行先付能量,第 32 行才进 applyEffect;而“打没打中”是在 applyEffect 里面才判断的。所以能量在知道结果之前就已经付了。下面是 Owlet 对 Impike 那一仗,看敌人 Impike 出手。编号 1 = 第 31 行付能量,编号 2 = 第 32 行的效果:
27 /* Template Method: pay the energy cost first (even if an attack then misses),28 * then apply the effect. Returns the message for the battle log.29 */30 public final String use(Fighter user, Fighter target, Random random) {311 user.spendEnergy(energyCost);322 return applyEffect(user, target, random);33 }
- 1Impike 的能量条(蓝色)满的:22 / 22

- 1Stun 没中,第 31 行还是扣了 4:18 / 22

- 1Lance 也没中,又扣 4:14 / 22

- 2
Enemy Impike used Stun!But they missed.:第 32 行进了applyEffect,没打中,返回 But they missed.
敌人的条上不写数字,但能量可以按代码算出来。Impike 开打时能量 22(这一仗地形不加不减,就是数据文件里的值)。它用 Stun(能量 4):第 31 行先扣,22 − 4 = 18;然后在 applyEffect 里没打中,返回 But they missed.。18 ÷ 22 ≈ 82%,就是蓝条剩下的长度。下一回合它用 Lance(也是 4),又没打中,又扣 4:18 − 4 = 14,蓝条剩 64%。
这一仗 Impike 一共出手攻击 5 次(Stun、Lance、Stun、Lance、Lance),只有最后一次打中;最后一回合它还用了一次 Wait,回 2 点。最后它的能量是 22 − 5 × 4 + 2 = 4。每一次没打中,那 4 点都白花了。
4. TargetedSkill:先判断打没打中
Bite、Tackle、Lance、Pocket Sand、Stun 这 5 个技能是冲着对手去的。它们共同的父类 ATargetedSkill 写好了命中判定,5 个技能共用这一份:
1package au.edu.unimelb.swen.oop.mokepon.skill;2 3import au.edu.unimelb.swen.oop.mokepon.battle.Fighter;4import au.edu.unimelb.swen.oop.mokepon.model.Stat;5import java.util.Random;6 7/* A skill aimed at the other fighter. It only works if it passes the hit check,8 * so every targeted skill shares the same hit check and miss message.9 */10public abstract class TargetedSkill extends Skill {111 private static final String MISS_MESSAGE = "\tBut they missed.";12 13 protected TargetedSkill(String name, int energyCost) {14 super(name, energyCost);15 }16 17 @Override182 protected final String applyEffect(Fighter user, Fighter target, Random random) {19 if (!hits(user, target, random)) {20 return MISS_MESSAGE;21 }223 return applyHit(user, target, random);23 }24 25 /* What the skill does once it has hit the target. */264 protected abstract String applyHit(Fighter user, Fighter target, Random random);27 28 /* The normal hit check uses the user's accuracy and the target's dodge.29 * A blinded user can still miss after passing it.30 */31 private boolean hits(Fighter user, Fighter target, Random random) {325 if (random.nextDouble() >= calculateHitChance(user, target)) {33 return false;34 }356 return random.nextDouble() >= user.getMissChance();36 }37 38 private double calculateHitChance(Fighter user, Fighter target) {39 int accuracy = user.getStat(Stat.ACCURACY);40 int dodge = target.getStat(Stat.DODGE);41 double accuracySquared = accuracy * accuracy;42 double dodgeSquared = dodge * dodge;437 return accuracySquared / (accuracySquared + dodgeSquared);44 }45}- 标注 1,第 11 行:打不中时的那句话
"\tBut they missed."。private static final是常量:整个程序只有一份,永远不变。开头的\t是制表符(转义字符),日志里结果那一行往里缩,就是它造成的。 - 标注 2,第 18 行:
applyEffect在这里有了代码。第 17 行的@Override是注解,告诉编译器“我在改写父类的方法”。这个方法也是final:下面的子类(AttackSkill、Lance、Pocket Sand、Stun……)都不能再改它,谁也跳不过命中判定。第 19–20 行:!hits(...)读作“没打中”(!是“不”),那就直接返回第 11 行那句话,后面什么都不做。 - 标注 3,第 22 行:打中了,才调用
applyHit(...),把它返回的话交回去。 - 标注 4,第 26 行:
applyHit又是一个抽象方法。打中以后做什么,交给子类写。 - 标注 5,第 32 行:第一次随机判断。
random.nextDouble()给一个 0(含)到 1(不含)之间的随机小数(Random)。如果它大于等于命中率(代码里是>加=两个字符),就返回 false:没打中。所以通过这一次判断的机会正好等于命中率。 - 标注 6,第 35 行:第二次随机判断。再取一个随机小数,要大于等于
user.getMissChance()才算打中。没被致盲时getMissChance()是 0,任何小数都 ≥ 0,这一次一定通过;被致盲时是 0.5,只有一半机会通过。注意是 user(用技能的那个人)的落空几率。 - 标注 7,第 38–44 行:命中率。用技能的人的 Accuracy 的平方,除以(Accuracy 的平方 + 对手 Dodge 的平方)。
accuracy * accuracy是两个 int 相乘(最大 100 × 100 = 10000),存进double变量;所以第 43 行是小数除法,结果在 0 到 1 之间。
两次随机判断
为什么用平方?平方会把差距拉大。Accuracy 83 对 Dodge 55,直接比是 83 ÷ (83 + 55) ≈ 0.60;用平方是 6889 ÷ (6889 + 3025) ≈ 0.69。谁的数值高,谁更占便宜。Project 1 的 Skill.use 第 52 行用的也是这个公式(第 9 节有那段代码)。
用真实截图里的数字算一算
下面用的都是那一仗战斗中的数值(地形可能改过它们,见 第 9 章),不是选择界面上的原始数值:
| 那一仗 | 谁打谁 | A | D | 命中率 | 结果 |
|---|---|---|---|---|---|
| Nightwing 对 Bubbore | Nightwing → Bubbore | 83 | 55 | 6889 ÷ 9914 ≈ 0.69 | Pocket Sand、Lance 都中 |
| Impike 对 Galecrest | Impike → Galecrest | 100 | 40 | 10000 ÷ 11600 ≈ 0.86 | Stun、Lance 都中 |
| Impike 对 Galecrest | Galecrest → Impike | 43 | 100 | 1849 ÷ 11849 ≈ 0.16 | Tackle 没中 |
| Owlet 对 Impike | Impike → Owlet | 74 | 89 | 5476 ÷ 13397 ≈ 0.41 | 5 次只中 1 次 |
| Ironshell 对 Frostback | Frostback → Ironshell | 87 | 20 | 7569 ÷ 7969 ≈ 0.95 | 3 次中 2 次 |
最后一行最值得看:Frostback 的命中率有 95%,它的第二次 Pocket Sand 还是落空了。随机就是随机,95% 不等于一定。反过来,Galecrest 只有 16%,它的 Tackle 落空一点也不奇怪。
Owlet 那一仗的两张日志,正好一张是没打中、一张是打中(编号 1 = 第 20 行,编号 2 = 第 22 行):
17 @Override18 protected final String applyEffect(Fighter user, Fighter target, Random random) {19 if (!hits(user, target, random)) {201 return MISS_MESSAGE;21 }222 return applyHit(user, target, random);23 }
- 1第一回合的 Stun、第二回合的 Lance:都没中,走第 20 行

- 2第 5 次攻击:Lance 打中了,走第 22 行,
applyHit返回Hit Owlet for 45 damage! ...
标注 1 框住的两次(第一回合的 Stun、第二回合的 Lance)都没通过判断,走的是第 20 行,日志写 But they missed.。Impike 的命中率只有 0.41,大约 5 次中 2 次;这一仗运气差一点,前 4 次全没中,第 5 次 Lance 才中(标注 2),走的是第 22 行。
被致盲:第二次判断才起作用
Pocket Sand 打中以后,会给对手加一个 CBlinded 效果(第 5 节)。接下来的 3 回合里,对手自己出手的时候,它就是 user,user.getMissChance() 就是 0.5:
128 /* The extra chance that this fighter's targeted skills miss, for example when blinded.129 * If there are several effects, only the strongest one counts.130 */131 public double getMissChance() {132 double missChance = 0;133 for (StatusEffect effect : effects) {134 missChance = Math.max(missChance, effect.getMissChance());135 }136 return missChance;137 }一个 CFighter 身上可以同时有好几个效果。getMissChance 用 for-each 把每个效果的落空几率拿出来,Math.max 只留最大的那个(第 134 行)。身上什么效果都没有时,就是一开始的 0。父类 AStatusEffect 默认返回 0,只有 CBlinded 改写成 0.5(第 13 章)。
下面这一仗里,Frostback 把 Ironshell 致盲了。日志最近的几行:

- 1Frostback 的 Pocket Sand 打中:Ironshell 被致盲 3 回合
- 2Ironshell 被致盲以后用 Aim:照样成功,accuracy +6
- 3Frostback 第二次 Pocket Sand:没中(它有 95%,还是落空了)
- 4Ironshell 再用 Aim:又成功,+1
- 5Frostback 的 Lance 打中:26 点伤害(第 5 节的伤害表里有它)
这张图说明两件事:
- 致盲只管 TargetedSkill。Ironshell 被致盲以后用了两次 Aim(标注 2、4),两次都成功。Aim 属于 ABoostSkill,它的
applyEffect根本不调用hits,所以getMissChance对它没有一点影响。 - 没被致盲的一方,只看第一次判断。Frostback 自己没被致盲,第二次判断它一定通过;它的第二次 Pocket Sand 落空(标注 3),是第一次判断没通过——那 5% 正好被它碰上了。
如果被致盲的人用了 TargetedSkill 呢?按代码算一个:Nightwing 那一仗,Bubbore 第一回合就被 Pocket Sand 致盲了。它的 4 个技能里只有 Tackle 是 TargetedSkill。Tackle 打 Nightwing 的命中率是 73² ÷ (73² + 86²) = 5329 ÷ 12725 ≈ 0.42;第二次判断只有一半能通过,合起来 0.42 × 0.5 ≈ 0.21。不过那几回合 Bubbore 用的是 Swole、Wait、Bunker,都不是 TargetedSkill,这次致盲白挨了。
5. TargetedSkill 下面的五个具体技能:三种攻击、两种干扰
ATargetedSkill 下面有两种孩子:AAttackSkill 一家(打中了扣血:Bite、Tackle、Lance),和直接继承它的 CPocketSand、CStun(打中了不扣血,只给对手加一个状态效果)。
AttackSkill:打中了就算伤害
1package au.edu.unimelb.swen.oop.mokepon.skill;2 3import au.edu.unimelb.swen.oop.mokepon.battle.Fighter;4import java.util.Random;5 6/* A targeted skill that deals damage when it hits.7 * Attacks differ in their power, which multiplies the user's strength.8 */9public abstract class AttackSkill extends TargetedSkill {101 private final double power;11 122 protected AttackSkill(String name, int energyCost, double power) {13 super(name, energyCost);14 this.power = power;15 }16 17 @Override18 protected String applyHit(Fighter user, Fighter target, Random random) {193 int damage = user.calculateDamage(target, power);204 target.takeDamage(damage);215 return "\tHit " + target.getName() + " for " + damage + " damage!";22 }23}- 标注 1,第 10 行:攻击多了一个字段
power(威力),private final double。Bite 是 1,Tackle 是 2,Lance 是 0.5。 - 标注 2,第 12–15 行:构造器收三个值。前两个用
super(name, energyCost)交给父类,第三个自己存进this.power。 - 标注 3,第 19 行:伤害不是技能自己算的,而是请用技能的人算:
user.calculateDamage(target, power)。因为要用到 user 的力量、target 的防御,还要看 user 身上有没有被眩晕——这些数据都在 CFighter 里。 - 标注 4,第 20 行:
target.takeDamage(damage):对手扣血。 - 标注 5,第 21 行:拼出日志里那句话,比如
"\tHit Bubbore for 22 damage!"。
这里的 applyHit 没有写 final,所以子类还能改写它(Lance 就改写了)。AAttackSkill 已经一个抽象方法都没有了,但它仍然写着 abstract:不让别人直接 new 一个“没名字、没数字”的攻击,只能 new Bite、Tackle、Lance。
伤害公式:Fighter.calculateDamage
下面是 CFighter 里和伤害有关的几段:第 19 行的常量、第 118–121 行的公式、第 168–174 行的倍率。编号和下面的算式卡片一样:
19 private static final int DAMAGE_SCALE = 50;⋯ 省略第 20–113 行114 /* Work out the damage this fighter does to the target with the Project 1 formula.115 * The power multiplies the user's strength: Tackle has power 2, while Lance and116 * bleeding use half strength (power 0.5). The damage is truncated to an integer.117 */118 public int calculateDamage(Fighter target, double power) {1191 double damage = power * getStat(Stat.STRENGTH) * DAMAGE_SCALE1202 / target.getStat(Stat.DEFENCE);1213 return (int) (damage * getDamageMultiplier());122 }⋯ 省略第 123–164 行165 /* Effects such as stun can lower this fighter's damage.166 * If there are several effects, only the strongest one counts.167 */168 private double getDamageMultiplier() {169 double multiplier = 1;170 for (StatusEffect effect : effects) {171 multiplier = Math.min(multiplier, effect.getDamageMultiplier());172 }173 return multiplier;174 }Nightwing 的 Lance 打 Bubbore:一步一步算
1 第 119 行:0.5(power:Lance 交给 AttackSkill 的常量 HALF_STRENGTH)× 68(出手的 Nightwing 的 Strength)× 50(第 19 行的常量 DAMAGE_SCALE)= 1700
2 第 120 行:÷ 74(挨打的 Bubbore 的 Defence)= 22.97…
3 第 121 行:× 1(getDamageMultiplier():Nightwing 身上没有眩晕,第 169 行的 1 没被改小)= 22.97…;(int) 把小数直接去掉(不是四舍五入)→ 22
公式里来自两只 Mokepon 的只有两个数:出手的 Nightwing 的力量 68(这一仗地形不加不减,就是数据文件里的值),和挨打的 Bubbore 的防御 74(一直没变)。Bubbore 自己的力量这时碰巧也是 68(第一回合 Swole 把它从 66 加到 68),但它和这一下伤害无关。
第 119–120 行先用 double 算出带小数的伤害,第 121 行乘上倍率,再用 (int) 强制转换成整数。来对一下截图:

- 4日志写的就是 22
- 5Bubbore 的 HP 条(绿色)剩 77%:151 / 195
日志的 22 和算出来的一样(标注 4)。血条为什么是 151 不是 195 − 22 = 173?Lance 打中以后还给 Bubbore 加了流血(下面讲 Lance)。这个回合最后,流血用同一个公式又扣了一次:attacker.calculateDamage(owner, 0.5),攻击者还是 Nightwing,防御还是 74,又是 22。所以 195 − 22 − 22 = 151,151 ÷ 195 ≈ 77%,就是标注 5 绿条的长度。流血扣的血不写进日志(第 13 章)。
截图里一共出现过 4 次 Lance 打中,每一次都能这样算对(power 都是 0.5):
| 那一仗 | 谁打谁 | 力量 | 防御 | 0.5 × 力量 × 50 ÷ 防御 | 日志 |
|---|---|---|---|---|---|
| Nightwing 对 Bubbore | Nightwing → Bubbore | 68 | 74 | 1700 ÷ 74 = 22.97 | 22 |
| Impike 对 Galecrest | Impike → Galecrest | 100 | 30 | 2500 ÷ 30 = 83.33 | 83 |
| Owlet 对 Impike | Impike → Owlet | 76 | 42 | 1900 ÷ 42 = 45.24 | 45 |
| Ironshell 对 Frostback | Frostback → Ironshell | 100 | 96 | 2500 ÷ 96 = 26.04 | 26 |
几个数值的来历:Impike 在森林里是 76 × 1.5 = 114,超过上限被压成 100;Galecrest 的防御 59 × 0.5 = 29.5,四舍五入成 30;Frostback 72 × 1.5 = 108,也压成 100(地形的规则见 第 9 章)。Owlet 的防御本来是 36,它用 Bunker 加了 6 点,变成 42。要是没用 Bunker,这一下就是 1900 ÷ 36 = 52.78 → 52,Bunker 每一下替它挡了 7 点。
Bite 和 Tackle:只有两个数字不一样
1package au.edu.unimelb.swen.oop.mokepon.skill;2 3/* A cheap attack with normal damage. */4public class Bite extends AttackSkill {51 private static final int ENERGY_COST = 1;62 private static final double POWER = 1;7 8 public Bite() {93 super("Bite", ENERGY_COST, POWER);10 }11}1package au.edu.unimelb.swen.oop.mokepon.skill;2 3/* An expensive attack with double damage. */4public class Tackle extends AttackSkill {51 private static final int ENERGY_COST = 5;62 private static final double POWER = 2;7 8 public Tackle() {93 super("Tackle", ENERGY_COST, POWER);10 }11}两个文件一模一样,只有标注 1、2 的数字不一样:
ENERGY_COST、POWER 是 private static final 常量:给数字起个名字,比直接写 super("Bite", 1, 1) 好懂。标注 3 的 super(...) 把名字和两个数字交给父类。new Bite() 的时候,四个构造器一层一层往上调用。下图和 UML 一样,父类画在上面:
做完以后,这个 Bite 对象身上有三个值:name = "Bite"、energyCost = 1(存在 ASkill 那一层)、power = 1(存在 AAttackSkill 那一层)。方法也是一层层继承来的:use 来自 Skill,applyEffect 来自 TargetedSkill,applyHit 来自 AttackSkill。Bite 自己一个方法都没写。
能量够不够:按钮为什么会变灰
能量消耗还决定按钮能不能点。每次画面刷新,CBattleScreen 的 updateSkillButtons 都会把每个技能按钮问一遍,问的就是 CFighter 的 canUse。编号在代码和截图上一样:
76 /* Check whether there is enough energy to use the skill. */77 public boolean canUse(Skill skill) {781 return getEnergy() >= skill.getEnergyCost();79 }147 /* A skill can only be used while the battle is running and there is enough energy. */148 private void updateSkillButtons() {149 List<Skill> skills = battle.getPlayer().getSkills();150 for (int i = 0; i < skillButtons.size(); i++) {1512 boolean usable = !battle.isFinished() && battle.getPlayer().canUse(skills.get(i));1523 skillButtons.get(i).setUsable(usable);153 }154 }
- 1第 78 行的
getEnergy():Shellurk 只剩 3 点 - 2i = 1 是 Aim [3]:3 ≥ 3 成立,第 151 行算出 usable = true,还能点
- 3i = 0 是 Tackle [5]:3 ≥ 5 不成立,usable = false;第 152 行
setUsable(false)让它变灰、点不了
- 标注 1,第 78 行:
canUse只比一下:现在的能量大于等于技能的getEnergyCost()吗?返回 true 或 false。 - 标注 2,第 151 行:
!battle.isFinished()是“战斗还没结束”;&&是“并且”,两边都是 true,结果才是 true。所以战斗一结束,所有技能按钮都灰;没结束的时候,就看canUse。 - 标注 3,第 152 行:
setUsable(usable)把结果交给按钮,CGameButton 自己负责变灰(第 14 章)。按钮的顺序和技能列表的顺序一样,所以用同一个下标i。
Lance:先当普通攻击,再加流血
1package au.edu.unimelb.swen.oop.mokepon.skill;2 3import au.edu.unimelb.swen.oop.mokepon.battle.Fighter;4import au.edu.unimelb.swen.oop.mokepon.effect.Bleeding;5import java.util.Random;6 7/* An attack that only uses half of the user's strength, but makes the target bleed. */8public class Lance extends AttackSkill {9 private static final int ENERGY_COST = 4;10 private static final double HALF_STRENGTH = 0.5;11 12 public Lance() {131 super("Lance", ENERGY_COST, HALF_STRENGTH);14 }15 16 /* Deal the normal attack damage first, then start the bleeding. */17 @Override18 protected String applyHit(Fighter user, Fighter target, Random random) {192 String hitMessage = super.applyHit(user, target, random);203 target.addEffect(new Bleeding(user));214 return hitMessage + " They started bleeding!";22 }23}
- 2
Hit Owlet for 45 damage!:super.applyHit返回的部分 - 3流血两回合以后:
The bleeding stopped.,Owlet 倒下 - 4
They started bleeding!:Lance 自己接上的半句
- 标注 1,第 13 行:Lance 的威力是
HALF_STRENGTH= 0.5,只用一半力气。能量 4。 - 标注 2,第 19 行:
super.applyHit(user, target, random):先运行父类 AAttackSkill 的applyHit,也就是普通攻击那一套:算伤害、扣血、拼出 “Hit Owlet for 45 damage!”,存进变量hitMessage。 - 标注 3,第 20 行:
target.addEffect(new Bleeding(user)):新造一个 CBleeding 对象,放进对手的效果列表。传进去的user让流血记住是谁打的:以后每回合结束,流血都用攻击者的力量来算(这就是上面 151 的来历)。 - 标注 4,第 21 行:在父类那句话后面接上 “ They started bleeding!”,一起返回。
截图里是 Impike 打 Owlet:0.5 × 76 × 50 ÷ 42 = 45.24 → 45。Owlet 的 HP 从 120 变成 75;这个回合结束,流血再扣 45,剩 30;下一回合结束,流血又扣 45,30 − 45 不会变成负数(takeDamage 里有 Math.max(0, …)),就是 0。日志写 “The bleeding stopped.” 和 “Player's Owlet fainted!”(标注 3 的两行)。
Pocket Sand 和 Stun:打中了只加效果
这两个直接继承 ATargetedSkill,不是 AAttackSkill:所以它们没有威力,也不扣血;但它们照样要过命中判定。
1package au.edu.unimelb.swen.oop.mokepon.skill;2 3import au.edu.unimelb.swen.oop.mokepon.battle.Fighter;4import au.edu.unimelb.swen.oop.mokepon.effect.Blinded;5import java.util.Random;6 7/* Deals no damage, but blinds the target so its targeted skills may miss. */8public class PocketSand extends TargetedSkill {9 private static final int ENERGY_COST = 4;10 11 public PocketSand() {12 super("Pocket Sand", ENERGY_COST);13 }14 15 @Override16 protected String applyHit(Fighter user, Fighter target, Random random) {171 target.addEffect(new Blinded());182 return "\tPocket sand blinded " + target.getName() + "!\n"19 + "\tTheir targeted skills may miss for " + Blinded.TURNS + " turns.";20 }21}
- 2第 18–19 行拼出的两行:
Pocket sand blinded Bubbore!和Their targeted skills may miss for 3 turns.,3 来自Blinded.TURNS
- 标注 1,第 17 行:
target.addEffect(new Blinded()):给对手新造一个 CBlinded。从此对手出手 TargetedSkill 时,第二次判断只有一半机会通过(第 4 节)。屏幕上看不到这个效果,只有日志里这两行。 - 标注 2,第 18–19 行:一句话里有
\n(换行),所以日志里占两行。Blinded.TURNS是 CBlinded 里的public static final常量 3。用常量而不是直接写 3,以后改了持续回合数,这句话会自动跟着变。
1package au.edu.unimelb.swen.oop.mokepon.skill;2 3import au.edu.unimelb.swen.oop.mokepon.battle.Fighter;4import au.edu.unimelb.swen.oop.mokepon.effect.Stunned;5import java.util.Random;6 7/* Deals no damage, but halves the target's damage for a few turns. */8public class Stun extends TargetedSkill {9 private static final int ENERGY_COST = 4;10 11 public Stun() {12 super("Stun", ENERGY_COST);13 }14 15 @Override16 protected String applyHit(Fighter user, Fighter target, Random random) {171 target.addEffect(new Stunned());182 return "\t" + target.getName() + " was stunned! Their damage is halved for "19 + Stunned.TURNS + " turns.";20 }21}
- 2
Galecrest was stunned! Their damage is halved for 3 turns.:第 18–19 行拼的一句 - 3顺便:下面两行是 Galecrest 的 Tackle,没打中(它的命中率只有 0.16,第 4 节)
CStun 和 Pocket Sand 的结构完全一样,只是换成 new Stunned()(标注 1)和另一句话(标注 2)。这句话没有 \n,所以只占一行。被眩晕的一方 3 回合内打出去的伤害减半,靠的就是上面 getDamageMultiplier() 里的 0.5。Pocket Sand 和 Stun 的能量都是 4。
6. BoostSkill 和 Wait:不用瞄准对手的技能
BoostSkill:一定成功,给自己加点
1package au.edu.unimelb.swen.oop.mokepon.skill;2 3import au.edu.unimelb.swen.oop.mokepon.battle.Fighter;4import au.edu.unimelb.swen.oop.mokepon.model.Stat;5import java.util.Random;6 7/* A skill that always works and raises one of the user's own stats by 1 to 10 points.8 * The boost skills only differ in which stat they raise.9 */10public abstract class BoostSkill extends Skill {111 private static final int MAX_BOOST = 10;12 132 private final Stat stat;14 15 protected BoostSkill(String name, int energyCost, Stat stat) {16 super(name, energyCost);17 this.stat = stat;18 }19 20 /* The stat cannot go above its limit, so the log shows the points actually gained. */21 @Override22 protected String applyEffect(Fighter user, Fighter target, Random random) {233 int boost = random.nextInt(MAX_BOOST) + 1;244 int gained = user.raiseStat(stat, boost);255 return "\tThey raised their " + stat.getLabel().toLowerCase() + " by " + gained26 + " points!";27 }28}- 标注 1,第 11 行:
MAX_BOOST = 10,一次最多加 10 点。 - 标注 2,第 13 行:
stat:这个技能要加哪一项属性。类型是枚举 EStat(HP、ENERGY、STRENGTH、ACCURACY、DEFENCE、DODGE 之一;枚举见 第 2 章第 6 节)。它由子类在构造器里传上来(第 15–18 行)。 - 标注 3,第 23 行:
random.nextInt(MAX_BOOST)得到 0 到 9 之间的整数,再 + 1,就是 1 到 10。 - 标注 4,第 24 行:
user.raiseStat(stat, boost):给用技能的人自己加。它返回实际加上去的点数(见下面的代码)。 - 标注 5,第 25–26 行:拼出 “They raised their accuracy by 3 points!”。
stat.getLabel()是 "Accuracy",toLowerCase()变成小写 "accuracy"。
ABoostSkill 直接继承 ASkill,它的 applyEffect 里没有 hits:一定成功,被致盲也没关系(第 4 节 Ironshell 的例子)。参数 target 一次都没用到:加点只加自己。
加点真正发生在 CFighter 的 raiseStat 里。编号 1 = 第 97 行压上限,编号 2 = 第 99 行返回实际加的点数:
94 /* Raise a stat without going above its limit, and return the points actually gained. */95 public int raiseStat(Stat stat, int amount) {96 int oldValue = getStat(stat);971 int newValue = stat.limit(oldValue + amount);98 stats.put(stat, newValue);992 return newValue - oldValue;100 }
- 2Shellurk 用 Aim:accuracy 62 → 65,日志写 3

- 2Owlet 用 Swole:strength 44 → 49,日志写 5

- 2Owlet 用 Bunker:defence 36 → 42,日志写 6

- 2敌人 Nightwing 第 2、3 回合用 Aim:83 → 91 → 96,随机出几就加几,日志写 8、5
- 1第 4 回合:96 + boost 到了 100 或更多,第 97 行
limit把它压在 100;第 99 行返回 100 − 96 = 4,日志写 4
第 97 行用 EStat 的 limit 把新值压在合法范围里。EStat 的每一项都存着自己的最小值和最大值,limit(value) 就是 Math.max(最小值, Math.min(value, 最大值))(限制范围):Strength、Accuracy、Defence、Dodge 都是 1 到 100。第 99 行返回 newValue - oldValue:真正加上去的点数,不是随机出来的 boost。平时两者一样;快到上限时就不一样了。
真实例子就是上面 Nightwing 连用三次 Aim 的那张日志(Shellurk 对 Nightwing 那一仗):敌人 Nightwing 连用 Aim,83 → 91(+8)→ 96(+5),第三次日志写 “by 4 points”,变成 100(标注 1)。96 离上限只差 4:随机出 4 到 10 里的任何一个数,结果都是 100,日志都只能写 4。所以 ABoostSkill 第 25 行写的是 gained,不是 boost(第 20 行的注释就是这么说的)。再用一次 Aim,日志会写 “by 0 points”。
截图里一共有 14 次加点,按代码对一遍(加的点数和能量都对得上):
| 那一仗 | 谁用 | 技能 | 属性变化(括号里是日志写的点数) | 能量 |
|---|---|---|---|---|
| Shellurk 对 Nightwing | Shellurk | Aim × 4 | accuracy 62 → 65 → 66 → 73 → 78(+3、+1、+7、+5) | 12 → 9 → 6 → 3 → 0 |
| Shellurk 对 Nightwing | Nightwing(敌人) | Aim × 3 | accuracy 83 → 91 → 96 → 100(+8、+5、+4) | 27 → 24 → 21 → 18 |
| Ironshell 对 Frostback | Ironshell | Aim × 3 | accuracy 55 → 62 → 68 → 69(+7、+6、+1) | 10 → 7 → 4 → 1 |
| Owlet 对 Impike | Owlet | Swole | strength 44 → 49(+5) | 28 → 25 |
| Owlet 对 Impike | Owlet | Bunker | defence 36 → 42(+6) | 25 → 22 |
| Nightwing 对 Bubbore | Bubbore(敌人) | Swole | strength 66 → 68(+2) | 24 → 21 |
| Nightwing 对 Bubbore | Bubbore(敌人) | Bunker | defence 74 → 82(+8) | 23 → 20 |
Nightwing 那一行:它第一回合用的是 Wait,开打时就是满的 27,所以还是 27;后面三次 Aim 各扣 3。属性值在战斗画面上不显示(屏幕上只有 HP 条和能量条),它们存在 CFighter 的 stats 里。战斗结束,这个 CFighter 被丢掉,加的点也就没了(第 10 章)。
Aim、Bunker、Nimble、Swole:只选一个属性
1package au.edu.unimelb.swen.oop.mokepon.skill;2 3import au.edu.unimelb.swen.oop.mokepon.model.Stat;4 5/* Raises the user's own accuracy. */6public class Aim extends BoostSkill {71 private static final int ENERGY_COST = 3;8 9 public Aim() {102 super("Aim", ENERGY_COST, Stat.ACCURACY);11 }12}1package au.edu.unimelb.swen.oop.mokepon.skill;2 3import au.edu.unimelb.swen.oop.mokepon.model.Stat;4 5/* Raises the user's own strength. */6public class Swole extends BoostSkill {71 private static final int ENERGY_COST = 3;8 9 public Swole() {102 super("Swole", ENERGY_COST, Stat.STRENGTH);11 }12}四个加点技能的代码一模一样,只有标注 2 传给 super 的那个 EStat 不同;能量(标注 1)都是 3:
| 类 | 传给 super 的 Stat | 能量 | 截图里有没有用过 |
|---|---|---|---|
| CAim | Stat.ACCURACY | 3 | 有(Shellurk、Nightwing、Ironshell) |
| CBunker | Stat.DEFENCE | 3 | 有(Owlet、Bubbore) |
| CNimble | Stat.DODGE | 3 | 没有 |
| CSwole | Stat.STRENGTH | 3 | 有(Owlet、Bubbore) |
下面是 Shellurk 一直用 Aim 的真实画面。每一帧看三个地方:左下蓝条上的能量数字、中间 Actions 下面的按钮、右边日志的最后一行:
![Shellurk 的战斗开始:技能 Tackle [5]、Aim [3]、Wait [-2],能量 12](shots/battle-start-b.jpg)



Wait:负的能量消耗
1package au.edu.unimelb.swen.oop.mokepon.skill;2 3import au.edu.unimelb.swen.oop.mokepon.battle.Fighter;4import java.util.List;5import java.util.Random;6 7/* Does nothing except restore energy (through its negative cost). */8public class Wait extends Skill {91 private static final int ENERGY_COST = -2;102 private static final List<String> MESSAGES = List.of(11 "\tThey get a little more bored.",12 "\tThey wonder if this is a good strategy.",13 "\tThey think about what they will have for dinner.");14 15 public Wait() {16 super("Wait", ENERGY_COST);17 }18 19 @Override20 protected String applyEffect(Fighter user, Fighter target, Random random) {213 return MESSAGES.get(random.nextInt(MESSAGES.size()));22 }23}- 标注 1,第 9 行:能量消耗是 −2。Wait 回能量,靠的不是自己的代码,而是父类
use第 31 行那句user.spendEnergy(energyCost):付 −2 点,就是多 2 点。 - 标注 2,第 10–13 行:三句台词,用 List.of 做成一个不能修改的列表。
- 标注 3,第 21 行:
random.nextInt(MESSAGES.size()):size 是 3,得到 0、1、2 里的一个,取出那一句。user、target都没用到。
三句台词在截图里都出现过:“They think about what they will have for dinner.” 和 “They wonder if this is a good strategy.”(Impike 对 Galecrest 第一回合,两边都用了 Wait),还有 “They get a little more bored.”(Owlet 输掉那一仗快结束时)。Wait 直接继承 ASkill:它不瞄准对手,也不加属性,放在哪一家都不合适。
“付 −2 点”具体怎么算,要看 CFighter 的两个方法(编号和截图一样):
76 /* Check whether there is enough energy to use the skill. */77 public boolean canUse(Skill skill) {781 return getEnergy() >= skill.getEnergyCost();79 }⋯ 省略第 80–85 行86 /* Pay an energy cost. A negative cost restores energy instead.87 * Energy always stays between zero and the starting energy.88 */89 public void spendEnergy(int amount) {902 int energy = Math.min(getEnergy() - amount, getMaxEnergy());913 stats.put(Stat.ENERGY, Math.max(0, energy));92 }
- 1第 78 行:0 ≥ 5?0 ≥ 3?都不成立,Tackle、Aim 灰了;0 ≥ −2 成立,Wait 能点。能量最小是 0(第 91 行),0 ≥ −2 永远成立,所以只要战斗没结束(上面 updateSkillButtons 第 151 行),Wait 永远能点
- 30 / 12:上一次 Aim 是 3 − 3 = 0;第 91 行保证不会小于 0

- 2Impike 用了 Wait,还是 33 / 33:本来就满了
按代码算三个真实情况(amount 是 −2):
- 满能量时(Impike 33 / 33):第 90 行
getEnergy() - amount= 33 − (−2) = 35;Math.min(35, 33)= 33,最多回到开打时的能量;第 91 行Math.max(0, 33)= 33。所以没变(标注 2)。截图下灰色说明写着“能量 +2”,但这一张里能量其实还是 33:已经满了,加不上去。 - 能量 0 时(Shellurk 0 / 12):0 + 2 = 2,
Math.min(2, 12)= 2。这时只有 Wait 能点(标注 1),用了就有 2 点。 - Owlet 那一仗:Owlet 从 22 开始连用 4 次 Wait:24、26、28,第 4 次还是 28(它开打时就是 28)。
7. SkillCatalogue:每个技能只有一个对象
数据文件里只写了技能的名字,比如 Nightwing 那一段:
56mokepon.5.name=Nightwing57mokepon.5.image=assets/Nightwing.png58mokepon.5.hp=15559mokepon.5.energy=2760mokepon.5.strength=6861mokepon.5.accuracy=8362mokepon.5.defence=4263mokepon.5.dodge=86641mokepon.5.skills=Lance|Aim|Pocket Sand|Wait65mokepon.5.type=NORMAL
- 1选择界面上的 Skills:Lance, Aim, Pocket Sand, Wait
读数据的 CMokeponParser 把这一串名字切开,再一个一个去 CSkillCatalogue 里找对象:
42 /* Skill names are separated by "|", for example Bite|Pocket Sand|Nimble|Wait. */43 private List<Skill> parseSkills(String skillNames) {44 List<Skill> skills = new ArrayList<>();451 for (String skillName : skillNames.split("\\|")) {462 skills.add(SkillCatalogue.find(skillName.trim()));47 }48 return skills;49 }- 标注 1,第 45 行:
split("\\|")按竖线切开(split):"Lance|Aim|Pocket Sand|Wait" → "Lance"、"Aim"、"Pocket Sand"、"Wait"。竖线在 split 用的规则里有特别的意思,所以要写成\\|。 - 标注 2,第 46 行:
trim()去掉两头的空格,再交给SkillCatalogue.find(...)。注意是“类名.方法名”:find是 static 方法,不用先 new 一个目录。(第 44 行的new ArrayList<>()里,<和>是两个字符,代码字体把它们画成了一个小菱形。)
1package au.edu.unimelb.swen.oop.mokepon.skill;2 3import java.util.List;4 5/* Every skill in the game. The data file only gives skill names, and this class6 * finds the skill object with that name. Skills never change after they are made,7 * so all Mokepon can share the same skill objects.8 * To add a new skill, write a new subclass and add one object of it to this list.9 */10public class SkillCatalogue {111 private static final List<Skill> SKILLS = List.of(12 new Bite(), new Tackle(), new Lance(), new Stun(), new PocketSand(),13 new Bunker(), new Aim(), new Swole(), new Nimble(), new Wait());14 15 /* Find a skill by its exact, case-sensitive name. */162 public static Skill find(String name) {17 for (Skill skill : SKILLS) {183 if (skill.getName().equals(name)) {19 return skill;20 }21 }224 throw new IllegalArgumentException("Unknown skill: " + name);23 }24}- 标注 1,第 11–13 行:
private static final List<Skill> SKILLS = List.of(new Bite(), …, new Wait())。static:这个列表属于类本身,整个程序只有一份;程序第一次用到 CSkillCatalogue 时造好,10 个new只执行这一次。List.of造出来的列表不能再加、不能再删。 - 标注 2,第 16 行:
public static Skill find(String name):返回类型是 ASkill,不管找到的是 Lance 还是 Wait。 - 标注 3,第 18 行:用
equals比较名字(字符串要用 equals,不能用两个等号,见 == 和 equals)。大小写要完全一样:写成 "lance" 就找不到。找到就直接返回列表里那个对象,不是复制一个新的。 - 标注 4,第 22 行:10 个都不是,就
throw new IllegalArgumentException("Unknown skill: " + name):抛出异常,读数据就停在这里,告诉你哪个名字写错了。
大家共用同一个对象
结果是:全游戏每种技能只有一个对象。36 只 Mokepon 的技能里都有 Wait,它们的 skills 列表里放的全是同一个 Wait 对象。画两只出来看:
两只 Mokepon 用的是同一个 Aim 对象,为什么不会互相影响?看 Shellurk 对 Nightwing 那一仗:两边都用了 Aim。编号 1 = 第 81 行,编号 2 = 第 89 行:
79 log.add("");80 log.add("Player's " + player.getName() + " used " + playerSkill.getName() + "!");811 log.add(playerSkill.use(player, enemy, random));82 if (!enemy.isAlive()) {83 finish(true);84 return;85 }86 87 Skill enemySkill = enemyStrategy.chooseSkill(enemy, random);88 log.add("Enemy " + enemy.getName() + " used " + enemySkill.getName() + "!");892 log.add(enemySkill.use(enemy, player, random));
- 1Shellurk 用 Aim:
user是 Shellurk,加的是 Shellurk 的 accuracy(+1) - 2Nightwing 用同一个 Aim 对象:
user是 Nightwing,加的是 Nightwing 的(+8)
第 81 行把 player 当 user 传进去,第 89 行把 enemy 当 user 传进去。Aim 对象自己什么都不记:它的 applyEffect 只改“这一次传进来的 user”。所有会变的东西(能量、HP、属性、效果)都在两个 CFighter 里。
所以共用是安全的。原因写在 CSkillCatalogue 第 6–7 行的注释里:Skills never change after they are made。具体来说:
- 所有技能的字段都是
private final:name、energyCost、power、stat,造好就改不了;其余都是static final常量。 - 没有任何 setter(设置方法)。Project 1 的
Skill有setName、setEnergyCost,那样的对象就不敢随便共用。 - 这样的对象叫不可变对象(immutable object)。不可变,就可以放心共用。
反过来,前面说过效果对象每次都新造:CBleeding 要记还剩几回合,这个数会变,两个对手不能共用一份。
8. 动起来看:模板方法一步一步跑
一次 Lance,从头到尾
把前面几节连起来,看第 1 节那一下 Lance 到底跑了哪些代码。怎么看这个动画:
- 上面的图:亮框 = 这一步正在跑哪个类写的代码。上排四个框是同一个 Lance 对象身上的四层代码;它们之间灰色的空心三角只说明谁继承谁(extends),不是调用方向。下排是它碰到的其他对象。红色虚线 = 这一步的调用或返回。
- 图下面:这一步正在跑的那段代码(亮的那几行)。
- 最下面三块截图:左边上下两块是 1 你的能量、2 Bubbore 的 HP 条,右边是 3 日志。画面要等整个回合做完、CBattleScreen 调用
update()才刷新,所以前 15 步截图一直是出手前的样子。
74 if (!player.canUse(playerSkill)) {75 log.add("Not enough energy.");76 return;77 }78 79 log.add("");80 log.add("Player's " + player.getName() + " used " + playerSkill.getName() + "!");81 log.add(playerSkill.use(player, enemy, random));87 Skill enemySkill = enemyStrategy.chooseSkill(enemy, random);88 log.add("Enemy " + enemy.getName() + " used " + enemySkill.getName() + "!");89 log.add(enemySkill.use(enemy, player, random));⋯ 省略第 90–94 行95 endTurn();27 /* Template Method: pay the energy cost first (even if an attack then misses),28 * then apply the effect. Returns the message for the battle log.29 */30 public final String use(Fighter user, Fighter target, Random random) {31 user.spendEnergy(energyCost);32 return applyEffect(user, target, random);33 }17 @Override18 protected final String applyEffect(Fighter user, Fighter target, Random random) {19 if (!hits(user, target, random)) {20 return MISS_MESSAGE;21 }22 return applyHit(user, target, random);23 }31 private boolean hits(Fighter user, Fighter target, Random random) {32 if (random.nextDouble() >= calculateHitChance(user, target)) {33 return false;34 }35 return random.nextDouble() >= user.getMissChance();36 }38 private double calculateHitChance(Fighter user, Fighter target) {39 int accuracy = user.getStat(Stat.ACCURACY);40 int dodge = target.getStat(Stat.DODGE);41 double accuracySquared = accuracy * accuracy;42 double dodgeSquared = dodge * dodge;43 return accuracySquared / (accuracySquared + dodgeSquared);44 }17 @Override18 protected String applyHit(Fighter user, Fighter target, Random random) {19 String hitMessage = super.applyHit(user, target, random);20 target.addEffect(new Bleeding(user));21 return hitMessage + " They started bleeding!";22 }10 private final double power;⋯ 省略第 11–16 行17 @Override18 protected String applyHit(Fighter user, Fighter target, Random random) {19 int damage = user.calculateDamage(target, power);20 target.takeDamage(damage);21 return "\tHit " + target.getName() + " for " + damage + " damage!";22 }19 private static final int DAMAGE_SCALE = 50;⋯ 省略第 20–117 行118 public int calculateDamage(Fighter target, double power) {119 double damage = power * getStat(Stat.STRENGTH) * DAMAGE_SCALE120 / target.getStat(Stat.DEFENCE);121 return (int) (damage * getDamageMultiplier());122 }81 /* Lose HP without going below zero. */82 public void takeDamage(int amount) {83 stats.put(Stat.HP, Math.max(0, getHp() - amount));84 }
- 1Nightwing 的能量(左下蓝条):23 / 27

- 2Bubbore 的 HP 条(右上绿条):195 / 195,满的

- 1Nightwing 的能量(左下蓝条):19 / 27

- 2Bubbore 的 HP 条(右上绿条):151 / 195

- 3点 Lance [4] 之前:日志里只有第一回合(Pocket Sand 和 Swole),下面是空的

- 3回合做完、画面刷新以后:新的 4 行字
- 你点了 Lance [4]。CBattleScreen 的
useSkill调用battle.playTurn(skill)。CBattle 第 79–80 行先写一个空行和 “Player's Nightwing used Lance!”,第 81 行调用playerSkill.use(player, enemy, random)。playerSkill指着 CSkillCatalogue 里那个 Lance 对象。截图是点之前的样子(标注 3:日志还停在第一回合)。 - Lance 类里没有
use,AAttackSkill、ATargetedSkill 里也没有:use只写在 ASkill 里(Skill 第 30 行),而且是 final,谁也不能改写。所以跑的是 ASkill 的use。 - Skill 第 31 行
user.spendEnergy(energyCost):energyCost 是 Lance 构造器一层层传上来的 4。Nightwing 的能量 23 → 19。这时还不知道打不打得中,能量已经付了。画面还没刷新,所以截图里(标注 1)仍是 23 / 27。 - Skill 第 32 行
return applyEffect(user, target, random)。applyEffect在 ASkill 里只有方法头。Java 从对象真正的类 Lance 开始往上找有代码的版本:Lance 没写,AttackSkill 没写,TargetedSkill 写了。所以红色虚线指向 ATargetedSkill。这叫动态绑定。 - 现在跑的是 TargetedSkill 第 18 行的
applyEffect。第 19 行第一件事就是hits(user, target, random):判断打没打中。 hits先调用calculateHitChance(TargetedSkill 第 38–44 行):读 user(Nightwing)的 Accuracy 83、target(Bubbore)的 Dodge 55。83 × 83 = 6889,55 × 55 = 3025,6889 ÷ (6889 + 3025) ≈ 0.69。- 第一次判断(TargetedSkill 第 32 行):随机出来的小数小于 0.69,所以第 33 行的
return false没有执行。第二次判断(第 35 行):Nightwing 没被致盲,getMissChance()是 0,任何小数都 ≥ 0,结果是 true。hits返回 true。 - 打中了,走到 TargetedSkill 第 22 行
return applyHit(user, target, random)。applyHit在 TargetedSkill 里也只有方法头;对象是 Lance,Lance 自己写了applyHit,所以红色虚线跳过 AttackSkill,直接指向 CLance。 - 现在跑的是 Lance 第 18 行的
applyHit。第 19 行第一句就是super.applyHit(user, target, random):先跑父类 AAttackSkill 的applyHit,也就是普通攻击那一套。 - AttackSkill 第 19 行请 user 算伤害:
user.calculateDamage(target, power)。power(第 10 行的字段)是 Lance 构造器交上来的 0.5。 - 在 Nightwing 的 CFighter 里算(Fighter 第 118–121 行):0.5 × 68(Nightwing 自己的 Strength)× 50(第 19 行的 DAMAGE_SCALE)÷ 74(target 也就是 Bubbore 的 Defence)= 22.97;Nightwing 没被眩晕,倍率 1;
(int)去掉小数 → 22。 - 回到 AttackSkill 第 20 行
target.takeDamage(22),跑的是 Fighter 第 82–84 行:Bubbore 的 HP 195 − 22 = 173。Math.max(0, …)保证 HP 不会小于 0。截图里的 HP 条(标注 2)还是满的:画面还没刷新。 - AttackSkill 第 21 行返回
"\tHit Bubbore for 22 damage!"。回到 Lance 第 19 行,这句话存进变量hitMessage。 - Lance 第 20 行
target.addEffect(new Bleeding(user)):新造一个 CBleeding,记住攻击者是 Nightwing,放进 Bubbore 的effects列表。它要等回合结束才扣血。 - Lance 第 21 行返回
hitMessage + " They started bleeding!"。这句话一路原样交回去:TargetedSkill 第 22 行 → Skill 第 32 行 → Battle 第 81 行log.add(...)(红色虚线:返回 String)。Lance 的出手到这里结束。 - 回合剩下的部分(第 11 章):Battle 第 87–89 行让 Bubbore 出手,它用 Wait(同一个
use,能量 +2);第 95 行endTurn(),Bleeding 再扣 22:173 → 151。然后 CBattleScreen 调用update(),画面才刷新:能量 19 / 27(标注 1),HP 条 151 / 195(标注 2),日志多了 4 行字(标注 3,前面还有 1 个空行)。
这 16 步里,Lance 自己的代码只跑了 3 行(第 19–21 行)。其余的:付能量是 ASkill 的,命中判定是 ATargetedSkill 的,算伤害、扣血是 AAttackSkill 和 CFighter 的。这就是继承加模板方法的好处:每个类只写自己那一点,合起来就是完整的一招。
一整场:家谱里哪些类在干活
再看 Nightwing 对 Bubbore 的前三个回合。上面是家谱(只画这一仗用到的类),亮的框和箭头 = 这一句日志是哪几层类的代码造出来的;中间是这一步说到的那段代码;下面是那一刻的日志(放大),亮的红框是这一步说的那几行。
10public class SkillCatalogue {11 private static final List<Skill> SKILLS = List.of(12 new Bite(), new Tackle(), new Lance(), new Stun(), new PocketSand(),13 new Bunker(), new Aim(), new Swole(), new Nimble(), new Wait());14 15 /* Find a skill by its exact, case-sensitive name. */16 public static Skill find(String name) {14 15 @Override16 protected String applyHit(Fighter user, Fighter target, Random random) {17 target.addEffect(new Blinded());18 return "\tPocket sand blinded " + target.getName() + "!\n"19 + "\tTheir targeted skills may miss for " + Blinded.TURNS + " turns.";20 }21 @Override22 protected String applyEffect(Fighter user, Fighter target, Random random) {23 int boost = random.nextInt(MAX_BOOST) + 1;24 int gained = user.raiseStat(stat, boost);25 return "\tThey raised their " + stat.getLabel().toLowerCase() + " by " + gained26 + " points!";27 }16 /* Deal the normal attack damage first, then start the bleeding. */17 @Override18 protected String applyHit(Fighter user, Fighter target, Random random) {19 String hitMessage = super.applyHit(user, target, random);20 target.addEffect(new Bleeding(user));21 return hitMessage + " They started bleeding!";22 }16 super("Wait", ENERGY_COST);17 }18 19 @Override20 protected String applyEffect(Fighter user, Fighter target, Random random) {21 return MESSAGES.get(random.nextInt(MESSAGES.size()));22 }24 public List<String> applyLingeringDamage(Fighter owner) {25 owner.takeDamage(attacker.calculateDamage(owner, HALF_STRENGTH));26 if (isLastTurn()) {27 return List.of("The bleeding stopped.");28 }29 return List.of();30 }
- 1只有开头一行

- 1Nightwing 的 Pocket Sand
- 2Bubbore 的 Swole

- 1Nightwing 的 Lance
- 2Bubbore 的 Wait

- 1Nightwing 的 Wait
- 2Bubbore 的 Bunker
- 3流血结束
- 战斗开始。Nightwing 的 4 个技能分在三家:CLance(AttackSkill 一家)、CPocketSand(TargetedSkill 一家)、CAim(BoostSkill 一家)、CWait(直接继承 Skill)。亮的就是这 4 个类,它们的对象都是 CSkillCatalogue 第 11–13 行造的那唯一的一个。
- Nightwing 用 Pocket Sand。ASkill.use 先扣能量 27 → 23;ATargetedSkill.applyEffect 判定命中(0.69,通过了);再跑 CPocketSand.applyHit:第 17 行给 Bubbore 新造一个 Blinded,第 18–19 行返回两行字。
- Bubbore 用 Swole。同一个 ASkill.use 先扣能量 24 → 21;然后是 ABoostSkill.applyEffect:第 23 行随机出一个 1–10 的数,第 24 行
raiseStat把力量 66 → 68,第 25 行写实际加的 2 点。Bubbore 刚被致盲,可 Swole 根本不判命中,照样成功。 - Nightwing 用 Lance:四层都亮了(上一个动画一步一步看过)。能量 23 → 19,伤害 22;第 20 行再给 Bubbore 加一个 CBleeding(右边亮的那个框)。
- Bubbore 用 Wait:ASkill.use 的
spendEnergy(-2)让能量 21 → 23;CWait.applyEffect 第 21 行只是随机挑一句话。回合结束时 CBleeding 扣了 22(日志里没有这一行),HP 173 → 151。 - 第三回合 Nightwing 用 Wait:19 → 21。它用的和刚才 Bubbore 用的是同一个 Wait 对象,跑的也是同一行第 21 行。
- Bubbore 用 Bunker:和 Swole 跑的是 ABoostSkill 里同一段代码,只是
stat换成了 DEFENCE。防御 74 → 82(+8),能量 23 → 20。 - “The bleeding stopped.” 不是技能写的:回合结束时,CBattle 让 Bubbore 身上的 CBleeding 再扣一次血(Bleeding 第 25 行,第 13 章);这是它的最后一回合,所以第 26–27 行返回这句话。防御刚升到 82,这次只扣 0.5 × 68 × 50 ÷ 82 = 20.73 → 20,HP 151 → 131。Bunker 马上就起作用了。
10 个技能一张表
最后一列可以点,直接跳到这一章里那张真实截图:
| 技能(父类) | 能量 | 做什么 | 真实日志 | 截图 |
|---|---|---|---|---|
| CBite(AttackSkill) | 1 | 威力 1 的攻击 | 截图里没人用过;打中会写 Hit … for N damage! | 无(截图里没人用过) |
| CTackle(AttackSkill) | 5 | 威力 2 的攻击 | Galecrest 用过一次:But they missed. | Stun 那张的标注 3 |
| CLance(AttackSkill) | 4 | 威力 0.5,打中后流血 2 回合 | Hit Bubbore for 22 damage! They started bleeding! | 第 1 节大图、Owlet 那张 |
| CPocketSand(TargetedSkill) | 4 | 对手致盲 3 回合 | Pocket sand blinded Bubbore! Their targeted skills may miss for 3 turns. | 第 5 节 |
| CStun(TargetedSkill) | 4 | 对手伤害减半 3 回合 | Galecrest was stunned! Their damage is halved for 3 turns. | 第 5 节 |
| CAim(BoostSkill) | 3 | 自己 Accuracy +1~10 | They raised their accuracy by 3 points! | 第 6 节、Nightwing 那张 |
| CBunker(BoostSkill) | 3 | 自己 Defence +1~10 | They raised their defence by 6 points! | 第 6 节 |
| CNimble(BoostSkill) | 3 | 自己 Dodge +1~10 | 截图里没人用过;会写 They raised their dodge by N points! | 无(截图里没人用过) |
| CSwole(BoostSkill) | 3 | 自己 Strength +1~10 | They raised their strength by 5 points! | 第 6 节 |
| CWait(Skill) | −2 | 最多回 2 点能量(不超过开打时的值),随机一句话 | They think about what they will have for dinner. | 第 6 节 |
9. 加一个新技能:开闭原则,和 Project 1 比一比
ASkill 第 7 行的注释写着:To add a new skill, write a new subclass instead of changing the existing ones. CSkillCatalogue 第 8 行也说:写一个新子类,再往列表里加一个对象。先看 Project 1 是怎么写技能的。它只有一个 Skill 类,先看造对象的方法:
31 // Create a supported skill with its fixed energy cost.32 public static Skill generateSkill(String name) {331 if (name.equals("Bite")) {34 return new Skill("Bite", 1);35 } else if (name.equals("Tackle")) {36 return new Skill("Tackle", 5);37 } else if (name.equals("Bunker")) {38 return new Skill("Bunker", 3);39 } else if (name.equals("Wait")) {40 return new Skill("Wait", -2);41 }42 return null;43 }标注 1,第 33–41 行:generateSkill 用一串 if / else if 按名字造对象,只认识 4 个技能:Bite、Tackle、Bunker、Wait。名字不认识就返回 null(第 42 行)。再看出手的方法:
47 public String use(Mokepon attacker, Mokepon target, Random random) {48 // Pay the energy cost even if the attack misses.49 attacker.spendEnergy(energyCost);50 51 // Use accuracy and dodge to calculate the hit chance.522 double hitProbability = Math.pow(attacker.getAccuracy(), 2) / (Math.pow(attacker.getAccuracy(), 2) + Math.pow(target.getDodge(), 2));53 54 // Round damage down with integer division.55 int biteDamage = attacker.getStrength() * 50 / target.getDefence();56 int tackleDamage = 2 * attacker.getStrength() * 50 / target.getDefence();57 583 if (name.equals("Bite")) {59 if (random.nextDouble() < hitProbability) {60 target.takeDamage(biteDamage);61 return "\tHit " + target.getName() + " for " + biteDamage + " damage!";62 }63 return "\tBut they missed.";64 654 } else if (name.equals("Tackle")) {66 if (random.nextDouble() < hitProbability) {67 target.takeDamage(tackleDamage);68 return "\tHit " + target.getName() + " for " + tackleDamage + " damage!";69 }70 return "\tBut they missed.";715 } else if (name.equals("Bunker")) {⋯ 省略第 72–78 行79 attacker.setDefence(attacker.getDefence() + defence);80 return "\tThey raised their defence by " + defence + " points!";816 } else {82 if (attacker.getEnergy() > attacker.getMaxEnergy()) {- 标注 2,第 52 行:命中率公式,和 Project 2 ATargetedSkill 第 43 行是同一个公式(这一行很长,往右拉才能看完)。第 49 行先付能量,顺序也和 Project 2 的
use一样,只是没有 final 来保证。 - 标注 3,第 58 行:
use里面又是一串 if / else,按名字决定做什么。这里是 Bite 一个分支。 - 标注 4,第 65 行:Tackle 一个分支。它和 Bite 那一段几乎一模一样,只是换了伤害变量:是复制粘贴出来的。
- 标注 5,第 71 行:Bunker 一个分支。
- 标注 6,第 81 行:
else:剩下的全当 Wait。命中率、伤害、加防御、回能量全挤在这一个方法里。
两种写法放在一起画出来:
假如要加 Lance(Project 2 才有的技能),两种写法要动的地方完全不一样:
| Project 1 | Project 2(你交的) | |
|---|---|---|
| 新加 | 没有新类,只能改旧的 | 一个新类 CLance(23 行) |
| 要改 | generateSkill 多一个 else if;use 多一个分支,流血也写进去 | CSkillCatalogue 第 12 行多一个 new Lance() |
| 不用碰 | (每个技能都在同一个方法里,改一处可能影响别的) | ASkill、ATargetedSkill、AAttackSkill、CBattle、CFighter、CBattleScreen、CRandomEnemyStrategy |
| 写错名字 | 返回 null,用的时候才出错 | find 马上抛出 Unknown skill |
这就是开闭原则(Open/Closed Principle):对扩展开放(可以加新的子类),对修改关闭(已经能用的类不用改)。能做到,靠的是两件事:
- 大家只依赖抽象的 ASkill。CBattle.playTurn 收的是
Skill,CFighter.canUse 收的是Skill,CBattleScreen 的按钮只调用getName()和getEnergyCost()。任何新的子类都能直接放进去用。 - 共同的步骤已经写在父类里。新的攻击只要继承 AAttackSkill,付能量、命中判定、伤害公式全都现成。
比如要加一个威力 1.5、能量 3 的攻击,只要写这样一个类(这是假设的例子,你交的代码里没有它):
// 假设的例子:不在你的项目里
public class Headbutt extends AttackSkill {
private static final int ENERGY_COST = 3;
private static final double POWER = 1.5;
public Headbutt() {
super("Headbutt", ENERGY_COST, POWER);
}
}然后在 CSkillCatalogue 的 List.of(...) 里加上 new Headbutt(),数据文件里某只 Mokepon 的 skills 写上 Headbutt。按钮、日志、敌人随机选技能,全都自动能用。
这也说明了另一个原则:任何 ASkill 的子类都能放在需要 ASkill 的地方,程序照样正常(里氏替换原则)。在真实代码里就是:CBattle.playTurn(Skill playerSkill) 收到 Lance、Wait,或者将来的 Headbutt,都一样跑 playerSkill.use(...),CBattle 从来不检查它是哪一个子类。模板方法、开闭原则和这些原则在整个项目里的位置,第 16 章会再统一整理。下一章 第 13 章 讲这一章多次提到的 CBleeding、CBlinded、CStunned 怎么持续几回合,以及道具。
自测
Skill.use 为什么写成 final?如果 Lance 能改写 use,会有什么风险?
use 是模板方法:它定死了“先 user.spendEnergy(energyCost),再 applyEffect(...)”这个顺序。final 让任何子类都改不了这个顺序,所以每个技能都一定先付能量。如果 Lance 能改写 use,它可能忘了扣能量,或者改成“打中才扣”,规则就不一样了。
English: use() is a final Template Method, so every skill pays its energy cost first and then applies its effect; no subclass can change that order.
Owlet 那一仗,Impike 的 Stun 没打中,能量却从 22 变成了 18。指出是哪一行代码造成的。
ASkill 第 31 行 user.spendEnergy(energyCost) 在第 32 行 applyEffect 之前执行;命中判定在 ATargetedSkill.applyEffect 里面(第 19 行 hits)。所以先付了 4 点,再发现没中,返回 But they missed.。
English: The energy is paid in Skill.use before applyEffect runs the hit check, so it is paid even if the attack misses.
Nightwing(Accuracy 83,Strength 68)用 Lance 打 Bubbore(Dodge 55,Defence 74)。命中率是多少?打中扣多少?
命中率 = 83² ÷ (83² + 55²) = 6889 ÷ 9914 ≈ 0.69(Nightwing 没被致盲,第二次判断一定通过)。伤害 = 0.5(Lance 的 power)× 68 × 50(DAMAGE_SCALE)÷ 74 = 22.97,倍率 1,(int) 去掉小数 = 22。回合结束流血再扣 22,所以 HP 195 → 151。
CLance.applyHit 第一句为什么是 super.applyHit(...)?把它删掉会怎样?
super.applyHit 运行父类 AAttackSkill 的版本:算伤害、takeDamage、返回 “Hit … for N damage!”。Lance 只在后面多加流血。如果删掉这一句,第 21 行用到的变量 hitMessage 就不存在了,编译都通不过;就算改成不用它,Lance 也只会让对手流血,不会扣那一下的伤害。
English: Lance overrides applyHit(), calls super.applyHit() to deal the normal attack damage, and then adds a Bleeding effect to the target.
Bubbore 被 Pocket Sand 致盲以后用 Bunker,会不会因为致盲而失败?为什么?
不会。致盲只在 ATargetedSkill.hits 的第二次判断起作用(user.getMissChance())。CBunker 属于 ABoostSkill,它的 applyEffect 根本不调用 hits,一定成功。截图里 Bubbore 被致盲后用 Bunker 加了 8 点防御,Ironshell 被致盲后用 Aim 也两次都成功。
Shellurk 和 Nightwing 用的是同一个 Aim 对象。为什么 Shellurk 用 Aim 不会把 Nightwing 的 Accuracy 也加上去?
Aim 对象自己不存任何会变的数据:它只有 final 的名字、能量和 stat。加点加的是这一次传进来的 user:CBattle 第 81 行传 player,第 89 行传 enemy。会变的数值都在各自的 CFighter 里。所以 CSkillCatalogue 可以让所有 Mokepon 共用同一个对象。
English: Skill objects are immutable and keep no battle state, so SkillCatalogue can share one object per skill; all changing state lives in the Fighter passed to use().
Wait 的能量消耗是 −2。为什么 Impike 满能量时用 Wait,能量还是 33?为什么能量是 0 时 Wait 按钮还能点?
spendEnergy(-2) 算 33 − (−2) = 35,再 Math.min(35, getMaxEnergy()) = 33:能量最多回到开打时的值。canUse 比较的是 能量 ≥ energyCost,0 ≥ −2 永远成立(能量不会小于 0),而 CBattleScreen.updateSkillButtons 第 151 行只在战斗结束时才另外把按钮变灰,所以只要战斗还没结束,Wait 就永远能点。
老师问:How would you add a new skill to this program? 用中文说一遍,再说英文。
写一个新的子类:攻击就继承 AAttackSkill,只用 super(...) 传名字、能量、威力;加点就继承 ABoostSkill,传一个 EStat;效果完全不同就继承 ATargetedSkill 写 applyHit,或者继承 ASkill 写 applyEffect。然后在 CSkillCatalogue 的列表里加一个对象。CBattle、CFighter、CBattleScreen 都不用改。Project 1 要在 generateSkill 和 use 的 if/else 里各加一个分支。
English: I would write one new subclass of AttackSkill, BoostSkill, TargetedSkill or Skill, and add one object of it to SkillCatalogue. No existing class has to change, because Battle only depends on the abstract Skill (Open/Closed Principle).