Space Blast

Bhathiya Perera

Hint: You need a keyboard to play this

# All assets from https://www.kenney.nl/assets/
# Code ported to Yaksha based on https://github.com/tashvit/space-blast

import raylib as rl
import raylib.utils
import libs.numbers as num
import libs.perlin
import libs.random

SCENE_TITLE: Const[u8] = 0u8
SCENE_PLAY: Const[u8] = 1u8
SCENE_GAME_OVER: Const[u8] = 2u8
GAME_W: Const[int] = 1920
GAME_H: Const[int] = 1080
STAR_COUNT: Const[int] = 100
ENEMY_COUNT: Const[int] = 20
FPS_TARGET: Const[int] = 60
PLAYER_BULLETS: Const[int] = 10
ENEMY_BULLET_MAX: Const[int] = 5

@onstack
class Star:
    x: int
    y: int
    speed: int
    r: int

@onstack
class Bullet:
    x: int
    y: int

@onstack
class Enemy:
    x: int
    y: int
    speed: int
    type: int
    swing: int
    bullet: Bullet

class State:
    frame_count: u64
    assets: Assets
    player_x: int
    player_y: int
    speed: int
    stars: Array[Star]
    enemies: Array[Enemy]
    scene: u8
    player_moving: bool
    player_bullets: Array[Bullet]
    player_score: int

class Assets:
    loaded: bool
    bg: rl.Color
    white: rl.Color
    player: rl.Texture2D
    enemy1: rl.Texture2D
    enemy2: rl.Texture2D
    enemy3: rl.Texture2D
    enemy4: rl.Texture2D
    enemy5: rl.Texture2D
    meteor: rl.Texture2D
    enemy_laser: rl.Texture2D
    player_laser: rl.Texture2D
    enemy_laser_sound: rl.Sound
    player_laser_sound: rl.Sound
    explosion_sound: rl.Sound

def update_stars(s: State) -> None:
    for star: Star in s.stars:
        star.y += star.speed
        # if we are out of the screen recreate the star
        if star.y > GAME_H + 10:
            star.y = -10
            star.x = rl.get_random_value(0, GAME_W)
            star.r = rl.get_random_value(1, 4)

def update_player_bullets(s: State, pressed_fire: bool) -> None:
    to_fire: bool = pressed_fire
    for bullet: Bullet in s.player_bullets:
        if bullet.y == -999 and to_fire and s.frame_count % 8u64 == 0u64:
            bullet.y = s.player_y - 32
            bullet.x = s.player_x - 32
            rl.play_sound(s.assets.player_laser_sound)
            to_fire = False
        if bullet.y < 0:
            bullet.y = -999
            bullet.x = -999
        if bullet.y != -999:
            bullet.y -= 10

def update_player(s: State) -> None:
    w2: int = cast("int", s.assets.player.width) / 2
    h2: int = cast("int", s.assets.player.height) / 2
    s.player_moving = False
    # User pressed a key -> move the player
    if rl.is_key_down(rl.KEY_W) or rl.is_key_down(rl.KEY_UP):
        s.player_y -= s.speed
        s.player_moving = True
    if rl.is_key_down(rl.KEY_A) or rl.is_key_down(rl.KEY_LEFT):
        s.player_x -= s.speed
        s.player_moving = True
    if rl.is_key_down(rl.KEY_S) or rl.is_key_down(rl.KEY_DOWN):
        s.player_y += s.speed
        s.player_moving = True
    if rl.is_key_down(rl.KEY_D) or rl.is_key_down(rl.KEY_RIGHT):
        s.player_x += s.speed
        s.player_moving = True
    update_player_bullets(s, rl.is_key_down(rl.KEY_SPACE))

    # Ensure that the player width/height is constrained
    # So we cannot go out of the bounds
    if s.player_x <= w2:
        s.player_x = w2
    if s.player_x >= GAME_W - w2:
        s.player_x = GAME_W - w2
    if s.player_y <= h2:
        s.player_y = h2
    if s.player_y >= GAME_H - h2:
        s.player_y = GAME_H - h2
    # Check collisions with enemies
    for enemy: Enemy in s.enemies:
        enemy_collide: bool = collides(get_enemy_ship(s, enemy.type), enemy.x, enemy.y, s.assets.player, s.player_x, s.player_y)
        bullet_collide: bool = collides(s.assets.enemy_laser, enemy.bullet.x, enemy.bullet.y, s.assets.player, s.player_x, s.player_y)
        if enemy_collide or bullet_collide:
            s.scene = SCENE_GAME_OVER
            rl.play_sound(s.assets.explosion_sound)
            break
        for bullet: Bullet in s.player_bullets:
            enemy_in_fire: bool = collides(get_enemy_ship(s, enemy.type), enemy.x, enemy.y, s.assets.player_laser, bullet.x, bullet.y)
            bulllets_collide: bool = collides(s.assets.enemy_laser, enemy.bullet.x, enemy.bullet.y, s.assets.player_laser, bullet.x, bullet.y)
            if bullet.y != -999 and enemy_in_fire:
                s.player_score += 10
                enemy.y = GAME_H + 50
                bullet.y = -999
                bullet.x = -999
                rl.play_sound(s.assets.explosion_sound)
            if bullet.y != -999 and bulllets_collide:
                enemy.bullet.x = -999
                enemy.bullet.y = -999
                bullet.y = -999
                bullet.x = -999

