Back

The Last of Us: Between The Years

Side Scroller / Action / Adventure - June 2024

Play The Last of Us: Between The Years on itch.io

OVERVIEW

Timeline

16 weeks

May - Aug '24

Genre

Side Scroller

Action

Adventure

Responsibilities

Lead Developer

Lead Designer

Gameplay Programmer

Project Manager

Tech

GB Studio

GBVM

Narrative Overview

The Last of Us: Between The Years takes place in the 20 years following Sarah's death and before the main events of the first game. It delves into Joel's character during this period and his grieving process. Explore the relationships Joel forms during this time and uncover some unexpected secrets hidden within the universe of The Last of Us. This game does not contain any spoilers for the main storyline of Part 1 or Part 2.

CREATION

Moodboard

The game needed to feel like The Last of Us, even without saying the name. Joel's character was key, but translating him into a GameBoy style bitmap was what really sold the connection. This is a unique experience set in new locations like Los Angeles with the moodboard reflecting that mix of the new and familiar and it's that crossover that makes this game its own both narratively and stylistically.

Design

My installment to the game franchise required familiar characters like Tess and Tommy, not only to preserve The Last of Us theme but also to give me the opportunity to explore new areas, characters, and themes that weren't fully explored in the main games.

I believe in the value of interactions between characters, where exploring these moments personally adds depth to the experience. Many interactions are missable, but when you do engage with one, it offers a unique glimpse into the characters and the world. It’s these rewarding moments that make games, and their characters, memorable.

Inspired by Ish from Part 1, the world in my game has its own story to tell. Beyond adding depth to the new characters, it hints at what’s to come or offers subtle nods to the original games. I aim to reward exploratory players not only with resources but also with additional layers of storytelling.

Even with the GameBoy’s limited hardware, accessibility remains a priority. I’ve implemented an interactive hint and help system that provides players with game or story assistance when they get stuck.

Beyond in-game cinematics, my game features larger, more dramatic cutscenes that elevate key story moments. These scenes are designed to be impactful, keeping players engaged. The scene ‘IT IS OVER TESS!’ heavily influenced my approach, shaping my decision to make these cinematics rich in character expression and dialogue.

The game introduces its mechanics naturally through gameplay, with contextual prompts and interactive learning moments guiding players. If necessary, your companions may offer helpful suggestions to keep you on track.

Familiar characters

In-Game Cutscenes

World Interactions

Accessibility

Cutscenes

Built-In Tutorials

Programming

Since Between The Years was made using GB Studio, a visual scripting engine, all code shown here has been manually translated into JavaScript to give a clearer look into the game’s logic.

attack.js

inventory.js

upgrade.js


//Example usage
const player = new Player();
const inputManager = new InputManager(player);

const revolver = new Weapon('Revolver', 6, 10);
player.equipWeapon(revolver);

inputManager.start();

// Snippets of classes are below for further context
class HoldableItem {
    constructor(name, maxAmount, type){
        this.name = name;
        this.maxAmount = maxAmount;
        this.type = type;
    }

    use() {
        //Overridden by inherited class
    }
}

class Weapon extends HoldableItem {
    constructor(name, ammo, storedAmmo, clipSize, reloadSpeed) {
        super(name, 1, "weapon");
        this.name = name;
        this.ammo = ammo;
        this.storedAmmo = storedAmmo;
        this.clipSize = clipSize;
        this.isLoaded = ammo > 0;
        this.reloadSpeed = reloadSpeed;
    }

    use() {
        if (this.isLoaded) {
            this.shoot();
        } 
        else {
            this.reload();
        }
    }

    shoot() {
        if (this.ammo > 0) {
            this.ammo--;
            bullet = new Bullet(this.name);
            bullet.sendProjectile();
            
            AnimationManager.playAnimation("shoot");
        }
    }

    reload() {
        if (this.ammo === 0 && this.storedAmmo > 0) {            
            let timer = this.reloadSpeed;

            let reloadInterval = setInterval(() => {
                timer--;

                if (timer <= 0){
                    clearInterval(reloadInterval);

                    AnimationManager.playAnimation("reload");

                    if (storedAmmo >= clipSize) {
                        this.ammo = this.clipSize;
                    }
                    else {
                        this.ammo = this.storedAmmo;
                    }
                    
                }

            }, 1000);
        }
    }
}

class Player {
    constructor() {
        this.weapon = null;
        this.consumable = null;
        this.isPunching = false;
    }

    equipWeapon(weapon) {
        this.weapon = weapon;
    }

    equipConsumable(consumable) {
        this.consumable = consumable;
    }

    attack() {
        if (!this.weapon && !this.consumable) {
            this.punch();
        } 
        else if (this.consumable) {
            this.consumable.use();
        } 
        else if (this.weapon) {
            this.weapon.use();
        }
    }

    punch() {
        AnimationManager.playAnimation("punch");
    }
}

class InputManager {
    constructor(player) {
        this.player = player;
    }

    start(){
        this.handleInput('A');
    }

    handleInput(buttonPress) {
        if (buttonPress === 'A') {
            this.player.attack();
        }
    }
}

                    

//Example usage
const player = new Player();
const inputManager = new InputManager(player);

const revolver = new Weapon('Revolver', 6, 10);
const medKit = new Medkit(2, 4);

player.equipWeapon(revolver);
player.equipConsumable(medKit);

inputManager.start();

