杀戮尖塔mod开发的二三事
注意:此文不是教程,只是个人的感想和体会。教程实际上也可以写,但是懒得写了(
文中提到的人物mod还没有做完,暂缓更新。
2023/9/14更新:此mod已经在创意工坊发布,且于今日增加了英文翻译。
开发过程
准备
本人对杀戮尖塔这款游戏的可拓展性早有耳闻,也玩过许许多多其他人开发的mod,于是就心血来潮想要自己开发一个杀戮尖塔的mod。
由于没有任何相关的项目实践基础,我只能去参考一些相关的教程来了解如何开发。下面是我参考过的教程链接(中文):
杀戮尖塔MOD制作 - 知乎 (zhihu.com)
REME大佬主编的mod制作教程
以及官方文档(生肉):
Github-Basemod
Github-StSLib
本人在开发过程中遇到的许多技术难题都是靠着这几份教程解决的。
除了这些文档与教程之外,我也大量参考了其他mod作品,由于java的开源本质,这些mod文件可以被很轻易地反编译。这些mod包括替罪羔羊mod、AKD遗物mod、蔡徐坤mod等等。在此向这些文档、教程以及mod作者致以谢意。
构建框架
在翻了一点资料后,我发现这款游戏是用基于Java编写的开源框架libGDX开发的。然后又查了一下,发现这框架可不得了,因为我在这个框架开发的游戏之中又见到了一些其他熟悉的身影:
- mindustry(像素工厂)
一款游戏性极强的RTS塔防游戏,整体玩法上类似异星工厂,开源免费
- unciv(像素版文明5)
手机上能玩的仿文明类游戏中最好的一款,轻量流畅,开源免费,还支持联机
- infinitode 2(无尽塔防2)
继塔防游戏三幻神(王国保卫战,植物大战僵尸,气球塔防)之后又一款优秀的塔防游戏,机制上比较纯粹,少数的雷点包括资源太肝(不过反正是单机,简单地修改一下掉落率就行)和制作者亲乌(没办法的事),其他都很不错,而且免费游戏要啥自行车啊(
- shattered pixel dungeon(破碎像素地牢)
这位更是重量级,极其经典的传统式rogue-like游戏像素地牢最成功的改版,也是开源免费
这就是开源框架吗,开源社区的威力可见一斑。而杀戮尖塔虽然不是免费游戏,但开源(代码可以随便反编译),而且作为界内公认的最佳rogue-like卡牌游戏,它也没有辜负这个框架的威名(笑)
官网也把杀戮尖塔放在了第一个。
实际上手
接下来就要看看这游戏mod的基本结构了。在折腾了好一段时间后,我也弄清楚了这一点。
这游戏mod的基本组成结构只有两个:
- 内容类,内部包含自己定义的增添内容,包括卡牌、遗物、事件、能力等;
- 注册类,将已经定义的内容在basemod中注册,以让MTS(ModTheSpire,mod启动器)在给游戏打包时将增添的内容打包进去。
注册类
先说注册类。在MTS打包游戏时,它首先会检测包含注解@SpireInitializer
的类,将这些类中的Subscriber
相关内容打包进游戏文件。这个动作是由接口——回调函数实现的。
虽然每一个类都可以加上这个注解,但出于代码管理与游戏性能考虑,一般把大宗的注册任务集中放在一个类中。
1 2 3 4 5 6
| @SpireInitializer public class xxxMod implements EditCardsSubscriber, EditCharactersSubscriber, EditStringsSubscriber, EditRelicsSubscriber, EditKeywordsSubscriber { …… }
|
这个注册类实现了EditCardsSubscriber
、EditCharactersSubscriber
等接口,而这些接口是另一个接口ISubscriber
的子接口。这些接口的定义中只有一个对应的回调函数(如EditCardsSubscriber
接口包含receiveEditCards()
等),而这些函数在这里被实现后便会被MTS在初始化过程中被调用。
1 2 3 4
| public xxxMod() { BaseMod.subscribe((ISubscriber) this); }
|
具体来讲,receiveEditCards()
里需要这样实现:
1 2 3 4
| @Override public void receiveEditCards() { BaseMod.addCard((AbstractCard) new Example_Card()); }
|
这样便可以在游戏中添加Example_Card()
类对应的卡牌。
而这样添加的本质,是basemod把这些卡加进了一个数组之中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public static void addCard(AbstractCard card) { switch (1.$SwitchMap$com$megacrit$cardcrawl$cards$AbstractCard$CardColor[card.color.ordinal()]) { case 1: redToAdd.add(card); break; case 2: greenToAdd.add(card); break; case 3: blueToAdd.add(card); break; case 4: purpleToAdd.add(card); break; case 5: colorlessToAdd.add(card); break; case 6: curseToAdd.add(card); break; default: customToAdd.add(card); } }
|
至于这里的数组具体是怎么被读取并加入游戏中的,这里就不过多追究了。
除了卡牌,其他的内容也是以类似的形式加进去的。
1 2 3 4 5
| @Override public void receiveEditRelics() { BaseMod.addRelicToCustomPool((AbstractRelic) new CustomRelic(), COLOR_OF_YOUR_CHARACTER); }
|
1 2 3 4 5 6
| @Override public void receiveEditStrings() { String cardStrings = Gdx.files.internal("path/to/your/localization/json").readString(String.valueOf(StandardCharsets.UTF_8)); BaseMod.loadCustomStrings(CardStrings.class, cardStrings); }
|
以上便是注册类的大致内容。
内容类
尖塔mod可加入的东西的种类非常之多,包括但不限于人物、卡牌、遗物、关键词、事件、动作、能力等等。接下来便会大致讲讲这些东西都是怎么实现的。
注意,在mod制作过程中应继承已由basemod封装的Customxxx
父类而非原版的Abstractxxx
父类。
人物
人物类是抽象类AbstractPlayer
的子类。它包括的抽象方法包括但不限于指定初始卡组、指定初始遗物、指定对应颜色(尖塔里的卡牌种类是按颜色区分的)、指定选人界面素材、指定心脏对话、指定吸血鬼对话性别等等。人物类的构建比较繁杂与单一,这里就不细说了。
卡牌
卡牌类是抽象类AbstractCard
的子类,需要在被定义后在注册类中注册。它包括的抽象方法有两个,use()
与upgrade()
。前者在卡牌被使用时调用,而后者在卡牌被升级时调用。
卡牌被使用/触发效果时,一般都会触发一个或多个动作(action)。动作留待后面叙述,这里主要讲卡牌相关的功能大致是怎么实现的。
CustomCard
的父类AbstractCard
预定义了大量的函数与内置钩子供开发者自己与mod作者使用。这些函数与钩子囊括了大量的相关功能,包括但不限于规定此卡在本回合中的耗能(setCostForTurn()
),规定此卡可以被打出的条件(canUse()
),在主动丢弃此卡时触发特定效果(triggerOnManualDiscard()
)等等。而这些函数或钩子只需要在对应子类中调用或覆写即可使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
|
package com.megacrit.cardcrawl.cards.red;
import com.megacrit.cardcrawl.actions.AbstractGameAction; import com.megacrit.cardcrawl.actions.common.DamageAction; import com.megacrit.cardcrawl.actions.common.DamageAllEnemiesAction; import com.megacrit.cardcrawl.cards.AbstractCard; import com.megacrit.cardcrawl.cards.DamageInfo; import com.megacrit.cardcrawl.characters.AbstractPlayer; import com.megacrit.cardcrawl.core.AbstractCreature; import com.megacrit.cardcrawl.core.CardCrawlGame; import com.megacrit.cardcrawl.core.Settings; import com.megacrit.cardcrawl.dungeons.AbstractDungeon; import com.megacrit.cardcrawl.localization.CardStrings; import com.megacrit.cardcrawl.monsters.AbstractMonster;
public class Strike_Red extends AbstractCard { public static final String ID = "Strike_R"; private static final CardStrings cardStrings = CardCrawlGame.languagePack.getCardStrings("Strike_R"); public Strike_Red() { super("Strike_R", cardStrings.NAME, "red/attack/strike", 1, cardStrings.DESCRIPTION, AbstractCard.CardType.ATTACK, AbstractCard.CardColor.RED, AbstractCard.CardRarity.BASIC, AbstractCard.CardTarget.ENEMY); this.baseDamage = 6; this.tags.add(AbstractCard.CardTags.STRIKE); this.tags.add(AbstractCard.CardTags.STARTER_STRIKE); } public void use(AbstractPlayer p, AbstractMonster m) { if (Settings.isDebug) { if (Settings.isInfo) { this.multiDamage = new int[(AbstractDungeon.getCurrRoom()).monsters.monsters.size()]; for (int i = 0; i < (AbstractDungeon.getCurrRoom()).monsters.monsters.size(); i++) this.multiDamage[i] = 150; addToBot((AbstractGameAction)new DamageAllEnemiesAction((AbstractCreature)p, this.multiDamage, this.damageTypeForTurn, AbstractGameAction.AttackEffect.SLASH_DIAGONAL)); } else { addToBot((AbstractGameAction)new DamageAction((AbstractCreature)m, new DamageInfo((AbstractCreature)p, 150, this.damageTypeForTurn), AbstractGameAction.AttackEffect.BLUNT_HEAVY)); } } else { addToBot((AbstractGameAction)new DamageAction((AbstractCreature)m, new DamageInfo((AbstractCreature)p, this.damage, this.damageTypeForTurn), AbstractGameAction.AttackEffect.SLASH_DIAGONAL)); } } public void upgrade() { if (!this.upgraded) { upgradeName(); upgradeDamage(3); } } public AbstractCard makeCopy() { return new Strike_Red(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
public class TrapLoop extends CustomCard implements AbstractMemoryCard { public static final String ID = YsyModHelper.MakePath(TrapLoop.class.getSimpleName()); private static final CardStrings cardStrings = CardCrawlGame.languagePack.getCardStrings(ID); private static final String NAME = cardStrings.NAME; private static final String DESCRIPTION = cardStrings.DESCRIPTION; private static final String IMG_PATH = "img/cards/TrapLoop.png"; private static final int COST = -2; private static final CardType CARD_TYPE = CardType.SKILL; private static CardColor COLOR = AbstractCardEnum.xxx_COLOR; private static CardRarity CARD_RARITY = CardRarity.UNCOMMON;
public TrapLoop() { super(NAME, cardStrings.NAME, IMG_PATH, COST, DESCRIPTION, CARD_TYPE, COLOR, CARD_RARITY, CardTarget.NONE); this.baseMagicNumber = 2; this.magicNumber = this.baseMagicNumber; tags.add(CardTagsEnum.WHEN_MEMORY); }
@Override public void use(AbstractPlayer p, AbstractMonster m) { this.addToBot(new DrawCardAction(p, this.magicNumber)); }
public boolean canUse(AbstractPlayer p, AbstractMonster m) { this.cantUseMessage = cardStrings.EXTENDED_DESCRIPTION[0]; return false; }
public void triggerWhenDrawn() { this.addToBot(new DrawCardAction(AbstractDungeon.player, this.magicNumber)); }
@Override public void memory() { AbstractDungeon.actionManager.addToBottom(new UnExhaustAction(this)); AbstractDungeon.actionManager.addToTop(new DrawCardAction(AbstractDungeon.player, this.magicNumber)); }
@Override public AbstractCard makeCopy() { return new TrapLoop(); }
@Override public void upgrade() { if (!this.upgraded) { this.upgradeName(); this.upgradeMagicNumber(1); } } }
|
遗物
遗物是抽象类AbstractRelic
的子类,不包含抽象方法。它的相关定义与卡牌类似,都是相关图片文本资源+定义属性+相关效果。遗物在定义后也需要在注册类中注册。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package com.megacrit.cardcrawl.relics;
import com.megacrit.cardcrawl.dungeons.AbstractDungeon; import com.megacrit.cardcrawl.orbs.AbstractOrb; import com.megacrit.cardcrawl.orbs.Lightning;
public class CrackedCore extends AbstractRelic { public static final String ID = "Cracked Core"; public CrackedCore() { super("Cracked Core", "crackedOrb.png", AbstractRelic.RelicTier.STARTER, AbstractRelic.LandingSound.CLINK); } public String getUpdatedDescription() { return this.DESCRIPTIONS[0]; } public void atPreBattle() { AbstractDungeon.player.channelOrb((AbstractOrb)new Lightning()); } public AbstractRelic makeCopy() { return new CrackedCore(); } }
|
遗物的实现方式与效果也多种多样,许多遗物的效果实际上并没有在本文件中说明,而是在对应的地方加入新的代码来实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| package com.megacrit.cardcrawl.relics;
import com.megacrit.cardcrawl.characters.AbstractPlayer; import com.megacrit.cardcrawl.dungeons.AbstractDungeon; import com.megacrit.cardcrawl.helpers.PowerTip;
public class Ectoplasm extends AbstractRelic { public static final String ID = "Ectoplasm"; public Ectoplasm() { super("Ectoplasm", "ectoplasm.png", AbstractRelic.RelicTier.BOSS, AbstractRelic.LandingSound.FLAT); } public String getUpdatedDescription() { if (AbstractDungeon.player != null) return setDescription(AbstractDungeon.player.chosenClass); return setDescription((AbstractPlayer.PlayerClass)null); } private String setDescription(AbstractPlayer.PlayerClass c) { return this.DESCRIPTIONS[1] + this.DESCRIPTIONS[0]; } public void updateDescription(AbstractPlayer.PlayerClass c) { this.description = setDescription(c); this.tips.clear(); this.tips.add(new PowerTip(this.name, this.description)); initializeTips(); } public void onEquip() { AbstractDungeon.player.energy.energyMaster++; } public void onUnequip() { AbstractDungeon.player.energy.energyMaster--; } public boolean canSpawn() { if (AbstractDungeon.actNum > 1) return false; return true; } public AbstractRelic makeCopy() { return new Ectoplasm(); } }
public void gainGold(int amount) { if (this.hasRelic("Ectoplasm")) { this.getRelic("Ectoplasm").flash(); } else { if (amount <= 0) { logger.info("NEGATIVE MONEY???"); } else { CardCrawlGame.goldGained += amount; this.gold += amount; Iterator var2 = this.relics.iterator();
while(var2.hasNext()) { AbstractRelic r = (AbstractRelic)var2.next(); r.onGainGold(); } } } }
|
那么自定义的卡牌遗物等如何加入新的效果呢?一般有两个方法:
不过这些功能就比较复杂了,可以先去看看上面的教程和文档(
动作
动作是AbstractGameAction
的子类,有一个抽象函数update()
,在动作执行时被调用。它一般是一段特定的效果,在对应的时机被调用,加入尖塔的动作管理器(actionManager)中。它的意义是对一段常用或会触发其他东西的效果进行封装。动作不需要在注册类中注册,在合适的时机调用即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| package com.megacrit.cardcrawl.actions.common;
import com.megacrit.cardcrawl.actions.AbstractGameAction; import com.megacrit.cardcrawl.cards.AbstractCard; import com.megacrit.cardcrawl.characters.AbstractPlayer; import com.megacrit.cardcrawl.core.AbstractCreature; import com.megacrit.cardcrawl.core.CardCrawlGame; import com.megacrit.cardcrawl.core.Settings; import com.megacrit.cardcrawl.dungeons.AbstractDungeon; import com.megacrit.cardcrawl.localization.UIStrings;
public class ExhaustAction extends AbstractGameAction { private static final UIStrings uiStrings = CardCrawlGame.languagePack.getUIString("ExhaustAction"); public static final String[] TEXT = uiStrings.TEXT; private AbstractPlayer p; private boolean isRandom; private boolean anyNumber; private boolean canPickZero; public static int numExhausted; public ExhaustAction(int amount, boolean isRandom, boolean anyNumber, boolean canPickZero) { this.anyNumber = anyNumber; this.p = AbstractDungeon.player; this.canPickZero = canPickZero; this.isRandom = isRandom; this.amount = amount; this.duration = this.startDuration = Settings.ACTION_DUR_FAST; this.actionType = AbstractGameAction.ActionType.EXHAUST; } public ExhaustAction(AbstractCreature target, AbstractCreature source, int amount, boolean isRandom, boolean anyNumber) { this(amount, isRandom, anyNumber); this.target = target; this.source = source; } public ExhaustAction(AbstractCreature target, AbstractCreature source, int amount, boolean isRandom) { this(amount, isRandom, false, false); this.target = target; this.source = source; } public ExhaustAction(AbstractCreature target, AbstractCreature source, int amount, boolean isRandom, boolean anyNumber, boolean canPickZero) { this(amount, isRandom, anyNumber, canPickZero); this.target = target; this.source = source; } public ExhaustAction(boolean isRandom, boolean anyNumber, boolean canPickZero) { this(99, isRandom, anyNumber, canPickZero); } public ExhaustAction(int amount, boolean canPickZero) { this(amount, false, false, canPickZero); } public ExhaustAction(int amount, boolean isRandom, boolean anyNumber) { this(amount, isRandom, anyNumber, false); } public ExhaustAction(int amount, boolean isRandom, boolean anyNumber, boolean canPickZero, float duration) { this(amount, isRandom, anyNumber, canPickZero); this.duration = this.startDuration = duration; } public void update() { if (this.duration == this.startDuration) { if (this.p.hand.size() == 0) { this.isDone = true; return; } if (!this.anyNumber && this.p.hand.size() <= this.amount) { this.amount = this.p.hand.size(); numExhausted = this.amount; int tmp = this.p.hand.size(); for (int i = 0; i < tmp; i++) { AbstractCard c = this.p.hand.getTopCard(); this.p.hand.moveToExhaustPile(c); } CardCrawlGame.dungeon.checkForPactAchievement(); return; } if (this.isRandom) { for (int i = 0; i < this.amount; i++) this.p.hand.moveToExhaustPile(this.p.hand.getRandomCard(AbstractDungeon.cardRandomRng)); CardCrawlGame.dungeon.checkForPactAchievement(); } else { numExhausted = this.amount; AbstractDungeon.handCardSelectScreen.open(TEXT[0], this.amount, this.anyNumber, this.canPickZero); tickDuration(); return; } } if (!AbstractDungeon.handCardSelectScreen.wereCardsRetrieved) { for (AbstractCard c : AbstractDungeon.handCardSelectScreen.selectedCards.group) this.p.hand.moveToExhaustPile(c); CardCrawlGame.dungeon.checkForPactAchievement(); AbstractDungeon.handCardSelectScreen.wereCardsRetrieved = true; } tickDuration(); } }
|
在其他地方(如卡牌遗物代码中),只需要addToBot()
或addToTop()
即可把对应动作加入动作队列的底部/顶部。游戏会从上往下依次处理这些事件。
能力
能力是抽象类AbstractPower
的子类,没有抽象方法。它的实现方法和卡牌遗物类似,也是图片文本素材+特定效果。不过某些效果可能比较难以实现。。。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| package com.megacrit.cardcrawl.powers;
import com.megacrit.cardcrawl.actions.utility.UseCardAction; import com.megacrit.cardcrawl.cards.AbstractCard; import com.megacrit.cardcrawl.core.AbstractCreature; import com.megacrit.cardcrawl.core.CardCrawlGame; import com.megacrit.cardcrawl.localization.PowerStrings;
public class CorruptionPower extends AbstractPower { public static final String POWER_ID = "Corruption"; private static final PowerStrings powerStrings = CardCrawlGame.languagePack.getPowerStrings("Corruption"); public static final String NAME = powerStrings.NAME; public static final String[] DESCRIPTIONS = powerStrings.DESCRIPTIONS; public CorruptionPower(AbstractCreature owner) { this.name = NAME; this.ID = "Corruption"; this.owner = owner; this.amount = -1; this.description = DESCRIPTIONS[0]; loadRegion("corruption"); } public void updateDescription() { this.description = DESCRIPTIONS[1]; } public void onCardDraw(AbstractCard card) { if (card.type == AbstractCard.CardType.SKILL) card.setCostForTurn(-9); } public void onUseCard(AbstractCard card, UseCardAction action) { if (card.type == AbstractCard.CardType.SKILL) { flash(); action.exhaustCard = true; } } }
|
然而腐化的实现远没有看起来那么简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public ShowCardAndAddToHandEffect(AbstractCard card, float offsetX, float offsetY) { this.card = card; UnlockTracker.markCardAsSeen(card.cardID); card.current_x = (float)Settings.WIDTH / 2.0F; card.current_y = (float)Settings.HEIGHT / 2.0F; card.target_x = offsetX; card.target_y = offsetY; this.duration = 0.8F; card.drawScale = 0.75F; card.targetDrawScale = 0.75F; card.transparency = 0.01F; card.targetTransparency = 1.0F; card.fadingOut = false; this.playCardObtainSfx(); if (card.type != CardType.CURSE && card.type != CardType.STATUS && AbstractDungeon.player.hasPower("MasterRealityPower")) { card.upgrade(); }
AbstractDungeon.player.hand.addToHand(card); card.triggerWhenCopied(); AbstractDungeon.player.hand.refreshHandLayout(); AbstractDungeon.player.hand.applyPowers(); AbstractDungeon.player.onCardDrawOrDiscard(); if (AbstractDungeon.player.hasPower("Corruption") && card.type == CardType.SKILL) { card.setCostForTurn(-9); } }
|
这里实际上就是让战斗中临时加入的卡牌也能吃到腐化的减费效果。而操控现实也类似。
然而游戏中的类似动作或效果实际上远不止这一个,而这些东西都要加上腐化、操控现实之类的判断,于是相关效果的实现实际上是非常麻烦且极其容易出bug的。
官方和basemod都没有提供相关钩子,这感觉也是很难看见mod里有类似设计的原因。。。(不过还是有的)
其他感想
总的来说,游戏的逻辑跟我原来想当然的完全不一样。我跟你讲逻辑,你跟我讲遍历,许多看似高大上的效果说到底还是在特定地方加个特定条件然后遍历而已。
1 2 3 4 5 6 7 8 9 10 11
| public static void subscribe(ISubscriber sub, Class<? extends ISubscriber> additionClass) { if (additionClass.equals(StartActSubscriber.class)) { startActSubscribers.add((StartActSubscriber)sub); } else if (additionClass.equals(PostCampfireSubscriber.class)) { postCampfireSubscribers.add((PostCampfireSubscriber)sub); } else if (additionClass.equals(PostDrawSubscriber.class)) { postDrawSubscribers.add((PostDrawSubscriber)sub); } }
|
那这些钩子是在哪起作用的呢?这里举个例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
@SpirePatch(cls = "com.megacrit.cardcrawl.actions.common.ApplyPowerAction", method = "update") public class ApplyPowerActionPostPowerApplyHook { @SpireInsertPatch(rloc = 6, localvars = {"powerToApply", "target", "source"}) public static void Insert(ApplyPowerAction apa, AbstractPower powerToApply, AbstractCreature target, AbstractCreature source) { BaseMod.publishPostPowerApply(powerToApply, target, source); } }
public static void publishPostPowerApply(AbstractPower p, AbstractCreature target, AbstractCreature source) { logger.info("publish on post power apply"); for (PostPowerApplySubscriber sub : postPowerApplySubscribers) sub.receivePostPowerApplySubscriber(p, target, source); unsubscribeLaterHelper((Class)PostPowerApplySubscriber.class); }
|
总的来说还是遍历。
有人说尖塔不过是一个带动画的计算器而已,这种说法确实有道理,不过有的代码里面还是有惊喜的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| package hermit.actions;
import com.megacrit.cardcrawl.actions.AbstractGameAction; import com.megacrit.cardcrawl.actions.common.DrawCardAction; import com.megacrit.cardcrawl.actions.common.EmptyDeckShuffleAction; import com.megacrit.cardcrawl.cards.AbstractCard; import com.megacrit.cardcrawl.characters.AbstractPlayer; import com.megacrit.cardcrawl.core.AbstractCreature; import com.megacrit.cardcrawl.core.Settings; import com.megacrit.cardcrawl.dungeons.AbstractDungeon;
public class LuckDrawAction extends AbstractGameAction { private AbstractPlayer p; private AbstractCard.CardType typeToCheck; private int energy; private int tracker = 0; public LuckDrawAction(int energy) { this.p = AbstractDungeon.player; setValues((AbstractCreature)this.p, (AbstractCreature)this.p); this.actionType = AbstractGameAction.ActionType.CARD_MANIPULATION; this.duration = Settings.ACTION_DUR_MED; this.typeToCheck = AbstractCard.CardType.CURSE; this.energy = energy; } public void update() { if (this.duration == Settings.ACTION_DUR_MED) { if (this.tracker >= this.energy || (this.p.drawPile.isEmpty() && this.p.discardPile.isEmpty()) || this.p.hand.size() >= 10) { this.isDone = true; return; } if (AbstractDungeon.player.hasPower("No Draw")) { AbstractDungeon.player.getPower("No Draw").flash(); this.isDone = true; return; } if (!this.p.drawPile.isEmpty()) { AbstractCard c = AbstractDungeon.player.drawPile.group.get(AbstractDungeon.player.drawPile.size() - 1); if (c.cost > 0) this.tracker += c.cost; addToTop(new LuckDrawAction(this.energy - this.tracker)); addToTop((AbstractGameAction)new DrawCardAction(1)); } else { addToTop(new LuckDrawAction(this.energy - this.tracker)); addToTop((AbstractGameAction)new EmptyDeckShuffleAction()); } this.isDone = true; return; } tickDuration(); } }
|
这里居然能看见递归,也是惊喜了。