Skip to content

Add dwarf multi-cannon #1192

Description

@GregHib

Sent in by SeekMercy, needs some tidying, checking & testing

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"
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions