# language: en

# LASER MAZE 3D — build a world once, then explore it through a camera.
# Run, then click Live preview to fire. W/S or UP/DOWN: walk; A/D or LEFT/RIGHT: turn.
# Q/E: strafe; LEFT CLICK or SPACE: fire; R: restart; ESC or IDE Stop: exit.
# Disable all six orange training drones. Solid walls block movement and shots.
# X goes across the map, Y goes up, and Z goes down the map.
# Try changing wall heights, drone positions, turnSpeed, or moveSpeed.

let world = [
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1],
    [1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1],
    [1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1],
    [1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1],
    [1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1],
    [1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]
let targetX = [5.5, 8.5, 10.5, 2.5, 6.5, 9.5]
let targetZ = [2.5, 2.5, 6.5, 7.5, 6.5, 8.5]
# Each drone is a small model made from boxes and four cylinder motor hubs.
# Keep every part name together so one hit removes the complete drone.
let partSuffixes = [
    "", "-canopy",
    "-arm-0", "-arm-1", "-arm-2", "-arm-3",
    "-motor-0", "-motor-1", "-motor-2", "-motor-3",
    "-blade-0-0", "-blade-0-1", "-blade-1-0", "-blade-1-1",
    "-blade-2-0", "-blade-2-1", "-blade-3-0", "-blade-3-1"
]
let motorX = [-0.27, 0.27, -0.27, 0.27]
let motorZ = [-0.27, -0.27, 0.27, 0.27]
let rotorDirections = [1, 1, -1, -1, -1, -1, 1, 1]
let droneParts = repeatList(6, [])
let droneRotors = repeatList(6, [])
let hitTargets = repeatList(6, false)
let score = 0
let shots = 0
let moveSpeed = 0.10
let turnSpeed = 3
let flashFrames = 0
let hitFlash = false
let spaceWasDown = false
let restartWasDown = false

function finishPart(name, x, y, z, color):
    position3D(name, x, y, z)
    color3D(name, color)
    solid3D(name, false)

function createDrone(index):
    let name = "drone-" + text(index)
    let x = targetX[index]
    let z = targetZ[index]
    let parts = repeatList(length(partSuffixes), "")
    let part = 0
    while part < length(partSuffixes):
        set parts[part] = name + partSuffixes[part]
        set part = part + 1
    set droneParts[index] = parts
    let rotors = repeatList(8, "")
    # The fuselage stays at eye height; its original name remains the first target.
    box3D(name, 0.38, 0.20, 0.30)
    finishPart(name, x, 1.5, z, "#ffad52")
    box3D(name + "-canopy", 0.23, 0.09, 0.21)
    finishPart(name + "-canopy", x, 1.625, z, "#55e6df")
    # Four diagonal arms connect the body to upright cylinder motors.
    let motor = 0
    while motor < 4:
        let arm = name + "-arm-" + text(motor)
        box3D(arm, 0.39, 0.06, 0.065)
        finishPart(arm, x + motorX[motor] / 2, 1.60, z + motorZ[motor] / 2, "#e8b56a")
        let angle = -45
        if motorX[motor] * motorZ[motor] > 0:
            set angle = 45
        turn3D(arm, angle)
        let hub = name + "-motor-" + text(motor)
        cylinder3D(hub, 0.075, 0.12)
        finishPart(hub, x + motorX[motor], 1.66, z + motorZ[motor], "#b7d1e6")
        # Two thin crossed blades spin above each motor, in opposite pairs.
        let blade = 0
        while blade < 2:
            let rotor = name + "-blade-" + text(motor) + "-" + text(blade)
            box3D(rotor, 0.36, 0.025, 0.045)
            finishPart(rotor, x + motorX[motor], 1.735, z + motorZ[motor], "#f3f7ff")
            turn3D(rotor, motor * 17 + blade * 90)
            set rotors[motor * 2 + blade] = rotor
            set blade = blade + 1
        set motor = motor + 1
    set droneRotors[index] = rotors

function resetGame():
    clear3D("#101c30")
    set score = 0
    set shots = 0
    set flashFrames = 0
    set hitFlash = false
    set hitTargets = repeatList(length(targetX), false)
    # The floor is decoration: it does not block walking.
    box3D("floor", 12, 0.2, 10)
    position3D("floor", 6, -0.1, 5)
    color3D("floor", "#263b51")
    texture3D("floor", "checker", 1)
    solid3D("floor", false)
    let row = 0
    while row < length(world):
        let column = 0
        while column < length(world[row]):
            if world[row][column] != 0:
                let name = "wall-" + text(row) + "-" + text(column)
                box3D(name, 1, 3, 1)
                position3D(name, column + 0.5, 1.5, row + 0.5)
                color3D(name, "#527a96")
                texture3D(name, "brick", 2)
                if world[row][column] == 2:
                    color3D(name, "#7863ac")
            set column = column + 1
        set row = row + 1
    let index = 0
    while index < length(targetX):
        createDrone(index)
        set index = index + 1
    camera3D(2.5, 1.5, 2.5)
    lookAt3D(5.5, 1.5, 2.5)

