# Necrochasm Prop Maker Feather RP2040 Python Code
# Written by Christopher Littlefield
# "Is your Light bright enough to stand in full gaze of the Hive's abyss?"

import board
import analogio
import digitalio
import pwmio
import time
import random
import neopixel
import audiobusio
import audiocore
import audiomixer
from adafruit_motor import motor
from adafruit_debouncer import Debouncer

max_ammo = 50
ammo = 0
rpm = 720  # The Necrochasm has two firing speeds: 720 and 900 rpm.
# This value controls the timing of the code to make the prop's firing rate accurate.
ammo_interval = 60 / rpm
ammo_time = -3600
infinite_ammo = False
infinite_ammo_timer = 5.0
# Desperation is an in-game feature where the Necrochasm's reload speed is temporailiy
# increased to 900 RPM. In the prop, this is meant to be triggered by a button press,
# then reloading the gun.
desperation_ready = False
desperation_activated = -3600.0
desperation_pressed = -3600.0
desperation_timer = 6.0
desperation_pin = digitalio.DigitalInOut(board.D12)
desperation_pin.direction = digitalio.Direction.INPUT
desperation_pin.switch_to_input(pull=digitalio.Pull.UP)
desperation_button = Debouncer(desperation_pin)

# The reload pin is grounded while the ammo cartridge is inserted into the prop.
# If the pin is open, the cartridge has been removed.
reload_pin = digitalio.DigitalInOut(board.D11)
reload_pin.direction = digitalio.Direction.INPUT
reload_pin.switch_to_input(pull=digitalio.Pull.UP)
reload = Debouncer(reload_pin)
trigger_pin = digitalio.DigitalInOut(board.EXTERNAL_BUTTON)
trigger_pin.direction = digitalio.Direction.INPUT
trigger_pin.switch_to_input(pull=digitalio.Pull.UP)
trigger = Debouncer(trigger_pin)
# This LED flashes in sync with the Necrochasm's firing rate.
barrel_led = digitalio.DigitalInOut(board.D10)
barrel_led.direction = digitalio.Direction.OUTPUT
barrel_led.value = False

rumble_a = pwmio.PWMOut(board.D24, frequency=50)
rumble_b = pwmio.PWMOut(board.D25, frequency=50)
rumble_motor = motor.DCMotor(rumble_a, rumble_b)
throttle_motor = throttle720 = 0.17
throttle900 = 0.25

# Neopixel Indicies
fiber_optics_indexA = 0
lower_flames_index = fiber_optics_indexA + 1
upper_flames_index = lower_flames_index + 13
targeting_light_index = upper_flames_index + 21
fiber_optics_indexB = targeting_light_index + 1
core_stripes_index = fiber_optics_indexB + 1
stripe_index = core_stripes_index + 3
core_index = stripe_index + 7
core_flames_index = core_index + 1
ridges_index = core_flames_index + 3
cartridge_index = ridges_index + 5
end_of_string = cartridge_index + 5
num_pixels = end_of_string + 1

# NOTE: "targeting_light_index" refers to a RGB LED, as opposed to the regular GRB leds.
# So, switch red and green values when programming color.

# Enable the external power pin so the addressable LEDS and sound can be used.
enable = digitalio.DigitalInOut(board.EXTERNAL_POWER)
enable.direction = digitalio.Direction.OUTPUT
enable.value = True

# These variables help measure the battery level of the Necrochasm.
vbat_voltage = analogio.AnalogIn(board.A3)
battery_voltage = 0.0

pixel = neopixel.NeoPixel(board.EXTERNAL_NEOPIXELS, num_pixels)
pixel.brightness = 0.1
flame_start = 128
# Audio Related
i2s = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA)
fire_wav = necro720 = audiocore.WaveFile("sounds/Necro720.wav")
necro900 = audiocore.WaveFile("sounds/Necro900.wav")
headshot = audiocore.WaveFile("sounds/Headshot.wav")
roar = audiocore.WaveFile("sounds/Roar.wav")
# The reason for the mixer is in the event the headshot file is played over the
# normal firing sound.
mixer = audiomixer.Mixer(voice_count=2, sample_rate=necro720.sample_rate,
    channel_count=2, bits_per_sample=necro720.bits_per_sample, samples_signed=True)
i2s.play(mixer)
mixer.voice[0].level = 1  # voice 0 is used for firing the Necrochasm
mixer.voice[1].level = 1  # voice 1 is used for playing the Headshot file

def animate_flames(pixel_array, low, high):
    flame_increment = 40
    flame_max_brightness = 255
    for i in range(low, high):
        color = list(pixel_array[i])  # Neopixel colors are a tuple, so convert to list.
        # Pull Green value from index 1.
        color[1] += flame_increment
        if color[1] > flame_max_brightness:
            color[1] = random.randrange(0, int(flame_increment/2))
        pixel_array[i] = (0, color[1], 0)

def get_voltage(pin):
    return pin.value / 65535 * 3.3 * 2

