本篇教程由作者设定使用 CC BY-NC 协议。
——Intro 介绍——————
该教程的编写基于 2022 Jun 1 发布的1.16-indev版本源码!使用的文本皆为1.16的翻译文本。
在星辉魔法(下列简称AS)的游玩过程中,星能力的升级是一件十分重要的事情,同时也是关键玩法之一。本篇教程将从源码层面分析AS的星能力经验获取,以及给出一些个人向的建议。
由于会涉及到源码,本篇教程更适合略懂一些编程知识的人宝宝阅读。
另外,游戏的随机刻(tick)也会在本教程中被大量运用,所以请先对随机刻了解一下再继续阅读为好。
本篇中会用到的一些数学知识:
floor(x) -> 对 x 向下取整
clamp(x, min, max) -> 对 x 取值,但 x 低于 min 时取 min,大于 max 时取 max
较为硬核的部分我会将提示放在前面,想要理解大概并不需要完全读懂本篇教程,大家按需阅读即可。
——Astral Sorcery Level 星能力等级————
星能力等级在默认情况下最高为40级,当然,也可以通过修改配置文件来进行更改,更改范围为10~100。每一级需求的经验值为150 + 100 * floor(1.2Level)。这也意味着,经验要求有着1.2倍的成长倍率,在最终100级的条件下,需求量会变得十分恐怖。
下面是一些等级的经验要求以及总经验要求。(基于python计算的结果,结果可能与java运行后的数据有所误差,使用格式更适合国人阅读)
等级 | 该等级的经验需求 | 达到该等级的总经验需求(-1) |
1 | 250 | 250 |
5 | 350 | 1350 |
20 | 3250 | 2,0700 |
40 | 12,2550 | 73,8400 |
60 | 469,5750 | 2817,9100 |
80 | 1,8001,9150 | 10,8012,1600 |
100 | 69,0149,7950 | 414,0899,6500 |
关于等级经验的源代码:
private void buildLevelRequirements() {
if (this.totalExpLevelRequired.isEmpty()) {
for (int i = 1; i <= this.levelCap; i++) {
long prev = this.totalExpLevelRequired.getOrDefault(i - 1, 0L);
this.totalExpLevelRequired.put(i, prev + 150L + 100L * MathHelper.floor(Math.pow(1.2F, i)));
}
}
}
(全部源码在这里 -> hellfirepvp.astralsorcery.common.perk.PerkLevelManager.java)
需要注意的是,作者对于单次获取经验的大小设置了限制,无论单次获取的经验为多少,最终获取的经验值不会超过当前等级需求经验的8%。
关于经验获取的源代码:
protected void modifyExp(double exp, PlayerEntity player) {
int currLevel = PerkLevelManager.getLevel(getPerkExp(), player, LogicalSide.SERVER);
if (exp >= 0 && currLevel >= PerkLevelManager.getLevelCap(LogicalSide.SERVER, player)) {
return;
}
long expThisLevel = PerkLevelManager.getExpForLevel(currLevel, player, LogicalSide.SERVER);
long expNextLevel = PerkLevelManager.getExpForLevel(currLevel + 1, player, LogicalSide.SERVER);
long cap = MathHelper.lfloor(((float) (expNextLevel - expThisLevel)) * 0.08F);
if (exp > cap) {
exp = cap;
}
this.perkExp = Math.max(this.perkExp + exp, 0);
}
(全部源码在这里 -> hellfirepvp.astralsorcery.common.data.research.PlayerPerkData.java)
——Vicio 经验获取:御虚座————
首先讲解一个有着比较简单的经验获取机制的星座:御虚座。
在书中,作者向我们介绍的是,御虚座通过环游世界获取经验。那么实际上呢?
其实,御虚座的真正需求是移动,只要是以走动、疾跑、飞行、鞘翅、游泳这五种方式进行移动即可:
每一tick,系统都会计算一遍在该tick中玩家通过以上方式进行移动的直线距离(单位:块)。
之后,将每项的距离乘上每项的系数,再将五项的计算结果相加后依次乘上0.02和星能力获取倍率即是最后获得的经验值。
移动方式 | 走动 | 疾跑 | 飞行 | 鞘翅 | 游泳 |
经验获取系数 | 0.9 | 0.8 | 0.3 | 0.55 | 1.2 |
需要注意的是,每一项的单次计算上限为500格,超过500格的均按照500格来计算。
关于御虚座经验获取的源代码:
@Override
public void onPlayerTick(PlayerEntity player, LogicalSide side) {
if (!side.isServer() || !(player instanceof ServerPlayerEntity)) {
return;
}
UUID uuid = player.getUniqueID();
ServerPlayerEntity sPlayer = (ServerPlayerEntity) player;
PlayerProgress prog = ResearchHelper.getProgress(player, side);
StatisticsManager mgr = sPlayer.getStats();
int walked = mgr.getValue(Stats.CUSTOM.get(Stats.WALK_ONE_CM));
int sprint = mgr.getValue(Stats.CUSTOM.get(Stats.SPRINT_ONE_CM));
int flown = mgr.getValue(Stats.CUSTOM.get(Stats.FLY_ONE_CM));
int elytra = mgr.getValue(Stats.CUSTOM.get(Stats.AVIATE_ONE_CM));
int swam = mgr.getValue(Stats.CUSTOM.get(Stats.SWIM_ONE_CM));
int lastWalked = this.moveTrackMap.computeIfAbsent(Stats.WALK_ONE_CM, s -> new HashMap<>()).computeIfAbsent(uuid, u -> walked);
int lastSprint = this.moveTrackMap.computeIfAbsent(Stats.SPRINT_ONE_CM, s -> new HashMap<>()).computeIfAbsent(uuid, u -> sprint);
int lastFly = this.moveTrackMap.computeIfAbsent(Stats.FLY_ONE_CM, s -> new HashMap<>()).computeIfAbsent(uuid, u -> flown);
int lastElytra = this.moveTrackMap.computeIfAbsent(Stats.AVIATE_ONE_CM, s -> new HashMap<>()).computeIfAbsent(uuid, u -> elytra);
int lastSwam = this.moveTrackMap.computeIfAbsent(Stats.SWIM_ONE_CM, s -> new HashMap<>()).computeIfAbsent(uuid, u -> swam);
float added = 0;
if (walked > lastWalked) {
added += Math.min(walked - lastWalked, 500F);
if (added >= 500F) {
added = 500F;
}
added *= 0.9F;
this.moveTrackMap.get(Stats.WALK_ONE_CM).put(uuid, walked);
}
if (sprint > lastSprint) {
added += Math.min(sprint - lastSprint, 500F);
if (added >= 500F) {
added = 500F;
}
added *= 0.8F;
this.moveTrackMap.get(Stats.SPRINT_ONE_CM).put(uuid, sprint);
}
if (flown > lastFly) {
added += Math.min(flown - lastFly, 500F);
added *= 0.3F;
this.moveTrackMap.get(Stats.FLY_ONE_CM).put(uuid, flown);
}
if (elytra > lastElytra) {
added += Math.min(elytra - lastElytra, 500F);
added *= 0.55F;
this.moveTrackMap.get(Stats.AVIATE_ONE_CM).put(uuid, elytra);
}
if (swam > lastSwam) {
added += Math.min(swam - lastSwam, 500F);
added *= 1.2F;
this.moveTrackMap.get(Stats.SWIM_ONE_CM).put(uuid, swam);
}
if (added > 0) {
added *= 0.02F;
added *= this.getExpMultiplier();
added *= this.getDiminishingReturns(player);
added *= PerkAttributeHelper.getOrCreateMap(player, side).getModifier(player, prog, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EFFECT);
added *= PerkAttributeHelper.getOrCreateMap(player, side).getModifier(player, prog, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EXP);
added = AttributeEvent.postProcessModded(player, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EXP, added);
ResearchManager.modifyExp(player, added);
}
}
(全部源码在这里 -> hellfirepvp.astralsorcery.common.perk.node.root.RootVicio.java)
——Diminishing Multiplier 万恶之源:递减倍率器————
再继续向下介绍另外四个星座之前,我需要先介绍一位重量级角色,让我们获取经验变得无比难受的狠人:递减倍率器。
在游玩过程中,大家可能遇见过这样的问题:明明我都挖了几个区块了,怎么才升了一级/为什么感觉刷经验的时候刷的效率越来越低……究其根本,递减倍率器绝对功不可没。(bushi
在AS中,每个玩家身上都有一个或多个,真的会有人发现其实五个根源星座能都被同时点上吗?用于计算经验获取倍率的递减倍率器(多个星座同时计算时,每个星座都拥有属于自己的独立的递减倍率器),它会在我们做出一些特定行为时(譬如一次性破坏/放置大量方块,一次性造成多次伤害等你是否在寻找:植物魔法?)大幅度降低我们能获取到的经验值。
细心的读者可能已经发现了,在上面一项星座的源码中,有这样一句:
added *= this.getDiminishingReturns(player);
也就是说,最后计算出来的经验结果一定会被乘上这个递减倍率器,而这个代码在五个星座的经验获取源码中全部存在!
所以为了能够了解后续的四个星座,我们需要先了解这个倍率递减器。
首先,它的触发机制是什么呢?这是比较简单的,当玩家在单个tick内做出多次获取经验的行为时,该递减器就会被激活。
例如使用AS自带的挖掘范围增加的稿子进行挖掘时,除了第一个被计算的方块,其他的每个方块都会触发一次递减器。
每次触发递减器后,AS都会将经验获取倍率减去一个固定的数:递减率dropRate,并且设定了最小值 min。这样,随着单tick内被挖掘的方块不断被计算,后续被计算的方块获得的经验越低。
那么问题就来了,我要怎么样才能把我掉下来的经验获取倍率提升上去呢?很简单,只要在单tick内不触发多次获取经验即可。
当玩家不触发单tick多次获取经验,而是单tick获取单次经验的话,经验获取倍率会按照以下公式增长,最大不超过1:
其中恢复倍率、恢复时间都是预设好的,而额外恢复倍率会随着每次单tick获取单次经验的行为逐渐加1,最大值为4,但触发单tick多次获取经验后会立即归一。
了解了它的运行机制后,我们就能够对症下药,使我们刷经验变得更加科学!
下面则是五个星座的递减倍率器的预设值:
星座 | 递减率 dropRate | 恢复时间 gainMsTime | 恢复倍率 gainRate | 最小值 min |
解离座 | 0.005 | 1000ms | 0.1 | 0.15 |
甲御座 | 0.2 | 2000ms | 0.3 | 0.01 |
攻烈座 | 0.025 | 6000ms | 0.075 | 0.15 |
生息座 | 1 | 600ms | 1 | 0.01 |
御虚座 | 0.003 | 10000ms | 0.065 | 0.2 |
那么问题来了?明明御虚座也有这个机制,那为什么笔者会先讲御虚座而不先介绍递减倍率器呢?
其实很简单,递减倍率器的触发条件是单tick内多次获取经验,而御虚座的经验获取是恒定的一tick更新一次,正常情况下并不会被递减倍率器所影响,所以也就不需要先了解倍率递减器了。
关于递减倍率器的源码:
(从这里可以看出,其实真正判断是否形成获取多次经验行为的基准其实是在一毫秒内的,但是由于笔者并不知道mc中的触发器是否是无条件唤起,所以默认为单tick循环处理时唤起,这样单tick内触发的事件几乎都会被视为单毫秒内触发,即使存在误差,影响也很小,比如获取经验间隔为1ms时,虽然不会扣掉倍率,但能恢复的倍率其实寥寥无几,几乎为0)
private void recalcMultiplier() {
long now = System.currentTimeMillis();
long diff = now - this.lastGain;
long times = (diff * (this.recoveryStack + 1)) / this.gainMsTime;
if (times > 0) {
this.lastGain = now;
this.recoveryStack = Math.min(this.recoveryStack + 1, 3);
this.multiplier = MathHelper.clamp(this.multiplier + times * gainRate, this.min, 1F);
} else {
this.multiplier = Math.max(this.multiplier - this.dropRate, this.min);
this.recoveryStack = 0;
}
}
(全部源码在这里:hellfirepvp.astralsorcery.common.util.DiminishingMultiplier.java)
——Armara 经验获取:甲御座————
下面来介绍下关于甲御座的经验获取。
在星辉辞典中,是这样描述它的经验获取的:通过承受伤害来获取经验。
此言不假,但是其内部机制却比它所介绍的略微复杂:
在受到伤害时,它会以以下判断形式来计算最终获得的经验倍率:
最后获得的经验值=受到伤害的总量(getAmount)*经验获取倍率(mul)*星能力经验获取倍率。
这时可能会有人问了,为什么最后计算时没有带上可爱的递减倍率器呢?
这是因为原版mc中存在无敌帧机制,即玩家在受击后的一定时间内是不会再次受到伤害的,而递减倍率器的触发条件为单tick多次获取经验。由此不难知晓,正常情况下,玩家是无法触发单tick受击多次的,也就不会被递减倍率器影响到经验获取效率。盖亚三:在想我的事情?
关于甲御座经验获取的源码:
private void onHurt(LivingHurtEvent event) {
if (!(event.getEntityLiving() instanceof PlayerEntity)) {
return;
}
PlayerEntity player = (PlayerEntity) event.getEntityLiving();
LogicalSide side = this.getSide(player);
if (!side.isServer()) {
return;
}
PlayerProgress prog = ResearchHelper.getProgress(player, side);
if (!prog.getPerkData().hasPerkEffect(this)) {
return;
}
float mul = 0.5F;
CombatTracker combat = player.getCombatTracker();
if (combat.inCombat) {
//noone is this long in combat...
if (combat.getCombatDuration() <= (4 * 60 * 20)) {
mul = 10.0F;
} else {
mul = 0.05F;
}
} else if (event.getSource().getTrueSource() instanceof LivingEntity) {
mul = 3.0F;
}
float expGain = Math.min(event.getAmount() * mul, 70F);
expGain *= this.getExpMultiplier();
expGain *= this.getDiminishingReturns(player);
expGain *= PerkAttributeHelper.getOrCreateMap(player, side).getModifier(player, prog, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EFFECT);
expGain *= PerkAttributeHelper.getOrCreateMap(player, side).getModifier(player, prog, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EXP);
expGain = AttributeEvent.postProcessModded(player, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EXP, expGain);
ResearchManager.modifyExp(player, expGain);
}
(全部源码在这里 -> hellfirepvp.astralsorcery.common.perk.node.root.RootArmara.java)
——Discidia 经验获取:攻烈座——
接下来是甲御座的反面:攻烈座啦!
攻烈座的经验获取机制其实比较简单,即只要玩家处于战斗状态的时间小于2分钟(2400刻)内,基础倍率则为4,一旦大于2分钟,则基础倍率变为0.01。
最终获得的经验值为:单次造成伤害*基础倍率*星能力经验获取倍率*递减倍率器
这也就解释了为什么一直在刷怪塔挂机,后续获得的经验会变得很少:等到挂机两分钟后,在机制和递减倍率器的双重制裁下,其经验获取会被压至原有的万分之一,自然不会有更高的效率了。
关于攻烈座经验获取的源码:
private void onDamage(LivingDamageEvent event) {
DamageSource ds = event.getSource();
PlayerEntity player = null;
if (ds.getImmediateSource() != null &&
ds.getImmediateSource() instanceof PlayerEntity) {
player = (PlayerEntity) ds.getImmediateSource();
}
if (player == null && ds.getTrueSource() != null &&
ds.getTrueSource() instanceof PlayerEntity) {
player = (PlayerEntity) ds.getTrueSource();
}
if (player == null) {
return;
}
LogicalSide side = this.getSide(player);
if (!side.isServer()) {
return;
}
PlayerProgress prog = ResearchHelper.getProgress(player, side);
if (!prog.getPerkData().hasPerkEffect(this)) {
return;
}
float mul = 4.0F;
CombatTracker combat = event.getEntityLiving().getCombatTracker();
if (combat.inCombat) {
if (combat.getCombatDuration() > (2 * 60 * 20)) {
mul = 0.01F;
}
}
float expGain = Math.min(event.getAmount() * mul, 100F);
expGain *= this.getExpMultiplier();
expGain *= this.getDiminishingReturns(player);
expGain *= PerkAttributeHelper.getOrCreateMap(player, side).getModifier(player, prog, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EFFECT);
expGain *= PerkAttributeHelper.getOrCreateMap(player, side).getModifier(player, prog, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EXP);
expGain = AttributeEvent.postProcessModded(player, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EXP, expGain);
ResearchManager.modifyExp(player, expGain);
}
(全部源码在这里 -> hellfirepvp.astralsorcery.common.perk.node.root.RootDiscidia.java)
——Aevitas/Evorsio 经验获取:生息座/解离座——
接下来是两个以方块为主来获取经验的星座,同时它们的经验获取也和方块的硬度息息相关。
对于生息座来说,它会在玩家放置方块时这样计算:
本次放置获得经验=clamp(放置的方块硬度,min: 1, max: 25) *4 *星能力经验获取倍率 *递减倍率器
而解离座是这样计算的:
本次破坏获得经验=破坏的方块硬度(获取不到时默认0.5) *星能力经验获取倍率 *递减倍率器
特别的,当玩家破坏的方块属于无法破坏的方块时(例如基岩等),硬度取0。
需要注意的是,由于生息座的递减倍率器的递减倍率为1,即一旦产生单tick多次放置就会造成递减倍率器直接惩罚到最小值0.01,所以使用该星座刷经验时需要慎重斟酌一下是否使用一些非常的手段来放置一大片方块。
下面是一些常见的方块的硬度表:
下界岩 | 冰/浮冰/霜冰/沙子/红沙/灵魂沙/泥土/灵魂土/混凝土粉末 | 沙砾 | 石头 | 圆石/潜影盒 | 末地石/深板岩 | 深板岩圆石 | 黑曜石 |
0.4 | 0.5 | 0.6 | 1.5 | 2 | 3 | 3.5 | 50 |
关于生息座经验获取的源码:
private void onPlace(BlockEvent.EntityPlaceEvent event) {
if (!(event.getEntity() instanceof PlayerEntity)) {
return;
}
PlayerEntity player = (PlayerEntity) event.getEntity();
LogicalSide side = this.getSide(player);
if (!side.isServer()) {
return;
}
PlayerProgress prog = ResearchHelper.getProgress(player, side);
if (!prog.getPerkData().hasPerkEffect(this)) {
return;
}
float hardness;
try {
hardness = Math.max(event.getPlacedBlock().getBlockHardness(event.getWorld(), event.getPos()), 1F);
} catch (Exception exc) {
hardness = 1F;
}
float xp = Math.min(hardness * 4F, 100F);
xp *= this.getExpMultiplier();
xp *= this.getDiminishingReturns(player);
xp *= PerkAttributeHelper.getOrCreateMap(player, side).getModifier(player, prog, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EFFECT);
xp *= PerkAttributeHelper.getOrCreateMap(player, side).getModifier(player, prog, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EXP);
xp = AttributeEvent.postProcessModded(player, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EXP, xp);
ResearchManager.modifyExp(player, xp);
}
(全部源码在这里 -> hellfirepvp.astralsorcery.common.perk.node.root.RootAevitas.java)
关于解离座经验获取的源码:
private void onBreak(BlockEvent.BreakEvent event) {
PlayerEntity player = event.getPlayer();
LogicalSide side = this.getSide(player);
if (!side.isServer()) {
return;
}
PlayerProgress prog = ResearchHelper.getProgress(player, side);
if (!prog.getPerkData().hasPerkEffect(this)) {
return;
}
BlockState broken = event.getState();
IWorld world = event.getWorld();
float gainedExp;
try {
gainedExp = broken.getBlockHardness(world, event.getPos());
} catch (Exception exc) {
gainedExp = 0.5F;
}
if (gainedExp < 0) {
return; //Unbreakable
}
gainedExp *= this.getExpMultiplier();
gainedExp *= this.getDiminishingReturns(player);
gainedExp *= PerkAttributeHelper.getOrCreateMap(player, side).getModifier(player, prog, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EFFECT);
gainedExp *= PerkAttributeHelper.getOrCreateMap(player, side).getModifier(player, prog, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EXP);
gainedExp = AttributeEvent.postProcessModded(player, PerkAttributeTypesAS.ATTR_TYPE_INC_PERK_EXP, gainedExp);
ResearchManager.modifyExp(player, gainedExp);
}
(全部源码在这里 -> hellfirepvp.astralsorcery.common.perk.node.root.RootEvorsio.java)
下面的部分较为硬核,需要一定的编程功底才能够有效理解,请斟酌阅读以免浪费时间。
——PerkModifier 额外的控制模块——
细心的读者应该发现了,每一项星座的源码里面,最后计算系数的时候总是会额外乘上两个系数。
那么这两个系数是什么呢?我又为何没有讲呢?
这是因为根据我的研究,这个是关于一个修饰器的模块,其本身非常大,再加之作者并未添加注释,导致笔者很难去整体地理解它。
但是能够弄清的是,在默认条件下,这两个系数地计算结果只会为1,只有当其他模块对其玩家的PerkModifier模块进行修改后,它才会根据设定的value值进行一些比较复杂的计算。
一旦内部存在设定好的大于1的value,基本上会产生基于多个value的2-3指数倍的增长,所以一般来讲,作者也确实不会让玩家一直以一个二十多倍的获取倍率几点几点地慢慢刷,其他的模块定然会对其有所修改,但在哪里修改的话,估计也只有作者他自己知道了。
这里笔者就不再向下分析了,感兴趣的读者可以去这里查看研究 -> hellfirepvp.astralsorcery.common.perk
——??? 未曾设想的道路——
知道了递减倍率器这个东西之后,你是否感到被这个东西羞辱了呢?
如果你的回答是是,那么恭喜你,笔者会向你提供一条未曾设想的道路。
在上面我们已经知道,其惩罚机制事实上只会在同一毫秒内进行多次经验增加才会触发,如果我们能将每次经验增加的时间错开就好了。
那么要怎么做呢?其实每次的经验增加都被设为了一次event,要走一次eventBus,所以如果我们能让eventBus拥堵起来,使两次经验增加的event隔离开来岂不是就能够达到我们想要的效果了?
没错,只要我们将游戏的随机刻堵塞下来,就能够大幅度避免这样的惩罚。
但是,怎么才能让随机刻拥堵呢?所以,咳咳,你懂的