diff --git a/Server/src/main/java/core/game/node/entity/npc/revenant/RevenantNPC.java b/Server/src/main/java/core/game/node/entity/npc/revenant/RevenantNPC.java index 80ae977a5..cbc0d6886 100644 --- a/Server/src/main/java/core/game/node/entity/npc/revenant/RevenantNPC.java +++ b/Server/src/main/java/core/game/node/entity/npc/revenant/RevenantNPC.java @@ -15,15 +15,18 @@ import core.game.world.map.path.Pathfinder; import core.game.world.map.zone.ZoneBorders; import core.game.world.map.zone.impl.WildernessZone; import core.game.world.update.flag.context.Animation; +import core.plugin.Initializable; import core.tools.RandomFunction; +import rs09.game.content.zone.wilderness.RevenantController; import rs09.game.node.entity.combat.CombatSwingHandler; import rs09.game.system.config.NPCConfigParser; import rs09.game.world.GameWorld; /** * Handles a revenant NPC. - * @author Vexia + * @author Ceikry-ish (mostly Vexia code still) */ +@Initializable public class RevenantNPC extends AbstractNPC { /** @@ -74,21 +77,21 @@ public class RevenantNPC extends AbstractNPC { setWalks(true); setRespawn(false); setAggressive(true); + setRenderable(true); setDefaultBehavior(); getAggressiveHandler().setRadius(64 * 2); getAggressiveHandler().setChanceRatio(9); getAggressiveHandler().setAllowTolerance(false); getProperties().setCombatTimeOut(120); - configureRoute(); - super.configure(); configureBonuses(); + super.configure(); this.swingHandler = new RevenantCombatHandler(getProperties().getAttackAnimation(), getProperties().getMagicAnimation(), getProperties().getRangeAnimation()); } @Override public void init() { super.init(); - RevenantPlugin.getRevenants().add(this); + RevenantController.registerRevenant(this); int spawnAnim = getDefinition().getConfiguration(NPCConfigParser.SPAWN_ANIMATION, -1); if (spawnAnim != -1) { animate(new Animation(spawnAnim)); @@ -98,8 +101,7 @@ public class RevenantNPC extends AbstractNPC { @Override public void clear() { super.clear(); - RevenantPlugin.getRevenants().remove(this); - RevenantPlugin.spawn(); + RevenantController.unregisterRevenant(this); } @Override @@ -111,7 +113,11 @@ public class RevenantNPC extends AbstractNPC { } @Override - public void handleTickActions() { + public void tick() { + skills.pulse(); + getWalkingQueue().update(); + if (this.getViewport().getRegion().isActive()) + getUpdateMasks().prepare(this); if (!DeathTask.isDead(this) && getSkills().getLifepoints() <= (getSkills().getStaticLevel(Skills.HITPOINTS) / 2) && getAttribute("eat-delay", 0) < GameWorld.getTicks()) { lock(3); getProperties().getCombatPulse().delayNextAttack(3); @@ -121,15 +127,6 @@ public class RevenantNPC extends AbstractNPC { } setAttribute("eat-delay", GameWorld.getTicks() + 6); } - if (!getLocks().isMovementLocked()) { - if (!getPulseManager().hasPulseRunning() && !getProperties().getCombatPulse().isAttacking() && !getProperties().getCombatPulse().isInCombat() && nextWalk < GameWorld.getTicks()) { - setNextWalk(); - Location l = getMovementDestination(); - if (canMove(l)) { - Pathfinder.find(this, l, true, Pathfinder.SMART).walk(this); - } - } - } if (aggressiveHandler != null && aggressiveHandler.selectTarget()) { return; } diff --git a/Server/src/main/java/core/game/node/entity/npc/revenant/RevenantPlugin.java b/Server/src/main/java/core/game/node/entity/npc/revenant/RevenantPlugin.java deleted file mode 100644 index 0e15f2cdd..000000000 --- a/Server/src/main/java/core/game/node/entity/npc/revenant/RevenantPlugin.java +++ /dev/null @@ -1,90 +0,0 @@ -package core.game.node.entity.npc.revenant; - -import java.util.ArrayList; -import java.util.List; - -import core.game.node.entity.npc.NPC; -import core.game.world.map.Location; -import core.plugin.Initializable; -import core.plugin.Plugin; -import rs09.plugin.ClassScanner; -import core.tools.RandomFunction; - -/** - * Handles the revenants. - * @author Vexia - */ -@Initializable -public class RevenantPlugin implements Plugin { - - /** - * The revenants npc. - */ - private static final List REVENANTS = new ArrayList<>(20); - - /** - * The spawning locations. - */ - private static final Location[] SPAWN_LOCATIONS = new Location[] { new Location(2968, 3695), new Location(2956, 3716), new Location(2969, 3753), new Location(2967, 3817), new Location(2976, 3849), new Location(2976, 3725), new Location(2973, 3576), new Location(2982, 3850), new Location(3348, 3822), new Location(3362, 3808), new Location(3026, 3734), new Location(2961, 3792), new Location(2984, 3801), new Location(2974, 3744), new Location(2984, 3718), new Location(2990, 3695), new Location(2960, 3590), new Location(3020, 3674), new Location(3019, 3723), new Location(3019, 3822), Location.create(3123, 3567, 0), Location.create(3123, 3567, 0), Location.create(3201, 3678, 0), Location.create(3220, 3755, 0), Location.create(3249, 3882, 0), Location.create(3283, 3893, 0), Location.create(3034, 3938, 0), Location.create(2964, 3617, 0), Location.create(3253, 3922, 0), Location.create(3205, 3907, 0), Location.create(3194, 3895, 0), Location.create(3168, 3788, 0), Location.create(3287, 3557, 0), Location.create(3327, 3558, 0), Location.create(3364, 3536, 0), Location.create(3262, 3595, 0), Location.create(3099, 3957, 0), Location.create(3028, 3915, 0), Location.create(3285, 3922, 0), Location.create(2980, 3855, 0), Location.create(3243, 3917, 0), Location.create(3190, 3630, 0), Location.create(3188, 3585, 0), Location.create(3117, 3590, 0), Location.create(3136, 3624, 0), Location.create(3356, 3701, 0) }; - - /** - * The maximum amount of revenants spawned. - */ - private static final int MAX = 20; - - @Override - public Plugin newInstance(Object arg) throws Throwable { - ClassScanner.definePlugin(new RevenantNPC()); - //CorruptEquipment.init(); - //PVPEquipment.init(); - spawn(); - return this; - } - - /** - * Spawns the revenants. - */ - public static void spawn() { - int size = REVENANTS.size(); - int left = MAX - size; - List taken = new ArrayList<>(20); - for (NPC n : REVENANTS) { - taken.add(n.getProperties().getSpawnLocation()); - } - if (left > 0) { - int spawnAmount = RandomFunction.random(1, left); - if (size == 0) { - spawnAmount = MAX; - } - for (int i = 0; i < spawnAmount; i++) { - Location loc = null; - while (loc == null) { - Location l = RandomFunction.getRandomElement(SPAWN_LOCATIONS); - if (taken.contains(l)) { - continue; - } - loc = l; - } - taken.add(loc); - RevenantType type = RandomFunction.getRandomElement(RevenantType.values()); - int id = type.getIds()[0]; - NPC revenant = NPC.create(id, loc); - revenant.init(); - } - } - } - - @Override - public Object fireEvent(String identifier, Object... args) { - return null; - } - - /** - * Gets the revenants. - * @return the revenants - */ - public static List getRevenants() { - return REVENANTS; - } - -} diff --git a/Server/src/main/java/core/tools/RandomFunction.java b/Server/src/main/java/core/tools/RandomFunction.java index 1170c304f..a162c25e6 100644 --- a/Server/src/main/java/core/tools/RandomFunction.java +++ b/Server/src/main/java/core/tools/RandomFunction.java @@ -293,4 +293,8 @@ public class RandomFunction { } return RANDOM.nextInt(value); } + + public static boolean nextBool() { + return RANDOM.nextBoolean(); + } } diff --git a/Server/src/main/kotlin/rs09/ServerConstants.kt b/Server/src/main/kotlin/rs09/ServerConstants.kt index a53abe50f..8f32f78a4 100644 --- a/Server/src/main/kotlin/rs09/ServerConstants.kt +++ b/Server/src/main/kotlin/rs09/ServerConstants.kt @@ -13,6 +13,9 @@ import java.math.BigInteger class ServerConstants { companion object { @JvmField + var REVENANT_POPULATION: Int = 30 + + @JvmField var BOTS_INFLUENCE_PRICE_INDEX = true @JvmField diff --git a/Server/src/main/kotlin/rs09/game/content/zone/wilderness/RevenantController.kt b/Server/src/main/kotlin/rs09/game/content/zone/wilderness/RevenantController.kt new file mode 100644 index 000000000..6cedad385 --- /dev/null +++ b/Server/src/main/kotlin/rs09/game/content/zone/wilderness/RevenantController.kt @@ -0,0 +1,239 @@ +package rs09.game.content.zone.wilderness + +import api.Commands +import api.TickListener +import api.poofClear +import api.teleport +import core.game.interaction.MovementPulse +import core.game.node.entity.npc.NPC +import core.game.node.entity.npc.revenant.RevenantNPC +import core.game.node.entity.npc.revenant.RevenantType +import core.game.node.entity.player.link.TeleportManager +import core.game.system.task.Pulse +import core.game.world.map.Location +import core.game.world.map.zone.ZoneBorders +import core.game.world.update.flag.context.Graphics +import core.tools.RandomFunction +import rs09.ServerConstants +import rs09.game.system.SystemLogger +import rs09.game.system.command.Privilege +import rs09.game.world.GameWorld +import rs09.game.world.repository.Repository +import java.util.* +import kotlin.collections.ArrayList +import kotlin.collections.HashMap + +class RevenantController : TickListener, Commands { + + companion object { + private val trackedRevenants = ArrayList() + private val taskTimeRemaining = HashMap() + private val currentTask = HashMap() + private var expectedRevAmount: Int = ServerConstants.REVENANT_POPULATION + private var groupPatrolQueue = ArrayList() + + @JvmStatic fun registerRevenant(revenantNPC: RevenantNPC) { + trackedRevenants.add(revenantNPC) + taskTimeRemaining[revenantNPC] = 0 + currentTask[revenantNPC] = RevenantTask.NONE + Repository.RENDERABLE_NPCS.add(revenantNPC) + } + + @JvmStatic fun unregisterRevenant(revenantNPC: RevenantNPC) { + trackedRevenants.remove(revenantNPC) + taskTimeRemaining.remove(revenantNPC) + currentTask.remove(revenantNPC) + Repository.RENDERABLE_NPCS.remove(revenantNPC) + } + + val routes = listOf( + arrayOf(Location.create(3070, 3651), Location.create(3083, 3640), Location.create(3106, 3645), Location.create(3133, 3647), Location.create(3149, 3642), Location.create(3160, 3654), Location.create(3171, 3665, 0), Location.create(3189, 3663, 0), Location.create(3202, 3675, 0), Location.create(3217, 3660, 0), Location.create(3235, 3661, 0), Location.create(3235, 3661, 0), Location.create(3279, 3650, 0), Location.create(3269, 3636, 0), Location.create(3253, 3632, 0), Location.create(3236, 3638, 0), Location.create(3220, 3637, 0), Location.create(3203, 3634, 0), Location.create(3187, 3631, 0), Location.create(3166, 3633, 0), Location.create(3160, 3616, 0), Location.create(3148, 3604, 0), Location.create(3134, 3596, 0), Location.create(3118, 3590, 0), Location.create(3104, 3597, 0)), + arrayOf(Location.create(3077, 3565, 0), Location.create(3093, 3559, 0), Location.create(3110, 3566, 0), Location.create(3127, 3574, 0), Location.create(3146, 3571, 0), Location.create(3164, 3575, 0), Location.create(3183, 3573, 0), Location.create(3197, 3587, 0), Location.create(3215, 3584, 0), Location.create(3233, 3576, 0), Location.create(3251, 3573, 0), Location.create(3269, 3577, 0), Location.create(3287, 3569, 0), Location.create(3305, 3568, 0), Location.create(3321, 3576, 0), Location.create(3338, 3584, 0), Location.create(3352, 3573, 0), Location.create(3354, 3554, 0), Location.create(3342, 3541, 0), Location.create(3324, 3536, 0), Location.create(3306, 3543, 0), Location.create(3290, 3544, 0), Location.create(3272, 3545, 0), Location.create(3255, 3546, 0), Location.create(3239, 3539, 0), Location.create(3222, 3543, 0), Location.create(3206, 3548, 0), Location.create(3189, 3549, 0), Location.create(3173, 3552, 0), Location.create(3157, 3549, 0), Location.create(3140, 3548, 0), Location.create(3122, 3548, 0), Location.create(3110, 3555, 0)), + arrayOf(Location.create(3318, 3691, 0), Location.create(3307, 3700, 0), Location.create(3290, 3696, 0), Location.create(3277, 3706, 0), Location.create(3260, 3706, 0), Location.create(3250, 3707, 0), Location.create(3245, 3723, 0), Location.create(3254, 3735, 0), Location.create(3251, 3754, 0), Location.create(3243, 3768, 0), Location.create(3253, 3780, 0), Location.create(3238, 3783, 0), Location.create(3224, 3793, 0), Location.create(3206, 3786, 0), Location.create(3192, 3780, 0), Location.create(3170, 3787, 0), Location.create(3156, 3800, 0), Location.create(3148, 3814, 0), Location.create(3148, 3814, 0), Location.create(3127, 3840, 0), Location.create(3124, 3856, 0), Location.create(3124, 3872, 0), Location.create(3116, 3892, 0)), + arrayOf(Location.create(2949, 3890, 0), Location.create(2965, 3899, 0), Location.create(2984, 3900, 0), Location.create(2998, 3895, 0), Location.create(3016, 3898, 0), Location.create(3032, 3893, 0), Location.create(3048, 3897, 0), Location.create(3068, 3894, 0), Location.create(3084, 3898, 0), Location.create(3101, 3895, 0), Location.create(3118, 3897, 0), Location.create(3136, 3893, 0), Location.create(3154, 3900, 0), Location.create(3172, 3895, 0), Location.create(3189, 3892, 0), Location.create(3206, 3897, 0), Location.create(3222, 3890, 0), Location.create(3240, 3897, 0), Location.create(3259, 3892, 0), Location.create(3278, 3895, 0), Location.create(3296, 3892, 0), Location.create(3313, 3899, 0), Location.create(3331, 3888, 0), Location.create(3345, 3880, 0)), + arrayOf(Location.create(3308, 3941, 0), Location.create(3301, 3925, 0), Location.create(3287, 3915, 0), Location.create(3276, 3922, 0), Location.create(3266, 3938, 0), Location.create(3267, 3952, 0), Location.create(3250, 3949, 0), Location.create(3235, 3944, 0), Location.create(3219, 3944, 0), Location.create(3206, 3938, 0), Location.create(3194, 3929, 0), Location.create(3182, 3921, 0), Location.create(3174, 3936, 0), Location.create(3180, 3952, 0), Location.create(3167, 3960, 0), Location.create(3155, 3959, 0), Location.create(3141, 3953, 0), Location.create(3126, 3954, 0), Location.create(3110, 3961, 0), Location.create(3093, 3962, 0), Location.create(3078, 3953, 0), Location.create(3066, 3942, 0), Location.create(3059, 3929, 0), Location.create(3049, 3916, 0), Location.create(3033, 3924, 0), Location.create(3020, 3921, 0), Location.create(3010, 3913, 0), Location.create(2993, 3906, 0), Location.create(2977, 3911, 0), Location.create(2970, 3928, 0)) + ) + + val spawnLocations = listOf(Location.create(3075, 3553, 0), Location.create(3077, 3563, 0), Location.create(3077, 3578, 0), Location.create(3093, 3581, 0), Location.create(3103, 3570, 0), Location.create(3101, 3564, 0), Location.create(3030, 3596, 0), Location.create(3015, 3598, 0), Location.create(3000, 3593, 0), Location.create(2986, 3588, 0), Location.create(2969, 3701, 0), Location.create(2982, 3689, 0), Location.create(2967, 3689, 0), Location.create(2953, 3711, 0), Location.create(2966, 3759, 0), Location.create(2989, 3759, 0), Location.create(2986, 3741, 0), Location.create(2961, 3763, 0), Location.create(2969, 3808, 0), Location.create(3004, 3816, 0)) + } + + override fun tick() { + taskTimeRemaining.replaceAll { _, t -> t - 1 } + currentTask.entries.forEach { entry -> + if (entry.value == RevenantTask.NONE) { + entry.setValue(assignRandomTask(entry.key)) + } else { + entry.value.execute(entry.key) + } + } + spawnMissingRevenants() + } + + private fun spawnMissingRevenants() { + val amountToSpawn = expectedRevAmount - trackedRevenants.size + + if (amountToSpawn <= 0) return + + for (i in 0 until amountToSpawn) { + val type = RevenantType.values().random() + val npc = NPC.create(type.ids[0], getRandomSpawnLocation()) + npc.init() + } + } + + private fun getRandomSpawnLocation(): Location { + return spawnLocations.random() + } + + private fun assignRandomTask(npc: RevenantNPC): RevenantTask { + return RevenantTask.values().random().also { it.assign(npc) } + } + + override fun defineCommands() { + define("setrevcap", Privilege.ADMIN) {player, strings -> + val amt = strings[1].toInt() + expectedRevAmount = amt + } + + define("listrevs", Privilege.ADMIN) {player, strings -> + for (rev in trackedRevenants) { + SystemLogger.logInfo("REV ${rev.id}-${rev.name} @ ${rev.location.toString()}") + } + + SystemLogger.logInfo("Total of ${trackedRevenants.size} revenants spawned.") + } + } + + //The current task for any given revenant - execute is called every tick. + enum class RevenantTask { + NONE { + override fun execute(revenantNPC: RevenantNPC) {} + }, + INTENTIONAL_IDLE { + private val MAX_IDLE_TIME: Int = 50 + + override fun execute(revenantNPC: RevenantNPC) { + if (taskTimeRemaining[revenantNPC] == 0) currentTask[revenantNPC] = NONE + } + + override fun assign(revenantNPC: RevenantNPC) { + taskTimeRemaining[revenantNPC] = RandomFunction.random(MAX_IDLE_TIME) + } + }, + RANDOM_ROAM { + private val MAX_ROAM_TICKS: Int = 250 + + override fun execute(revenantNPC: RevenantNPC) { + if (!canMove(revenantNPC)) return + + revenantNPC.pulseManager.run(object : MovementPulse(revenantNPC, getNextLocation(revenantNPC)) { + override fun pulse(): Boolean { + if (taskTimeRemaining[revenantNPC]!! <= 0) currentTask[revenantNPC] = NONE + return true + } + }) + } + + override fun assign(revenantNPC: RevenantNPC) { + taskTimeRemaining[revenantNPC] = RandomFunction.random(MAX_ROAM_TICKS) + } + + fun canMove(revenantNPC: RevenantNPC) : Boolean { + return !revenantNPC.walkingQueue.isMoving + && !revenantNPC.properties.combatPulse.isAttacking + && !revenantNPC.properties.combatPulse.isInCombat + } + + fun getNextLocation(revenantNPC: RevenantNPC) : Location { + val nextX = RandomFunction.random(-revenantNPC.walkRadius, revenantNPC.walkRadius) + val nextY = RandomFunction.random(-revenantNPC.walkRadius, revenantNPC.walkRadius) + return revenantNPC.location.transform(nextX, nextY, 0) + } + }, + PATROLLING_ROUTE { + private val MAXIMUM_GROUP_PATROL_LEVEL = 105 + + override fun assign(revenantNPC: RevenantNPC) { + if (canGroup(revenantNPC)) { + addToPatrolGroup(revenantNPC) + } else { + revenantNPC.setAttribute("route", routes.random()) + revenantNPC.setAttribute("routeidx", -1) + } + } + + private fun addToPatrolGroup(revenantNPC: RevenantNPC) { + revenantNPC.setAttribute("group", true) + groupPatrolQueue.add(revenantNPC) + + if (groupPatrolQueue.size == 3) { + val groupRoute = routes.random() + for (rev in groupPatrolQueue) { + rev.setAttribute("route", groupRoute) + rev.setAttribute("routeidx", -1) + } + groupPatrolQueue.clear() + } + } + + private fun canGroup(revenantNPC: RevenantNPC) = + revenantNPC.properties.currentCombatLevel <= MAXIMUM_GROUP_PATROL_LEVEL && RandomFunction.nextBool() + + override fun execute(revenantNPC: RevenantNPC) { + val isGroup = revenantNPC.getAttribute("group", false) + val route = revenantNPC.getAttribute>("route", null) + val routeIdx = revenantNPC.getAttribute("routeidx", -1) + + if (!canMove(revenantNPC)) return + + if (isGroup && route == null) { //if this is a grouped rev and we are waiting on more revs still + taskTimeRemaining[revenantNPC] = 50 //just to make sure it doesn't time out roaming... + RANDOM_ROAM.execute(revenantNPC) + return + } + + if (routeIdx == -1) { + GameWorld.Pulser.submit(object : Pulse() { + override fun pulse(): Boolean { + Graphics.send(Graphics(86), revenantNPC.location) + return true + } + }) + teleport(revenantNPC, route[0], TeleportManager.TeleportType.INSTANT) + revenantNPC.setAttribute("routeidx", 1) + } else { + if (routeIdx == route.size) { + poofClear(revenantNPC) + revenantNPC.setAttribute("done", true) + return + } + val pathVariance = if (isGroup) 4 else 10 + val nextLoc = route[routeIdx].transform( + RandomFunction.random(-pathVariance, pathVariance), + RandomFunction.random(-pathVariance, pathVariance), + 0 + ) + revenantNPC.pulseManager.run(object : MovementPulse(revenantNPC, nextLoc) { + override fun pulse(): Boolean { + return true + } + }) + revenantNPC.setAttribute("routeidx", routeIdx + 1) + } + } + + fun canMove(revenantNPC: RevenantNPC) : Boolean { + return !revenantNPC.walkingQueue.isMoving + && !revenantNPC.pulseManager.hasPulseRunning() + && !revenantNPC.properties.combatPulse.isAttacking + && !revenantNPC.properties.combatPulse.isInCombat + && revenantNPC.properties.teleportLocation == null + && !revenantNPC.getAttribute("done", false) + } + } + ; + + abstract fun execute(revenantNPC: RevenantNPC) + open fun assign(revenantNPC: RevenantNPC) {} + } +} \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/game/system/config/ServerConfigParser.kt b/Server/src/main/kotlin/rs09/game/system/config/ServerConfigParser.kt index 5e008145b..97dbbe9ca 100644 --- a/Server/src/main/kotlin/rs09/game/system/config/ServerConfigParser.kt +++ b/Server/src/main/kotlin/rs09/game/system/config/ServerConfigParser.kt @@ -115,6 +115,7 @@ object ServerConfigParser { ServerConstants.SERVER_GE_NAME = data.getString("world.name_ge") ?: ServerConstants.SERVER_NAME ServerConstants.RULES_AND_INFO_ENABLED = data.getBoolean("world.show_rules", true) ServerConstants.BOTS_INFLUENCE_PRICE_INDEX = data.getBoolean("world.bots_influence_ge_price", true) + ServerConstants.REVENANT_POPULATION = data.getLong("world.revenant_population", 30L).toInt() } diff --git a/Server/src/main/kotlin/rs09/game/world/repository/Repository.kt b/Server/src/main/kotlin/rs09/game/world/repository/Repository.kt index fbf02aac7..085bc4271 100644 --- a/Server/src/main/kotlin/rs09/game/world/repository/Repository.kt +++ b/Server/src/main/kotlin/rs09/game/world/repository/Repository.kt @@ -1,6 +1,7 @@ package rs09.game.world.repository import core.game.node.entity.npc.NPC +import core.game.node.entity.npc.revenant.RevenantNPC import core.game.node.entity.player.Player import core.game.world.map.Location import core.game.world.map.RegionManager @@ -37,7 +38,7 @@ object Repository { /** * The renderable NPCs. */ - private val RENDERABLE_NPCS: MutableList = CopyOnWriteArrayList() + public val RENDERABLE_NPCS: MutableList = CopyOnWriteArrayList() /** * A mapping holding the players sorted by their names. @@ -100,6 +101,7 @@ object Repository { */ @JvmStatic fun addRenderableNPC(npc: NPC) { + if (npc is RevenantNPC) return // hack to make sure we can update revenants every tick. if (!RENDERABLE_NPCS.contains(npc)) { RENDERABLE_NPCS.add(npc) npc.isRenderable = true @@ -112,6 +114,7 @@ object Repository { */ @JvmStatic fun removeRenderableNPC(npc: NPC) { + if (npc is RevenantNPC) return // hack to make sure we can update revenants every tick. RENDERABLE_NPCS.remove(npc) npc.isRenderable = false }