v3.3.0: polish to skeleton key mechanics and visuals
This commit is contained in:
@@ -429,6 +429,7 @@ items.artifacts.skeletonkey.ac_insert=INSERT
|
||||
items.artifacts.skeletonkey.cursed=You can't use a cursed skeleton key.
|
||||
items.artifacts.skeletonkey.prompt=Choose a target
|
||||
items.artifacts.skeletonkey.invalid_target=There's nothing to lock or unlock there.
|
||||
items.artifacts.skeletonkey.lock_no_space=You can't lock a character inside of a door!
|
||||
items.artifacts.skeletonkey.iron_charges=Opening a regular lock requires 1 charge.
|
||||
items.artifacts.skeletonkey.gold_charges=Opening a gold lock requires 2 charges.
|
||||
items.artifacts.skeletonkey.lock_charges=Locking a door requires 2 charges.
|
||||
@@ -438,9 +439,11 @@ items.artifacts.skeletonkey.wont_open=The key refuses to fit into this lock for
|
||||
items.artifacts.skeletonkey.locked_with_key=That door was locked by your skeleton key.
|
||||
items.artifacts.skeletonkey.force_lock=The lock has weakened without your skeleton key. You manage to force it open.
|
||||
items.artifacts.skeletonkey.discard=You discard your excess keys.
|
||||
items.artifacts.skeletonkey.levelup=Your skeleton key grows stronger!
|
||||
items.artifacts.skeletonkey.desc=An important-looking key with a head shaped like a skull. Its teeth seem to be different every time you look at them.
|
||||
items.artifacts.skeletonkey.desc_worn=This magical key seems to gain power as you explore the dungeon and open more locks.\n\nThe key can be inserted into any of the dungeon's locks to either open or close them. This includes most special locks and doors that were never locked previously. The key can even be used on empty space in a cardinal or diagonal direction to 'lock' the air in front of you, creating a temporary solid wall. Locking will push away enemies if there is space to do so.
|
||||
items.artifacts.skeletonkey.desc_worn=This magical key seems to gain power as you use it instead of the dungeon's regular keys. It can be inserted into almost any of the dungeon's locks to either open or close them, including doors that were never locked previously.\n\nThe key can even be used on empty space in a cardinal or diagonal direction to 'lock' the air in front of you, creating a 3 tile wide solid wall that lasts for 10 turns. Locking will push away enemies if there is space to do so.
|
||||
items.artifacts.skeletonkey.desc_cursed=The cursed key seems to be actively avoiding fitting into any lock, and is even making it difficult (but not impossible) for you to use your other keys.
|
||||
items.artifacts.skeletonkey$keywall.desc=A temporary magical wall has been placed here by the skeleton key.
|
||||
|
||||
items.artifacts.talismanofforesight.name=talisman of foresight
|
||||
items.artifacts.talismanofforesight.ac_scry=SCRY
|
||||
|
||||
@@ -2359,9 +2359,6 @@ public class Hero extends Char {
|
||||
hasKey = Notes.remove(new IronKey(Dungeon.depth));
|
||||
if (hasKey) {
|
||||
Level.set(doorCell, Terrain.DOOR);
|
||||
if (skele != null && !skele.isCursed()){
|
||||
skele.keyUsed(new IronKey(Dungeon.depth));
|
||||
}
|
||||
}
|
||||
} else if (door == Terrain.HERO_LKD_DR) {
|
||||
hasKey = true;
|
||||
@@ -2373,17 +2370,11 @@ public class Hero extends Char {
|
||||
Level.set(doorCell, Terrain.EMPTY);
|
||||
Sample.INSTANCE.play(Assets.Sounds.TELEPORT);
|
||||
CellEmitter.get( doorCell ).start( Speck.factory( Speck.DISCOVER ), 0.025f, 20 );
|
||||
if (skele != null && !skele.isCursed()){
|
||||
skele.keyUsed(new CrystalKey(Dungeon.depth));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hasKey = Notes.remove(new WornKey(Dungeon.depth));
|
||||
if (hasKey) {
|
||||
Level.set(doorCell, Terrain.UNLOCKED_EXIT);
|
||||
if (skele != null && !skele.isCursed()){
|
||||
skele.keyUsed(new WornKey(Dungeon.depth));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2411,14 +2402,8 @@ public class Hero extends Char {
|
||||
Sample.INSTANCE.play( Assets.Sounds.BONES );
|
||||
} else if (heap.type == Type.LOCKED_CHEST){
|
||||
hasKey = Notes.remove(new GoldenKey(Dungeon.depth));
|
||||
if (hasKey && skele != null && !skele.isCursed()){
|
||||
skele.keyUsed(new GoldenKey(Dungeon.depth));
|
||||
}
|
||||
} else if (heap.type == Type.CRYSTAL_CHEST){
|
||||
hasKey = Notes.remove(new CrystalKey(Dungeon.depth));
|
||||
if (hasKey && skele != null && !skele.isCursed()){
|
||||
skele.keyUsed(new CrystalKey(Dungeon.depth));
|
||||
}
|
||||
}
|
||||
|
||||
if (hasKey) {
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Pixel Dungeon
|
||||
* Copyright (C) 2012-2015 Oleg Dolya
|
||||
*
|
||||
* Shattered Pixel Dungeon
|
||||
* Copyright (C) 2014-2025 Evan Debenham
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package com.shatteredpixel.shatteredpixeldungeon.effects.particles;
|
||||
|
||||
import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
|
||||
import com.watabou.noosa.particles.Emitter;
|
||||
import com.watabou.noosa.particles.PixelParticle;
|
||||
import com.watabou.utils.ColorMath;
|
||||
import com.watabou.utils.Random;
|
||||
|
||||
public class SpectralWallParticle extends PixelParticle {
|
||||
|
||||
public static final Emitter.Factory FACTORY = new Emitter.Factory() {
|
||||
@Override
|
||||
public void emit( Emitter emitter, int index, float x, float y ) {
|
||||
//scale frequency roughly to the size of the bricks used
|
||||
int type = 1 + Dungeon.depth/5;
|
||||
if (type > 5) type = 5;
|
||||
|
||||
switch (type){
|
||||
case 1:
|
||||
if (Random.Int(2) != 0) return;
|
||||
break;
|
||||
case 2:
|
||||
if (Random.Int(3) != 0) return;
|
||||
break;
|
||||
case 3:
|
||||
break;
|
||||
case 4:
|
||||
break;
|
||||
case 5:
|
||||
if (Random.Int(4) != 0) return;
|
||||
break;
|
||||
}
|
||||
|
||||
((SpectralWallParticle)emitter.recycle( SpectralWallParticle.class )).reset( x, y );
|
||||
}
|
||||
@Override
|
||||
public boolean lightMode() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
private int type = 0; //1-5 for sewers - demon halls
|
||||
|
||||
public SpectralWallParticle() {
|
||||
super();
|
||||
|
||||
lifespan = 4f;
|
||||
|
||||
am = 0.6f;
|
||||
}
|
||||
|
||||
public void reset( float x, float y ) {
|
||||
revive();
|
||||
|
||||
type = 1 + Dungeon.depth/5;
|
||||
if (type > 5) type = 5;
|
||||
|
||||
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
left = lifespan;
|
||||
|
||||
switch (type){
|
||||
case 1:
|
||||
this.x = Math.round(x/7)*7;
|
||||
this.y = Math.round(y/4)*4 - 6;
|
||||
this.x += Math.round(this.y % 8)/4f - 1;
|
||||
color(ColorMath.random(0xD4D4D4, 0xABABAB));
|
||||
break;
|
||||
case 2:
|
||||
this.x = Math.round(x/7)*7;
|
||||
this.y = Math.round(y/6)*6 - 6;
|
||||
this.x += Math.round(this.y % 8)/4f - 1;
|
||||
color(ColorMath.random(0xc4be9c, 0x9c927d));
|
||||
break;
|
||||
case 3:
|
||||
this.y -= 6;
|
||||
float colorScale = (this.x%16)/16 + (this.y%16)/16;
|
||||
if (colorScale > 1f) colorScale = 2f - colorScale;
|
||||
color(ColorMath.interpolate(0xb7b0a5, 0x6a6662, colorScale));
|
||||
break;
|
||||
case 4:
|
||||
this.x = Math.round(x/4)*4;
|
||||
this.y = Math.round(y/4)*4 - 6;
|
||||
this.x += Math.round(this.y % 16)/4f - 2;
|
||||
color(ColorMath.interpolate(0xd0bca3, 0xa38d81));
|
||||
break;
|
||||
case 5:
|
||||
this.x = Math.round(x/4)*4;
|
||||
this.y = Math.round((y+8)/16)*16 - 14;
|
||||
color(ColorMath.interpolate(0xa2947d, 0x594847));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void update(){
|
||||
super.update();
|
||||
|
||||
float sizeFactor = (left / lifespan);
|
||||
|
||||
switch (type){
|
||||
case 1:
|
||||
scale.set(sizeFactor*6, sizeFactor*3);
|
||||
break;
|
||||
case 2:
|
||||
scale.set(sizeFactor*6, sizeFactor*5);
|
||||
break;
|
||||
case 3:
|
||||
scale.set(sizeFactor*4, sizeFactor*4);
|
||||
break;
|
||||
case 4:
|
||||
scale.set(sizeFactor*3, sizeFactor*3);
|
||||
break;
|
||||
case 5:
|
||||
scale.set(sizeFactor*4, sizeFactor*20);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,15 +32,13 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Regeneration;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.effects.BlobEmitter;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.effects.MagicMissile;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.effects.Speck;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.effects.particles.SpectralWallParticle;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.items.Heap;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.items.Item;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.items.keys.CrystalKey;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.items.keys.GoldenKey;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.items.keys.IronKey;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.items.keys.Key;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.items.keys.WornKey;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfEnergy;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.items.wands.WandOfBlastWave;
|
||||
import com.shatteredpixel.shatteredpixeldungeon.journal.Notes;
|
||||
@@ -60,17 +58,16 @@ import com.watabou.utils.PathFinder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
//TODO needs a way to handle the excess keys it can general
|
||||
public class SkeletonKey extends Artifact {
|
||||
|
||||
{
|
||||
image = ItemSpriteSheet.ARTIFACT_KEY;
|
||||
|
||||
levelCap = 5;
|
||||
levelCap = 10;
|
||||
|
||||
charge = 3+level();
|
||||
charge = 3+level()/2;
|
||||
partialCharge = 0;
|
||||
chargeCap = 3+level();
|
||||
chargeCap = 3+level()/2;
|
||||
|
||||
defaultAction = AC_INSERT;
|
||||
}
|
||||
@@ -111,6 +108,21 @@ public class SkeletonKey extends Artifact {
|
||||
}
|
||||
}
|
||||
|
||||
//levels when used, with bonus xp for opening locks that could be opened with keys
|
||||
public void gainExp( int xpGain ){
|
||||
if (level() == levelCap){
|
||||
return;
|
||||
}
|
||||
|
||||
exp += xpGain;
|
||||
if (exp > 4+level()){
|
||||
exp -= 4+level();
|
||||
upgrade();
|
||||
GLog.p(Messages.get(this, "levelup"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public CellSelector.Listener targeter = new CellSelector.Listener(){
|
||||
|
||||
@Override
|
||||
@@ -145,6 +157,7 @@ public class SkeletonKey extends Artifact {
|
||||
Level.set(target, Terrain.DOOR);
|
||||
GameScene.updateMap(target);
|
||||
charge -= 1;
|
||||
gainExp(2 + 1);
|
||||
curUser.spendAndNext(Actor.TICK);
|
||||
curUser.sprite.idle();
|
||||
}
|
||||
@@ -181,6 +194,7 @@ public class SkeletonKey extends Artifact {
|
||||
Level.set(target, Terrain.EMPTY);
|
||||
GameScene.updateMap(target);
|
||||
charge -= 5;
|
||||
gainExp(2 + 5);
|
||||
Sample.INSTANCE.play(Assets.Sounds.TELEPORT);
|
||||
CellEmitter.get( target ).start( Speck.factory( Speck.DISCOVER ), 0.025f, 20 );
|
||||
curUser.spendAndNext(Actor.TICK);
|
||||
@@ -198,11 +212,26 @@ public class SkeletonKey extends Artifact {
|
||||
|
||||
//attempt to knock back char
|
||||
if (Actor.findChar(target) != null){
|
||||
int pushDIR = target - curUser.pos;
|
||||
Ballistica push = new Ballistica(target, target + pushDIR, Ballistica.PROJECTILE);
|
||||
WandOfBlastWave.throwChar(Actor.findChar(target), push, 1, false, false, this);
|
||||
//TODO what about pushing to the side?
|
||||
//TODO fail if there's no push DIR?
|
||||
|
||||
int pushCell = -1;
|
||||
//push to the closest open cell that's further than the door
|
||||
for (int i : PathFinder.NEIGHBOURS8){
|
||||
if (!Dungeon.level.solid[target+i] && Actor.findChar(target+i) == null){
|
||||
if (Dungeon.level.trueDistance(curUser.pos, target+i) > Dungeon.level.trueDistance(curUser.pos, target)) {
|
||||
if (pushCell == -1 || Dungeon.level.trueDistance(curUser.pos, pushCell) > Dungeon.level.trueDistance(curUser.pos, target + i)){
|
||||
pushCell = target + i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pushCell != -1){
|
||||
Ballistica push = new Ballistica(target, pushCell, Ballistica.PROJECTILE);
|
||||
WandOfBlastWave.throwChar(Actor.findChar(target), push, 1, false, false, this);
|
||||
} else {
|
||||
GLog.w(Messages.get(SkeletonKey.class, "lock_no_space"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Sample.INSTANCE.play(Assets.Sounds.UNLOCK);
|
||||
@@ -212,6 +241,7 @@ public class SkeletonKey extends Artifact {
|
||||
Level.set(target, Terrain.HERO_LKD_DR);
|
||||
GameScene.updateMap(target);
|
||||
charge -= 2;
|
||||
gainExp(2);
|
||||
curUser.spendAndNext(Actor.TICK);
|
||||
curUser.sprite.idle();
|
||||
}
|
||||
@@ -231,6 +261,7 @@ public class SkeletonKey extends Artifact {
|
||||
Buff.affect(curUser, KeyReplacementTracker.class).processGoldLockOpened();
|
||||
Dungeon.level.heaps.get(target).open(curUser);
|
||||
charge -= 2;
|
||||
gainExp(2 + 2);
|
||||
curUser.spendAndNext(Actor.TICK);
|
||||
curUser.sprite.idle();
|
||||
}
|
||||
@@ -250,6 +281,7 @@ public class SkeletonKey extends Artifact {
|
||||
Buff.affect(curUser, KeyReplacementTracker.class).processCrystalLockOpened();
|
||||
Dungeon.level.heaps.get(target).open(curUser);
|
||||
charge -= 5;
|
||||
gainExp(2 + 5);
|
||||
curUser.spendAndNext(Actor.TICK);
|
||||
curUser.sprite.idle();
|
||||
}
|
||||
@@ -299,6 +331,7 @@ public class SkeletonKey extends Artifact {
|
||||
}
|
||||
|
||||
charge -= 2;
|
||||
gainExp(2);
|
||||
|
||||
Dungeon.observe();
|
||||
GameScene.updateFog();
|
||||
@@ -327,7 +360,17 @@ public class SkeletonKey extends Artifact {
|
||||
|
||||
@Override
|
||||
public void charge(Hero target, float amount) {
|
||||
super.charge(target, amount); //TODO
|
||||
if (charge < chargeCap && !cursed && target.buff(MagicImmune.class) == null){
|
||||
partialCharge += 0.133f*amount;
|
||||
while (partialCharge >= 1){
|
||||
partialCharge--;
|
||||
charge++;
|
||||
}
|
||||
if (charge >= chargeCap){
|
||||
partialCharge = 0;
|
||||
}
|
||||
updateQuickslot();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -338,7 +381,7 @@ public class SkeletonKey extends Artifact {
|
||||
if (cursed){
|
||||
desc += "\n\n" + Messages.get(this, "desc_cursed");
|
||||
} else {
|
||||
desc += "\n\n" + Messages.get(this, "desc_worn"); //TODO probably want more info on making walls
|
||||
desc += "\n\n" + Messages.get(this, "desc_worn");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,33 +416,18 @@ public class SkeletonKey extends Artifact {
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO either finish this or think of a different levelling mechanic
|
||||
public void keyUsed(Key key ){
|
||||
if (level() == levelCap){
|
||||
return;
|
||||
}
|
||||
|
||||
if (key instanceof IronKey){
|
||||
exp += 3;
|
||||
} else if (key instanceof WornKey){
|
||||
exp += 5;
|
||||
} else {
|
||||
exp += 2;
|
||||
}
|
||||
|
||||
if (exp >= 5 + 5*level()){
|
||||
exp -= 5 + 5*level();
|
||||
upgrade();
|
||||
}
|
||||
|
||||
}
|
||||
@Override
|
||||
public Item upgrade() {
|
||||
chargeCap = 3 + (level()+1)/2;
|
||||
return super.upgrade();
|
||||
}
|
||||
|
||||
private void placeWall(int pos, int knockbackDIR ){
|
||||
if (!Dungeon.level.solid[pos]) { //TODO this prevents wall stacking
|
||||
//TODO 10 or 20 turns?
|
||||
GameScene.add(Blob.seed(pos, 20, KeyWall.class));
|
||||
Blob wall = Dungeon.level.blobs.get(KeyWall.class);
|
||||
if (!Dungeon.level.solid[pos] || (wall != null && wall.cur[pos] > 0)) {
|
||||
GameScene.add(Blob.seed(pos, 10, KeyWall.class));
|
||||
|
||||
Char ch = Actor.findChar(pos);
|
||||
if (ch != null && ch.alignment == Char.Alignment.ENEMY){
|
||||
@@ -430,9 +458,8 @@ public class SkeletonKey extends Artifact {
|
||||
cellEnded = true;
|
||||
}
|
||||
|
||||
//caps at 20
|
||||
//TODO or just one wall at a time?
|
||||
off[cell] = Math.min(off[cell], 19);
|
||||
//caps at 10 turns
|
||||
off[cell] = Math.min(off[cell], 9);
|
||||
|
||||
volume += off[cell];
|
||||
|
||||
@@ -489,7 +516,7 @@ public class SkeletonKey extends Artifact {
|
||||
@Override
|
||||
public void use(BlobEmitter emitter) {
|
||||
super.use( emitter );
|
||||
emitter.pour( MagicMissile.WhiteParticle.WALL, 0.02f ); //TODO
|
||||
emitter.pour(SpectralWallParticle.FACTORY, 0.02f );
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user