Download
262 lines 9.3 KiBTomoModified 2025-04-07 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
# This file defines a World struct that keeps track of everything

use random
use ./raylib.tm
use ./player.tm
use ./camera.tm
use ./box.tm
use ./letter.tm
use ./satellite.tm

struct Goal(pos:Vector2)
    SIZE := Vector2(32,32)
    func draw(g:Goal)
        texture := Texture.load((./assets/Hurricane.png))
        texture.draw(g.pos, Goal.SIZE)

# Return a displacement relative to `a` that will push it out of `b`
func solve_overlap(a_pos:Vector2, a_size:Vector2, b_pos:Vector2, b_size:Vector2 -> Vector2)
    a_left := a_pos.x - a_size.x/2
    a_right := a_pos.x + a_size.x/2
    a_top := a_pos.y - a_size.x/2
    a_bottom := a_pos.y + a_size.y/2

    b_left := b_pos.x - b_size.x/2
    b_right := b_pos.x + b_size.x/2
    b_top := b_pos.y - b_size.y/2
    b_bottom := b_pos.y + b_size.y/2

    # Calculate the overlap in each dimension
    overlap_x := (a_right _min_ b_right) - (a_left _max_ b_left)
    overlap_y := (a_bottom _min_ b_bottom) - (a_top _max_ b_top)

    # If either axis is not overlapping, then there is no collision:
    if overlap_x <= 0 or overlap_y <= 0
        return Vector2(0, 0)

    if overlap_x < overlap_y
        if a_right > b_left and a_right < b_right
            return Vector2(-(overlap_x), 0)
        else if a_left < b_right and a_left > b_left
            return Vector2(overlap_x, 0)
    else
        if a_top < b_bottom and a_top > b_top
            return Vector2(0, overlap_y)
        else if a_bottom > b_top and a_bottom < b_bottom
            return Vector2(0, -overlap_y)

    return Vector2(0, 0)

func overlaps(a_pos:Vector2, a_size:Vector2, b_pos:Vector2, b_size:Vector2 -> Bool)
    return solve_overlap(a_pos, a_size, b_pos, b_size) != Vector2(0, 0)

struct Particle(pos,vel:Vector2,radius:Num32,color:Color)
    func update(p:&Particle, dt:Num32)
        p.pos += dt*p.vel
        p.vel *= Num32(0.95)
        p.radius -= dt*Num32(20.0)

    func draw(p:Particle)
        if p.radius > 0
            DrawCircleV(p.pos, p.radius, p.color)

func draw_centered_text(x,y:Int32, text:Text, color:Color, font_size=Int32(48), shadow=yes)
    if shadow
        draw_centered_text(x+2,y,text, Color(0,0,0), font_size, shadow=no)
        draw_centered_text(x-2,y,text, Color(0,0,0), font_size, shadow=no)
        draw_centered_text(x,y+2,text, Color(0,0,0), font_size, shadow=no)
        draw_centered_text(x,y-2,text, Color(0,0,0), font_size, shadow=no)

    string := CString(text)
    width := MeasureText(string, 48)
    DrawText(string, x - width/2, y - font_size/2, font_size, color)

