import content.entity.combat.hit.hit
import content.entity.player.bank.bank
import content.entity.proj.shoot
import world.gregs.voidps.engine.Script
import world.gregs.voidps.engine.client.message
import world.gregs.voidps.engine.entity.World
import world.gregs.voidps.engine.entity.character.npc.NPC
import world.gregs.voidps.engine.entity.character.npc.NPCs
import world.gregs.voidps.engine.entity.character.player.Player
import world.gregs.voidps.engine.entity.character.player.name
import world.gregs.voidps.engine.entity.character.player.skill.Skill
import world.gregs.voidps.engine.entity.obj.GameObject
import world.gregs.voidps.engine.entity.obj.GameObjects
import world.gregs.voidps.engine.entity.obj.ObjectShape
import world.gregs.voidps.engine.inv.add
import world.gregs.voidps.engine.inv.inventory
import world.gregs.voidps.engine.inv.remove
import world.gregs.voidps.engine.queue.queue
import world.gregs.voidps.type.Tile
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.max
import kotlin.random.Random
class DwarfMulticannon : Script {
init {
itemOption("Set-up", "*") { option ->
if (option.item.id == parts.first().item) setUp(this)
}
itemOnObjectOperate("*", "*") { option ->
if (option.item.id == CANNONBALL && option.target.intId == parts.last().objectId) {
load(this, option.target)
}
}
objectOperate("Fire", "*") { option ->
if (option.target.intId == parts.last().objectId) fire(this, option.target)
}
objectOperate("Empty", "*") { option ->
if (option.target.intId == parts.last().objectId) empty(this, option.target)
}
objectOperate("Pick-up", "*") { option ->
if (option.target.intId in cannonObjectIds) pickUp(this, option.target)
}
playerDespawn { recover(this, notify = false) }
}
private data class CannonPart(
val item: String,
val objectId: Int,
val setupMessage: String,
)
private data class Direction(
val animation: Int,
val x: Int,
val y: Int,
)
private data class Cannon(
val tile: Tile,
var obj: GameObject,
var stage: Int = 1,
var ammunition: Int = 0,
var direction: Int = 0,
var firing: Boolean = false,
var pulsing: Boolean = false,
)
companion object {
private const val CANNONBALL = "cannonball"
private const val SETUP_ANIMATION = "827"
private const val PROJECTILE = "dwarf_multicannon_projectile"
private const val MAX_AMMUNITION = 30
private const val RANGE = 8
private const val MAX_HIT = 300
private const val HIT_DELAY_TICKS = 2
private const val PULSE_TICKS = 1
private val parts = listOf(
CannonPart("cannon_base", 7, "You place the cannon base on the ground."),
CannonPart("cannon_stand", 8, "You add the stand to the cannon base."),
CannonPart("cannon_barrels", 9, "You add the barrels to the cannon."),
CannonPart("cannon_furnace", 6, "You add the furnace and complete the dwarf multicannon."),
)
private val directions = listOf(
Direction(515, 0, 1),
Direction(516, 1, 1),
Direction(517, 1, 0),
Direction(518, 1, -1),
Direction(519, 0, -1),
Direction(520, -1, -1),
Direction(521, -1, 0),
Direction(514, -1, 1),
)
private val cannonObjectIds = parts.mapTo(mutableSetOf()) { it.objectId }
private val cannons = mutableMapOf<String, Cannon>()
private fun key(player: Player) = player.name.trim().lowercase()
fun hasCannon(player: Player) = key(player) in cannons
fun recover(player: Player, notify: Boolean = true) {
val cannon = cannons.remove(key(player)) ?: return
removeObject(cannon.obj)
returnCannon(player, cannon)
if (notify) player.message("Your dwarf multicannon has been returned to you.")
}
private fun returnCannon(player: Player, cannon: Cannon) {
parts.forEach { giveOrBank(player, it.item, 1) }
giveOrBank(player, CANNONBALL, cannon.ammunition)
}
private fun giveOrBank(player: Player, item: String, amount: Int) {
if (amount <= 0 || player.inventory.add(item, amount)) return
val name = item.replace('_', ' ')
if (player.bank.add(item, amount)) {
player.message("Your $name was sent to your bank.")
} else {
player.message("Your inventory and bank are too full to receive your $name.")
}
}
private fun removeObject(obj: GameObject) = GameObjects.remove(obj, collision = false)
}
private fun setUp(player: Player) {
if (hasCannon(player)) {
player.message("You already have a dwarf multicannon set up.")
return
}
if (parts.any { !player.inventory.contains(it.item) }) {
player.message("You need all four dwarf multicannon parts to set it up.")
return
}
val tile = player.tile.add(1, 0)
if (!validPlacement(player, tile) || !removeParts(player)) return
val cannon = Cannon(tile, createObject(parts.first().objectId, tile))
cannons[key(player)] = cannon
player.face(tile)
player.queue("dwarf_multicannon_setup") {
for ((index, part) in parts.withIndex()) {
if (!isActive(player, cannon)) return@queue
player.face(tile)
player.anim(SETUP_ANIMATION)
if (index > 0) replaceObject(cannon, part.objectId, index + 1)
player.message(part.setupMessage)
if (index < parts.lastIndex) delay(2)
}
player.message("Use cannonballs on the cannon to load it.")
}
}
private fun removeParts(player: Player): Boolean {
val removed = mutableListOf<String>()
for (part in parts) {
if (player.inventory.remove(part.item, 1)) {
removed += part.item
} else {
removed.forEach { player.inventory.add(it, 1) }
return false
}
}
return true
}
private fun createObject(id: Int, tile: Tile): GameObject =
GameObject(id, tile, ObjectShape.CENTRE_PIECE_STRAIGHT, 0).also {
GameObjects.add(it, collision = false)
}
private fun replaceObject(cannon: Cannon, id: Int, stage: Int) {
removeObject(cannon.obj)
cannon.obj = createObject(id, cannon.tile)
cannon.stage = stage
}
private fun load(player: Player, target: GameObject) {
val cannon = ownedCannon(player, target) ?: return
if (!complete(player, cannon)) return
loadAvailable(player, cannon)
}
private fun fire(player: Player, target: GameObject) {
val cannon = ownedCannon(player, target) ?: return
if (!complete(player, cannon)) return
if (cannon.ammunition < MAX_AMMUNITION) loadAvailable(player, cannon)
if (cannon.ammunition <= 0) {
player.message("Your cannon is empty. You need some cannonballs to fire it.")
return
}
if (cannon.firing) {
player.message("Your dwarf multicannon is firing.")
return
}
cannon.firing = true
startPulse(player, cannon)
player.message("You start the dwarf multicannon.")
}
private fun loadAvailable(player: Player, cannon: Cannon): Int {
val amount = minOf(
player.inventory.count(CANNONBALL),
MAX_AMMUNITION - cannon.ammunition,
)
if (amount <= 0) {
player.message(
if (cannon.ammunition >= MAX_AMMUNITION) "Your cannon is already full."
else "You do not have any cannonballs.",
)
return 0
}
if (!player.inventory.remove(CANNONBALL, amount)) return 0
cannon.ammunition += amount
cannon.firing = true
startPulse(player, cannon)
player.message("You load $amount cannonball${amount.plural()} into the cannon.")
player.message("The cannon now contains ${cannon.ammunition} cannonballs.")
return amount
}
private fun empty(player: Player, target: GameObject) {
val cannon = ownedCannon(player, target) ?: return
val amount = cannon.ammunition
if (amount <= 0) {
player.message("Your cannon does not contain any cannonballs.")
return
}
if (!player.inventory.add(CANNONBALL, amount)) {
player.message("You do not have enough inventory space to empty the cannon.")
return
}
cannon.ammunition = 0
cannon.firing = false
player.message("You remove $amount cannonball${amount.plural()} from the cannon.")
}
private fun pickUp(player: Player, target: GameObject) {
val cannon = ownedCannon(player, target) ?: return
cannons.remove(key(player))
removeObject(cannon.obj)
returnCannon(player, cannon)
player.message("You pick up your dwarf multicannon.")
}
private fun startPulse(player: Player, cannon: Cannon) {
if (cannon.pulsing) return
cannon.pulsing = true
cannon.direction = 0
pulse(player, cannon)
}
private fun queuePulse(player: Player, cannon: Cannon) {
World.queue("dwarf_multicannon_rotation_${key(player)}", PULSE_TICKS) {
pulse(player, cannon)
}
}
private fun pulse(player: Player, cannon: Cannon) {
if (!isActive(player, cannon) || !cannon.firing || cannon.ammunition <= 0 || cannon.stage < parts.size) {
cannon.pulsing = false
return
}
animate(cannon)
findTarget(cannon)?.let { shoot(player, cannon, it) }
cannon.direction = (cannon.direction + 1) % directions.size
queuePulse(player, cannon)
}
private fun animate(cannon: Cannon) {
cannon.obj.anim(directions[cannon.direction].animation.toString())
}
private fun shoot(owner: Player, cannon: Cannon, target: NPC) {
if (!validTarget(cannon, target) || cannon.ammunition <= 0) return
cannon.ammunition--
projectileSource(cannon).shoot(
id = PROJECTILE,
target = target,
)
owner.hit(
target = target,
delay = HIT_DELAY_TICKS,
offensiveType = "range",
damage = Random.nextInt(MAX_HIT + 1),
)
if (cannon.ammunition == 0) {
cannon.firing = false
owner.message("<col=ff0000>Your dwarf multicannon has run out of cannonballs.</col>")
}
}
private fun projectileSource(cannon: Cannon): Tile {
val centre = cannon.tile.add(1, 1)
val direction = directions[cannon.direction]
return centre.add(direction.x, direction.y)
}
private fun findTarget(cannon: Cannon): NPC? {
var nearest: NPC? = null
var nearestDistance = Int.MAX_VALUE
for (tile in cannon.tile.toCuboid(RANGE)) {
for (npc in NPCs.at(tile)) {
if (!validTarget(cannon, npc) || !faces(cannon, npc.tile)) continue
val distance = distance(cannon.tile, npc.tile)
if (distance < nearestDistance) {
nearest = npc
nearestDistance = distance
}
}
}
return nearest
}
private fun validTarget(cannon: Cannon, npc: NPC): Boolean =
npc.def.combat > 0 &&
!npc.hide &&
npc.levels.get(Skill.Constitution) > 0 &&
npc.tile.level == cannon.tile.level &&
distance(cannon.tile, npc.tile) <= RANGE
private fun faces(cannon: Cannon, target: Tile): Boolean {
val dx = target.x - cannon.tile.x
val dy = target.y - cannon.tile.y
if (dx == 0 && dy == 0) return false
val targetAngle = (Math.toDegrees(atan2(dx.toDouble(), dy.toDouble())) + 360.0) % 360.0
val cannonAngle = cannon.direction * 45.0
val difference = abs(((targetAngle - cannonAngle + 540.0) % 360.0) - 180.0)
return difference <= 22.5
}
private fun ownedCannon(player: Player, target: GameObject): Cannon? {
val cannon = cannons[key(player)]
if (cannon == null || cannon.tile != target.tile) {
player.message("This is not your dwarf multicannon.")
return null
}
return cannon
}
private fun complete(player: Player, cannon: Cannon): Boolean {
if (cannon.stage == parts.size) return true
player.message("You must finish building the cannon first.")
return false
}
private fun isActive(player: Player, cannon: Cannon): Boolean {
if (cannons[key(player)] !== cannon) return false
if (player.tile.level == cannon.tile.level) return true
recover(player)
return false
}
private fun validPlacement(player: Player, tile: Tile): Boolean {
val message = when {
cannons.values.any { distance(it.tile, tile) <= 3 } ->
"You are too close to another dwarf multicannon."
player["tournament_active", false] || player["tournament_match", false] ->
"You cannot set up a dwarf multicannon during a tournament."
else -> return true
}
player.message(message)
return false
}
private fun distance(first: Tile, second: Tile) =
max(abs(first.x - second.x), abs(first.y - second.y))
private fun Int.plural() = if (this == 1) "" else "s"
}
Sent in by SeekMercy, needs some tidying, checking & testing