function fireLaser():
    if score == length(targetX):
        return
    set shots = shots + 1
    set flashFrames = 3
    set hitFlash = false
    # The closest object wins: a wall in front of a drone blocks the laser.
    let target = aim3D(30)
    let index = 0
    while index < length(targetX):
        if not hitTargets[index]:
            # Every visible part belongs to one drone, but walls still block the ray.
            for part in droneParts[index]:
                if target == part:
                    for piece in droneParts[index]:
                        remove3D(piece)
                    set hitTargets[index] = true
                    set score = score + 1
                    set hitFlash = true
                    return
        set index = index + 1

function updatePlayer():
    let turn = 0
    if keyDown("left") or keyDown("a"):
        set turn = turn - 1
    if keyDown("right") or keyDown("d"):
        set turn = turn + 1
    # Turning changes direction, while movement changes position.
    turnCamera3D(turn * turnSpeed)
    let forwardMove = 0
    let sidewaysMove = 0
    if keyDown("w") or keyDown("up"):
        set forwardMove = forwardMove + 1
    if keyDown("s") or keyDown("down"):
        set forwardMove = forwardMove - 1
    if keyDown("q"):
        set sidewaysMove = sidewaysMove - 1
    if keyDown("e"):
        set sidewaysMove = sidewaysMove + 1
    let speed = moveSpeed
    if forwardMove != 0 and sidewaysMove != 0:
        set speed = speed * 0.707107
    # Pliro checks solid objects and slides along blocked walls for us.
    moveCamera3D(forwardMove * speed, sidewaysMove * speed)

function drawGame():
    let index = 0
    while index < length(targetX):
        if not hitTargets[index]:
            # Only the propellers rotate. The body and arms keep their positions.
            let rotor = 0
            while rotor < length(droneRotors[index]):
                turn3D(droneRotors[index][rotor], 18 * rotorDirections[rotor])
                set rotor = rotor + 1
        set index = index + 1
    # draw3D starts a complete frame. Ordinary 2D shapes form the HUD above it.
    draw3D()
    drawLine(307, 180, 315, 180, 2, "#ffffff")
    drawLine(325, 180, 333, 180, 2, "#ffffff")
    drawLine(320, 167, 320, 175, 2, "#ffffff")
    drawLine(320, 185, 320, 193, 2, "#ffffff")
    rectangle(294, 280, 52, 54, "#17263c")
    rectangle(309, 259, 22, 60, "#a6bbd2")
    if flashFrames > 0:
        drawLine(320, 260, 320, 180, 3, "#a8fff4")
        if hitFlash:
            drawText(320, 209, "DRONE DISABLED", 13, "#a8fff4", "sans", "center")
    rectangle(0, 0, 640, 34, "#101828")
    drawText(14, 9, "LASER MAZE 3D", 16, "#8ffff0", "pixel")
    drawText(292, 10, "DRONES " + text(score) + "/6", 13, "#ffffff")
    drawText(469, 10, "SHOTS " + text(shots), 13, "#ffcf8d")
    rectangle(0, 334, 640, 26, "#101828")
    drawText(320, 342, "W/S move | A/D turn | Q/E strafe | CLICK/SPACE fire | R restart | ESC exit", 11, "#c9d7e8", "sans", "center")
    if score == length(targetX):
        rectangle(124, 114, 392, 113, "#101828")
        drawText(320, 133, "MAZE COMPLETE!", 23, "#8ffff0", "pixel", "center")
        drawText(320, 175, "Six drones disabled in " + text(shots) + " shots.", 14, "#ffffff", "sans", "center")
        drawText(320, 204, "R: play again     ESC: exit", 13, "#ffcf8d", "sans", "center")

# Game loop
when pointerClicked("primary"):
    fireLaser()

when started():
    resetGame()
    while true:
        if keyDown("escape"):
            exitApp()
        let restartIsDown = keyDown("r")
        if restartIsDown and not restartWasDown:
            resetGame()
        let spaceIsDown = keyDown("space")
        if score < length(targetX):
            updatePlayer()
            if spaceIsDown and not spaceWasDown:
                fireLaser()
        drawGame()
        if flashFrames > 0:
            set flashFrames = flashFrames - 1
        set spaceWasDown = spaceIsDown
        set restartWasDown = restartIsDown
        wait(0.04)