# Startup Sequence
if reload.value is False:
    ammo = max_ammo
else:
    ammo = 0
battery_voltage = get_voltage(vbat_voltage)

pixel[fiber_optics_indexA] = (0, 255, 0)
for i in range(lower_flames_index, targeting_light_index):
    pixel[i] = (0, random.randrange(0, flame_start), 0)
# Remember, we swap red and green at this index.
pixel[targeting_light_index] = (255, 0, 0)
for i in range(fiber_optics_indexB, core_flames_index):
    pixel[i] = (0, 255, 0)
for i in range(core_flames_index, ridges_index):
            pixel[i] = (0, random.randrange(0, flame_start), 0)
for i in range(ridges_index, end_of_string):
    pixel[i] = (0, 255, 0)

while True:
    reload.update()
    desperation_button.update()
    trigger.update()

    #Battery Voltage Indicator
    battery_voltage = get_voltage(vbat_voltage)
    if battery_voltage > 3.8:
        pixel[stripe_index] = (0, 255, 0)
    else:
        pixel[stripe_index] = (0, 0, 0)
    if battery_voltage > 3.7:
        pixel[stripe_index+1] = (0, 255, 0)
    else:
        pixel[stripe_index+1] = (0, 0, 0)
    if battery_voltage > 3.6:
        pixel[stripe_index+2] = (0, 255, 0)
    else:
        pixel[stripe_index+2] = (0, 0, 0)
    if battery_voltage > 3.5:
        pixel[stripe_index+3] = (0, 255, 0)
    else:
        pixel[stripe_index+3] = (0, 0, 0)
    if battery_voltage > 3.4:
        pixel[stripe_index+4] = (0, 255, 0)
    else:
        pixel[stripe_index+4] = (0, 0, 0)
    if battery_voltage > 3.3:
        pixel[stripe_index+5] = (0, 255, 0)
    else:
        pixel[stripe_index+5] = (0, 0, 0)
    if battery_voltage > 3.2:
        pixel[stripe_index+6] = (0, 255, 0)
    else:
        pixel[stripe_index+6] = (0, 0, 0)

    if trigger.fell and ammo > 0:
        mixer.voice[0].play(fire_wav, loop=infinite_ammo
        )
        rumble_motor.throttle = throttle_motor

    if trigger.rose or ammo <= 0:
        mixer.voice[0].stop()
        rumble_motor.throttle = 0
        barrel_led.value = False
        for i in range(ridges_index, cartridge_index):
            pixel[i] = (0, 255, 0)

    if trigger.value is False and ammo > 0:
        if time.monotonic() > (ammo_time + (ammo_interval / 2)):
            if barrel_led.value is False:
                barrel_led.value = True
                for i in range(ridges_index, cartridge_index):
                    pixel[i] = (0, 255, 0)
                ammo_time = time.monotonic()
            else:
                barrel_led.value = False
                for i in range(ridges_index, cartridge_index):
                    pixel[i] = (0, 0, 0)
                ammo_time = time.monotonic()
            if infinite_ammo is False:
                ammo -= 1

            if ammo > 25:
                topLED = (ammo - 25) * 10
                pixel[cartridge_index] = pixel[cartridge_index + 2] = (0, 250, 0)
                pixel[cartridge_index + 1] = pixel[cartridge_index + 3] = (0, topLED, 0)
            else:
                pixel[cartridge_index] = pixel[cartridge_index + 2] = (0, ammo * 10, 0)
                pixel[cartridge_index + 1] = pixel[cartridge_index + 3] = (0, 0, 0)
    else:
        animate_flames(pixel, lower_flames_index, targeting_light_index)
        animate_flames(pixel, core_flames_index, ridges_index)
    if reload.fell:
        ammo = max_ammo
        for i in range(cartridge_index, cartridge_index + 4):
            pixel[i] = (0, 255, 0)
        if desperation_ready:
            desperation_activated = time.monotonic()
            rpm = 900
            ammo_interval = 60 / rpm
            fire_wav = necro900
            throttle_motor = throttle900
            desperation_ready = False

    if reload.rose:
        ammo = 0

    if desperation_button.fell:
        desperation_pressed = time.monotonic()

    if desperation_button.rose:
        if time.monotonic() > (desperation_pressed + infinite_ammo_timer):
            mixer.voice[1].play(roar, loop=False)
            infinite_ammo = not infinite_ammo
            ammo = max_ammo
            for i in range(cartridge_index, cartridge_index + 4):
                pixel[i] = (0, 255, 0)
        else:
            mixer.voice[1].play(headshot, loop=False)
            desperation_ready = True

    if time.monotonic() > (desperation_activated + desperation_timer) and rpm == 900:
        rpm = 720
        ammo_interval = 60 / rpm
        fire_wav = necro720
        throttle_motor = throttle720
        if trigger.value is False:
            rumble_motor.throttle = throttle720
        else:
            rumble_motor.throttle = 0
        if mixer.voice[0].playing:
            mixer.voice[0].play(fire_wav, loop=True)