def update_enemies(s: State) -> None:
    active_bullets: int = 0
    for enemy: Enemy in s.enemies:
        if enemy.bullet.y != -999:
            active_bullets += 1

    for enemy: Enemy in s.enemies:
        enemy.y += enemy.speed
        swing: u64 = cast("u64", enemy.swing) + s.frame_count
        enemy.x += enemy.speed * iif(swing % 60u64 < 30u64, 1, -1)
        if enemy.y > GAME_H + 50:
            enemy.y = -rl.get_random_value(60, 100)
            enemy.x = rl.get_random_value(0, GAME_W)
            enemy.type = rl.get_random_value(1, 5)
            enemy.swing = rl.get_random_value(100, 10000)
        elif swing % 6u64 == 0u64 and enemy.bullet.y == -999 and enemy.y > 5 and active_bullets < ENEMY_BULLET_MAX:
            enemy.bullet.x = enemy.x + 32
            enemy.bullet.y = enemy.y + 32
            rl.play_sound(s.assets.enemy_laser_sound)
            active_bullets += 1
        if enemy.bullet.y > GAME_H + 50:
            enemy.bullet.x = -999
            enemy.bullet.y = -999
            active_bullets -= 1
        elif enemy.bullet.y != -999:
            enemy.bullet.y += 6

def draw_stars(s: State) -> None:
    for star: Star in s.stars:
        if star.r == 4:
            draw_image(s.assets.meteor, star.x, star.y)
        else:
            rl.draw_circle(star.x, star.y, cast("float", star.r), s.assets.white)

def draw_enemies(s: State) -> None:
    for enemy: Enemy in s.enemies:
        draw_image(get_enemy_ship(s, enemy.type), enemy.x, enemy.y)
        if enemy.bullet.y != -999:
            draw_image(s.assets.enemy_laser, enemy.bullet.x, enemy.bullet.y)

def draw_player(s: State) -> None:
    draw_image(s.assets.player, s.player_x, s.player_y)
    for bullet: Bullet in s.player_bullets:
        if bullet.y != -999:
            draw_image(s.assets.player_laser, bullet.x, bullet.y)

def game_step(d: utils.Data) -> None:
    s: State = cast("State", d)
    ensure_assets(s)
    rl.begin_drawing()
    rl.clear_background(s.assets.bg)
    # ----------------------------------------------
    # ----------------------------------------------
    if s.scene == SCENE_PLAY:
        update_player(s)
        update_stars(s)
        update_enemies(s)
        if s.frame_count % 60u64 == 0u64:
            s.player_score += 1
    draw_stars(s)
    draw_player(s)
    draw_enemies(s)
    if s.scene == SCENE_TITLE:
        if (s.frame_count / 50u64) % 2u64 == 0u64:
            rl.draw_text("Press [enter] to start", GAME_W / 2 - 450, GAME_H / 2 - 30, 80, s.assets.white)
        if rl.is_key_down(rl.KEY_ENTER):
            s.scene = SCENE_PLAY
    if s.scene == SCENE_GAME_OVER:
        if (s.frame_count / 50u64) % 2u64 == 0u64:
            rl.draw_text("Game over ", GAME_W / 2 - 200, GAME_H / 2 - 30, 80, s.assets.white)
            rl.draw_text("Press [enter] to start", GAME_W / 2 - 450, GAME_H / 2 + 50, 80, s.assets.white)
        if rl.is_key_down(rl.KEY_ENTER):
            s.scene = SCENE_PLAY
            reset_state(s)
    # -----------------------------------------------
    # ----------------------------------------------
    rl.draw_fps(0, 0)
    rl.draw_text(num.i2s(s.player_score), GAME_W - 300, 0, 64, rl.color(0, 255, 0, 255))
    rl.end_drawing()
    s.frame_count = s.frame_count + 1u64

def init_state() -> State:
    s: State = State()
    s.frame_count = 0u64
    s.assets = Assets()
    s.assets.loaded = False
    s.stars = arrnew("Star", STAR_COUNT)
    s.enemies = arrnew("Enemy", ENEMY_COUNT)
    s.player_bullets = arrnew("Bullet", PLAYER_BULLETS)
    random.init_random()
    reset_state(s)
    return s