// Snippets of classes are below for further context
class Consumable {
    constructor(name, quantity) {
        this.name = name;
        this.quantity = quantity;
    }

    use() {
        //Overridden by inherited class
    }
}

class Medkit extends Consumable {
    constructor(quantity, healTime) {
        super("Medkit", quantity);
        this.healTime = healTime;
    }

    use(player) {
        if (this.quantity > 0 && player.health < 100){
            this.quantity --;

            let timer = this.healTime;

            const healInterval = setInterval(() => {
                timer--;

                if (timer <= 0) {
                    clearInterval(healInterval);
                    player.health = 100;
                }
            }, 1000);
        }
    }
}

class Player {
    constructor() {
        this.weapons = [];
        this.consumables = [];
        this.itemInHand = null;
        this.health = 100;
    }

    equipWeapon(weapon) {
        this.weapons.push(weapon);
        if (!this.itemInHand) {
            this.itemInHand = weapon;
        }
    }

    equipConsumable(consumable) {
        this.consumables.push(consumable);
        if (!this.itemInHand) { 
            this.itemInHand = consumable;
        }
    }

    cycleWeapon() {
        if (this.weapons.length === 0) return;
        const currentIndex = this.weapons.indexOf(this.itemInHand);
        const nextIndex = (currentIndex + 1) % this.weapons.length;
        this.itemInHand = this.weapons[nextIndex];
    }

    cycleConsumable() {
        if (this.consumables.length === 0) return; 
        const currentIndex = this.consumables.indexOf(this.itemInHand);
        const nextIndex = (currentIndex + 1) % this.consumables.length;
        this.itemInHand = this.consumables[nextIndex];
    }
}

class InputManager {
    constructor(player) {
        this.player = player;
    }

    start(){
        this.handleInput('UP');
        this.handleInput('DOWN');
    }

    handleInput(buttonPress) {
        if (buttonPress === 'UP') {
            this.player.cycleWeapon();
        }
        else if (buttonPress === 'DOWN'){
            this.player.cycleConsumable();
        }
    }
}

                    

//Example usage
// This usage is heavily simplified
const player = new Player();

const revolver = new Weapon('Revolver', 6, 8, 10);
player.equipWeapon(revolver);

player.addParts(15);

const bench = new UpgradeBench(player);
bench.upgradeWeapon(revolver); // Upgrades to tier 2

// Snippets of classes are below for further context
class UpgradeBench {
    constructor(player) {
        this.player = player;
    }

    upgradeWeapon(weapon) {
        if (!weapon) return;

        const upgradeCost = weapon.getUpgradeCost();

        if (weapon.tier != 3){
            if (this.player.parts >= upgradeCost) {
                this.player.parts -= upgradeCost;
                weapon.upgrade();
            } 
            else {
                console.log("You do not have enough parts.");
            }
        }
        else {
            console.log("Weapon is fully upgraded.");
        }
        
    }
}

class Weapon {
    constructor(name, clipSize, damage, reloadSpeed) {
        this.name = name;
        this.clipSize = clipSize;
        this.damage = damage;
        this.reloadSpeed = reloadSpeed;
        this.tier = 1;
    }

    getUpgradeCost() {
        const costs = [10, 13, 17];
        return costs[this.tier - 1] || 0;
    }

    upgrade() {
        this.clipSize += 2;
        this.damage += 5;
        this.reloadSpeed *= 0.9;

        this.tier++;
    }
}

class Player {
    constructor() {
        this.weapon = null;
        this.parts = 0;
    }

    equipWeapon(weapon) {
        this.weapon = weapon;
    }

    addParts(parts) {
        this.parts += parts;
    }
}

                    

output.html

How It Works

The attack system handles punching, shooting, and using consumables. When the player presses the attack button the system performs checks. If no weapon or consumable is equipped, the player punches. If a consumable is equipped, it’s used. If a weapon is equipped, the system checks if it’s loaded; if so, it fires and decreases the ammo. If not loaded, the system initiates a reload.

The inventory system keeps track of the player’s weapons and consumables. Pressing the up arrow cycles through weapons and equips the next one. Pressing the down arrow does the same for consumables. If the player runs out of items in a category, the system loops back to having nothing equipped. Switching between weapons and consumables updates the item in the player’s hand.

The upgrade system allows the player to improve their weapons at upgrade benches. Each weapon has three tiers, and upgrading increases clip size, damage, and reload speed. When the player interacts with a bench, the system checks if they have enough parts to pay for the next tier and if the weapon is not already maxed. If it passes, the parts are deducted and the weapon is upgraded.

DISCLAIMER

Project Affiliation

This is a fan-made game set within the universe of The Last of Us, developed independently by Kyle Jussab. This project is not affiliated with or endorsed by Naughty Dog, the creators of The Last of Us franchise. I do not claim ownership of any intellectual property belonging to Naughty Dog, including characters, settings, or storyline elements. This game is created out of admiration for the original work and is intended as a non-commercial, fan-made tribute. I do not seek to profit from the use of Naughty Dog's intellectual property. All trademarks and copyrights pertaining to The Last of Us belong to their respective owners.

FEEDBACK

NEXT GAME

03 | GAMEPLAY PROGRAMMER & GAME DESIGNER

A ball on a tower

Project Phoebe

Master your skills getting through rooms by competing in 5 different modes, all of which challenge you in a different and unique way.

Release

April 2023

Genre

Third Person

Platformer

Puzzler

Tech

Unity

C#