struct World(
    player=@Player(Vector2(0,0), Vector2(0,0)),
    camera=@Camera(Vector2(0,0)),
    goal:Goal?=none,
    satellites:@[@Satellite]=@[],
    particles:@[@Particle]=@[],
    boxes:@[@Box]=@[],
    letters:@[Letter]=@[],
    dt_accum=Num32(0.0),
    won_time:Num?=none,
)
    DT := Num32(1.)/Num32(60.)
    STIFFNESS := Num32(0.3)

    func from_map(path:Path -> @World)
        world := @World() 
        world.load_map(path.read() or exit("Could not find the game map: $path"))
        world.camera.zoom = C_code : Num32 (
            (float)GetScreenWidth()/1000.
        )
        return world

    func update(w:@World, dt:Num32)
        w.dt_accum += dt
        while w.dt_accum > 0
            w.update_once()
            w.dt_accum -= World.DT
        w.camera.follow(w.player.pos)
        w.camera.update(dt)

    func update_once(w:@World)
        if w.goal and w.player.pos.dist(w.goal!.pos) < 30
            w.player.target_vel = Vector2(0,0)
            w.player.pos = w.player.pos.mix(w.goal!.pos, .03)
        else if w.player.has_signal and not w.player.dead
            target_x := C_code:Num32 (
                (Num32_t)((IsKeyDown(KEY_A) ? -1 : 0) + (IsKeyDown(KEY_D) ? 1 : 0))
            )
            target_y := C_code:Num32 (
                (Num32_t)((IsKeyDown(KEY_W) ? -1 : 0) + (IsKeyDown(KEY_S) ? 1 : 0))
            )
            w.player.target_vel = Vector2(target_x, target_y).norm() * Player.WALK_SPEED

        w.player.update()
        if w.won_time
            w.player.facing = w.player.facing.norm().rotated(Num32.TAU/60)

        w.player.has_signal = no
        for s in w.satellites
            s.update(w.boxes, w.player)

        for p in w.particles
            p.update(World.DT)

        w.particles[] = [p for p in w.particles if p.radius > 0]

        GR := Num32(.5) + Num32.sqrt(5)!/Num32(2)
        if not w.won_time and w.goal and overlaps(w.player.pos, Player.SIZE, w.goal!.pos, Goal.SIZE)
            w.won_time = GetTime()

            colors := [
                Color(0xFF, 0x00, 0x66),
                Color(0x00, 0xCC, 0xFF),
                Color(0xFF, 0xFF, 0x33),
                Color(0x66, 0xFF, 0x66),
                Color(0xFF, 0x66, 0x00),
                Color(0x99, 0x33, 0xFF),
                Color(0xFF, 0x33, 0x99),
                Color(0x00, 0xFF, 0xCC),
            ]
            for i in 50
                angle := Num32.TAU * ((Num32(i) * GR) mod Num32(1))
                w.particles.insert(
                    @Particle(
                        pos=w.goal!.pos,
                        vel=Vector2(random.num32(100,500),0).rotated(angle),
                        radius=random.num32(7,10),
                        color=colors[i mod1 colors.length],
                    )
                )


        # Resolve player overlapping with any boxes:
        for i in 3
            for b in w.boxes
                correction := solve_overlap(w.player.pos, Player.SIZE, b.pos, b.size)
                if b.fatal and correction != Vector2(0,0) and not w.player.dead
                    # Player hit a killer wall
                    w.player.dead = yes
                    w.camera.add_shake(100)
                    colors := [
                        Color(0xFF, 0xA5, 0x00, 0xc0),
                        Color(0xD2, 0x69, 0x1E, 0xc0),
                        Color(0x8B, 0x45, 0x13, 0xc0),
                        Color(0x70, 0x42, 0x22, 0xc0),
                        Color(0x55, 0x33, 0x22, 0xc0),
                        Color(0x88, 0x88, 0x88, 0xc0),
                        Color(0x44, 0x22, 0x11, 0xc0),
                        Color(0xCC, 0x44, 0x00, 0xc0),
                    ]
                    for i in 50
                        angle := Num32.TAU * ((Num32(i) * GR) mod Num32(1))
                        w.particles.insert(
                            @Particle(
                                pos=w.player.pos,
                                vel=Vector2(random.num32(100,600),0).rotated(angle),
                                radius=random.num32(10,30),
                                color=colors[i mod1 colors.length],
                            )
                        )

                w.player.pos += World.STIFFNESS * correction

        w.camera.update(World.DT)

    func draw(w:@World)
        ClearBackground(Color(0,0,0))
        bg := Texture.load((./assets/background.png), yes)
        bg.draw(
            Vector2(0,0),
            Vector2(Num32(GetScreenWidth()), Num32(GetScreenHeight())),
            texture_offset=w.camera.pos*Num32(0.5),
            tint=Color(0xff,0xff,0xc0,0xFF),
        )
        bg.draw(
            Vector2(0,0),
            Vector2(Num32(GetScreenWidth()), Num32(GetScreenHeight())),
            texture_offset=w.camera.pos*Num32(0.25) + Vector2(500,300),
            tint=Color(0xff,0xff,0xc0,0x80),
        )

        do
            w.camera.begin_drawing()
            defer w.camera.end_drawing()

            for l in w.letters
                l.draw()

            for s in w.satellites
                s.draw_beam()

            for b in w.boxes
                b.draw()

            for s in w.satellites
                s.draw()

            if goal := w.goal
                goal.draw()

            w.player.draw()

            for p in w.particles
                p.draw()


        if w.won_time
            draw_centered_text(GetScreenWidth()/2, GetScreenHeight()/2 + 70, "You Win!", color=Color(0x80,0xff,0x80))
        else if w.player.dead or (not w.player.has_signal and w.player.pos.dist(w.player.prev_pos) < Num32(1.0))
            draw_centered_text(GetScreenWidth()/2, GetScreenHeight()/2 + 70, "Press 'R' to Restart", color=Color(0xff,0x80,0x80))

    func load_map(w:@World, map:Text)
        w.boxes[] = []
        box_size := Vector2(25., 50.)
        star_textures := [Texture.load(t) for t in (./assets/WhiteStar*).glob()]
        for y,line in map.lines()
            for x,cell in line.split()
                pos := Vector2((Num32(x)-1) * box_size.x, (Num32(y)-1) * box_size.y)
                if cell == "[" or cell == "]"
                    box := @Box(pos, size=box_size)
                    w.boxes.insert(box)
                else if cell == "#"
                    box := @Box(pos, size=box_size, color=Color(0xe0,0x10,0x10), fatal=yes)
                    w.boxes.insert(box)
                else if cell == "-"
                    box := @Box(pos, size=box_size, color=Color(0xc0,0xc0,0xff,0x40))
                    w.boxes.insert(box)
                else if cell == "]"
                    pass # Ignored
                else if cell == "@"
                    pos += box_size/2 - Player.SIZE/2
                    w.player = @Player(pos,pos)
                else if cell == "?"
                    w.goal = Goal(pos)
                else if cell == "+"
                    w.satellites.insert(@Satellite(pos))
                else if cell != " "
                    w.letters.insert(Letter(CString(cell), pos))