def reset_state(s: State) -> None:
    s.player_x = GAME_H / 2
    s.player_y = GAME_W / 2
    s.speed = 5
    s.player_score = 0

    for bullet: Bullet in s.player_bullets:
        bullet.x = -999
        bullet.y = -999

    for star: Star in s.stars:
        star.x = rl.get_random_value(0, GAME_W)
        star.y = rl.get_random_value(0, GAME_H)
        star.speed = rl.get_random_value(1, 4)
        star.r = rl.get_random_value(1, 4)

    for enemy: Enemy in s.enemies:
        enemy.x = rl.get_random_value(0, GAME_W)
        enemy.y = -rl.get_random_value(60, 100)
        enemy.speed = rl.get_random_value(1, 5)
        enemy.type = rl.get_random_value(1, 5)
        enemy.swing = rl.get_random_value(100, 10000)
        enemy.bullet.x = -999
        enemy.bullet.y = -999

def ensure_assets(s: State) -> None:
    if s.assets.loaded:
        return
    s.assets.bg = rl.color(0, 0, 0, 255)
    s.assets.white = rl.color(255, 255, 255, 255)
    s.assets.player = load_image("playerShip1_blue.png")
    s.assets.enemy1 = load_image("shipBeige_manned.png")
    s.assets.enemy2 = load_image("shipBlue_manned.png")
    s.assets.enemy3 = load_image("shipPink_manned.png")
    s.assets.enemy4 = load_image("shipGreen_manned.png")
    s.assets.enemy5 = load_image("shipYellow_manned.png")
    s.assets.meteor = load_image("meteorGrey_tiny2.png")
    s.assets.enemy_laser = load_image("laserRed03.png")
    s.assets.player_laser = load_image("laserBlue03.png")
    s.assets.player_laser_sound = load_sound("laserRetro_000.ogg")
    s.assets.enemy_laser_sound = load_sound("laserRetro_004.ogg")
    s.assets.explosion_sound = load_sound("explosionCrunch_000.ogg")
    s.assets.loaded = True

def del_state(current: utils.Data) -> None:
    s: State = cast("State", current)
    if s.assets.loaded:
        rl.unload_texture(s.assets.player)
        rl.unload_texture(s.assets.enemy1)
        rl.unload_texture(s.assets.enemy2)
        rl.unload_texture(s.assets.enemy3)
        rl.unload_texture(s.assets.enemy4)
        rl.unload_texture(s.assets.enemy5)
        rl.unload_texture(s.assets.enemy_laser)
        rl.unload_texture(s.assets.player_laser)
        rl.unload_sound(s.assets.player_laser_sound)
        rl.unload_sound(s.assets.enemy_laser_sound)
        rl.unload_sound(s.assets.explosion_sound)
    del s.enemies
    del s.stars
    del s.assets
    del s.player_bullets
    del s

def main() -> int:
    s: State = init_state()
    s.scene = SCENE_TITLE
    rl.init_window(GAME_W, GAME_H, "Space blast")
    rl.set_target_fps(60)
    rl.init_audio_device()
    while not rl.window_should_close():
        game_step(cast("utils.Data", s))
    del_state(cast("utils.Data", s))
    rl.close_audio_device()
    rl.close_window()
    return 0

# ------------ Utilities -----------

def load_image(s: str) -> rl.Texture2D:
    path: str
    path = "assets/img/" + s
    return rl.load_texture(path)

def load_sound(s: str) -> rl.Sound:
    path: str
    path = "assets/audio/" + s
    return rl.load_sound(path)

def draw_image(img: rl.Texture2D, x: int, y: int) -> None:
    w: int = cast("int", img.width)
    h: int = cast("int", img.height)
    rl.draw_texture(img, x - w / 2, y - h / 2, rl.color(255, 255, 255, 255))

def rectangle(img: rl.Texture2D, x: int, y: int) -> rl.Rectangle:
    w: int = cast("int", img.width) - 2
    h: int = cast("int", img.height) - 2
    actual_x: float = cast("float", x - w / 2) + 1.0f
    actual_y: float = cast("float", y - h / 2) + 1.0f
    return rl.rectangle(actual_x, actual_y, cast("float", w), cast("float", h))

def collides(img1: rl.Texture2D, x1: int, y1: int, img2: rl.Texture2D, x2: int, y2: int) -> bool:
    return rl.check_collision_recs(rectangle(img1, x1, y1), rectangle(img2, x2, y2))

def get_enemy_ship(s: State, asset: int) -> rl.Texture2D:
    enemy: rl.Texture2D
    if asset == 1:
        enemy = s.assets.enemy1
    if asset == 2:
        enemy = s.assets.enemy2
    if asset == 3:
        enemy = s.assets.enemy3
    if asset == 4:
        enemy = s.assets.enemy4
    if asset == 5:
        enemy = s.assets.enemy5
    return enemy