Building incremental games as web applications
Have you ever wanted to make an idle clicker game like this?
Click at least three times.Good! Now try to get your number up to 100. Great job! Can you make it to 1,000?
I really like these kinds of games. There's something about the predictability of numbers-go-up combined with exploration and unfolding that my brain is deeply attracted to. Lately I've been experimenting with different kinds of clicker game ideas, and when I do, I reach for starter code to get me up and running quickly that is very similar to what we are building in this tutorial.
My definition of an idle clicker game (or "incremental" game) is one that has at least three features:
- It should have at least one way to generate a resource
- It should have at least one way to spend that resource
- It should have at least one way to automate the generation of resources
In this guide, I'll walk you through how to start building a basic web-based idle clicker game like the one above that leverages Vue, Vite, Pinia, and TypeScript. By using web technologies for our prototypes or production games, we gain all the affordances of a modern browser coupled with an unparalleled distribution strategy (e.g., share a link to the game instead of having someone download and install a game).
By the end of the tutorial, you'll have a foundation from which to build your own web-based incremental game—and hopefully some inspiration to start making it your own!
We cover the following topics:
- Setting up your project
- Defining types to organize the game's resources
- Defining a Pinia store to manage the game's state
- Helper functions for computing resource costs and affordability
- Vue components to display state data
A follow-on tutorial will expand on this template, and add to our game:
- Researchable items
- Unlockable items
- Event-based narration updates
For now, the goal is to keep it simple.
We'll be using the following computer instruction technologies:
Tool | To follow along, you should be... |
---|---|
Vue | Familiar (can read and understand what's going on) |
Vite | Somewhat familiar (have used or could use) |
Pinia | Vaguely familiar (have heard of) |
TypeScript | Familiar (can read and understand what's going on) |
Demo and code #
âŹď¸Ź If you'd like to skip the tutorial and just get started with the template, it's available here on GitHub.
đź’» You can also play the demo of what we're building before we begin.
Designing the game #
We're building a foundation from which to build our clicker game ideas, so it makes sense to think about what a "foundational" clicker game might be so we can figure out what needs to be built.
Every clicker game starts out with some thing to click. Clicking the thing generates some kind of resource, and a resource is something that can be exchanged for other things and/or resources. Additionally, there will be some kind of way to automation resource generation, sometimes called "auto-clickers", for one or more resources.
The game we're building in this tutorial will consist of these basic elements of an incremental game—which I have helpfully referenced here for you as The Three Laws of incremental games— implemented as energy, capacitors, and circuits.
Here's the general outline of what the game will consist of:
-
You generate energy. The core resource is Energy; you'll click a Create Energy button to create Energy.
-
Capacitors increase energy per click. You can spend energy to purchase (just another word for generate, if you think about it) capacitors. Each capacitor increases the amount of energy you generate per click.
-
Circuits auto-generate Energy. You can spend energy and capacitors on circuits. Each circuit automatically clicks the button that creates energy once per second. This allows players to generate energy even when they're not actively clicking the button (e.g., while they have your game running in a browser tab somewhere in the background).
When finished, we'll have a working prototype that is playable ad infinitum, with just enough scaffolding to start customizing and implementing functionality unique to the experience you want to create. It's a foundation for a game moreso than a working game demo per se; it's meant for you to finish this and then get to tinkering.
Setting up the project #
Let's start by setting up a project for our incremental game with create-vue
,
a tool for rapidly scaffolding a Vue project:
npm create vue@3
If you've never used create-vue
before, you'll be prompted to confirm
installation. Go ahead and proceed.
You'll be prompted to enter a project name. I'll be using pinia-clicker
for
the duration of this tutorial.
You'll be asked whether you want to install TypeScript. Since this demo uses TypeScript, choose yes.
As for the rest of the questions, the only thing I included was Pinia. Here's what my terminal looks like now that I'm done:
Vue.js - The Progressive JavaScript Framework
✔ Project name: … pinia-clicker
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … No / Yes
Scaffolding project in /Users/jesselawson/dev/pinia-clicker...
Done. Now run:
cd pinia-clicker
npm install
npm run dev
Follow the directions (cd
into your project dir and run the commands above).
When you're finished, you will have started the Vite development server and
should be able to open the link in your terminal to view your application.
With the project scaffolded, we're ready to get started building the game.
Building the game components #
Let's start out defining some types that will give the game resources some structure. From there, we'll create a new Pinia store to manage the game's state and state mutations (e.g., when someone clicks, increment a resource, when someone purchases a resource, subtract the costs correctly, etc). Finally, we'll create some Vue components to collect input and represent our game state to the player.
Defining types #
Since type systems are syntactic methods of enforcing software abstractions —and writing software involves reasoning about different abstractions and their relationships—let's start our game by defining some types.
In the src
folder of your project, create a new file called types.ts
.
The first type we'll create represents a resource cost:
export type Cost = {
[key: string]: number;
} & {
energy?: number;
capacitors?: number;
circuits?: number;
};
What is [key: string]: number } & {
?
This is an index signature. Generally, index signatures are used when you don't know the structure of a type ahead of time. We're using it here as part of an intersection type so that we can have a type of optional (but known) properties.
Technically, Cost
is an intersection type comprised of of an index signature
([key: string]: number
) and an object type with optional properties
({ energy?: number; capacitors?: number; circuits?: number; }
).
const someCost: Cost = { energy: 10, circuits: 5, // note that the `capacitors` property is omitted }; console.log(someCost.energy); // Output: 10 console.log(someCost.capacitors); // Output: undefined console.log(someCost.circuits); // Output: 5
Something in this game can cost any combination of the following:
- zero or more energy
- zero or more capacitors
- zero or more circuits
Next, let's create a type that represents the base costs of things that we can purchase:
export type BaseCosts = {
[key: string]: Cost;
} & {
energy?: Cost;
capacitors: Cost;
circuits: Cost;
};
Why is energy
optional?
Energy is the basic resource of the game, so I don't want it to cost anything. Since a cost of some combination of energy, capacitors, and circuits, the resulting base cost for what is essentially a free resource would be:
energy: { energy: 0, capacitors: 0, circuits: 0 } as Cost,
Making it optional here with the ?
operator will allow us to define base costs
for capacitors and circuits and elide a definition of energy costs.
Use this technique for other resources you add to the game that don't cost anything to produce (e.g., maybe you have a research tree that just costs time instead of resources).
Why do we need to define base costs?
The base costs are used along with a few other variables to calculate the costs of each purchase. The formula—sometimes called a "growth formula" or "cost function"—goes like this:
next cost = [base cost] * [cost multiplier]^[# owned]
Here, "cost multiplier" is synonymous with "rate of growth". For our game, we're
going to use a cost multiplier of 1.35
. This is a completely arbitrary number;
tweak it while you play your prototype until you settle on a value that feels
nice, just make sure it's always above 1.00
. The multiplier is the rate of
growth, so a larger multiplier is going to produce a more drastic exponential
increase than a smaller one.
To illustrate how this works, let's say we set the base cost for circuits to 25 energy and 10 capacitors. Using our formula, the first circuit will cost 25 energy and 10 capacitors:
next energy cost = 25 * 1.35^0 = 25 next capacitor cost = 10 * 1.35^0 = 10
After we purchase our first capacitor, the cost of the next capacitor will be:
next energy cost = 25 * 1.35^1 = 33 next capacitor cost = 10 * 1.35^1 = 13
The last type we'll define has to do with our game's state:
export type GameState = {
[key: string]: number;
} & {
energy: number;
capacitors: number;
circuits: number;
costMultiplier: number;
baseCosts: {
capacitors: Cost;
circuits: Cost;
};
};
These types give us an expandable structure from which to build an incremental game. In the next section, we'll use them to implement the core features of our game.
Managing state #
Let's now implement our state management system with Pinia.
By the end of this part, you will have:
- a fully functional Pinia store to manage state in your game, including initial state, getters for retrieving state, and actions for mutating state
What is Pinia and why are we using it?
Pinia is a state management library for Vue. With Pinia, you create a place to store your data aptly called a "store". Your stores have an initial state, getters for retrieving state data, and actions for mutating state data.
If you'd like to learn more about Pinia, I recommend the official documentation.
Inside the src
folder you'll find that create-vue
has setup a basic Hello
World project for us. Since we opted in to Pinia for state management during the
setup process, there is a src/stores/
folder with an example store counter
defined in counter.ts
.
Let's rename src/stores/counter.ts
to src/stores/gamestate.ts
. We'll use
this file to define a Pinia store that represents and manages our game's state.
Next, delete everything in that file, then import and use defineStore
to start
things off:
import { defineStore } from 'pinia'
export const gameState = defineStore({
id: 'gamestate',
Here we begin defining gameState
, something we'll import into other parts of
our game engine here to get and mutate state.
A Pinia store has, in addition to an id
, three main sections:
- the intitial state of the store, called
state
- methods to retrieve values from the store, called
getters
- methods to mutate values from the store, called
actions
We'll define these in order, starting with state
:
// Describe the initial state of our store:
state: () => ({
energy: 0, // Start at 0 energy
capacitors: 1, // Start with 1 capacitor
circuits: 0, // Start with 0 circuits
costMultiplier: 1.35, // Set rate of growth to 1.35
baseCosts: {
capacitors: {
energy: 3 // Capacitors base cost is
} as Cost, // 3 energy
circuits: {
energy: 5, // Circuits base cost is
capacitors: 5 // 5 energy and 5 capacitors
} as Cost
} as BaseCosts
} as GameState),
The state
property is where we describe our game's state— the variables we
want to keep track of and be able to pass into other parts of our game.
State property | What it's for |
---|---|
id |
The unique identifier for the store |
energy |
The total amount of energy generated in the game |
capacitors |
The number of capacitors purchased; each increase the amount of energy generated per click |
circuits |
The number of auto-clickers purchased; each automatically click one time per second per unit purchased |
That takes care of our game's initial state. Our next step is to define some methods to retrieve data from our game's state manager. For the game we are building, we need to know the following:
- the total amount of each resource that we've either generated or purchased
- the next cost of something that is able to be purchased
- whether or not we can afford that cost
Before we define these getters, though, let's build some helper functions based on the information we know we'll need.
Generating the next resource cost #
One thing we'll need to know throughout this game is the cost of the next resource we want to purchase. I'm using the word "resource" loosely here. In your game, you may call it an "upgrade" or "unlockable" or something like that. Either way, the basic premise is the same: I should be able to see the amount of other resources I will need to purchase something.
At the top of src/stores/gamestate.ts
, just after the import statement, let's
define a helper function that will calculate the next resource cost of a given
resource:
const generateNextCost = (state: any, resource: string): Cost => {
// Get the base cost for the resource:
const baseCosts = state.baseCosts[resource];
// Get how many of these resources we already own:
const currentResourceCount = state[resource];
// Iterate over baseCosts to dynamically calculate
// the next cost for each cost type (e.g., energy,
// capacitors, circuits). The reduce function helps us
// accumulate the next cost values into a new `Cost`
// object that we can then return:
return Object.keys(baseCosts as BaseCosts).reduce(
(nextCost: Cost, costKey: string) => {
// Get the base cost—or, if baseCosts[costKey] is falsy,
// default the base cost to zero:
const baseCost = baseCosts[costKey] || 0;
// Calculate the next cost by multiplying the
// base cost by the cost multiplier raised to the
// power of the current resource count, and wrap it
// in Math.floor() to ensure the result is an integer:
nextCost[costKey as keyof Cost] = Math.floor(
baseCost * Math.pow(state.costMultiplier, currentResourceCount),
);
return nextCost;
},
{},
);
};
With this function, we'll be able to pass along a reference to our game's
current state and the string name of something we can buy, then get a Cost
object in return containing the cost of that purchasable item:
// For example:
const nextCircuitCost = (state) => generateNextCost(state, "circuits");
The state
parameter will come from Pinia; each getter will be passed the
current state so we can always get the most current value of everything.
Checking if player can afford next resource #
Knowing the cost of the next purchase with generateNextCost
, we can now create
a second helper function to return whether we can actually afford it. This
second helper function will compare the next cost with the player's current
inventory, then return true
or false
base on whether the player can afford
it:
const canAffordNext =
// Check if the player can afford the next resource
// based on the current state:
(state: GameState, resource: string): boolean => {
// Generate the cost of the next resource based on the current state:
const nextCost = generateNextCost(state, resource);
// Check if player has enough of each cost key:
return Object.keys(nextCost).every(
// For every cost key, see if the matching state key
// is greater than or equal (if yes, then player
// can afford):
(costKey) => {
// Get the current count of the resource,
// and default to zero if it doesn't exist:
const currentResourceCount = state[costKey] || 0;
// Get the cost of the resource,
// and default to zero if it doesn't exist:
const cost = nextCost[costKey as keyof Cost] || 0;
// Return whether the current count is
// greater than or equal to the cost:
return currentResourceCount >= cost;
},
);
};
With these two helper functions, we're now ready to implement the getters for our Pinia store that holds are game's state.
Getter functions #
Our src/stores/gamestate.ts
file has two helper functions followed by the
beginning of our Pinia store. The store has the state configured, and we're now
ready to get started on the next section: the getter functions.
Getter functions are used to "get" variables from the state and bring them into our presentation logic—which, for this game, are Vue components.
Just like how the initial state is defined in a state
property, getters in a
Pinia store are defined in a getters
property. So let's start by creating a
getters
property that we can put our getters in, along with our first getter:
} as GameState),
// Define our getters:
getters: {
getEnergy: (state) => state.energy,
Our first getter returns the total amount of energy in our current state. Notice
how we provide state
as a parameter. In Pinia,
getters rely on state,
so any getter function should, at a minimum, expect a state
.
Since capacitors represent how much energy is generated per click and circuits represent how many clicks are automatically made per second, let's create a getter for each of those next:
// Define our getters:
getters: {
getEnergy: (state) => state.energy,
energyPerClick: (state) => state.capacitors,
energyPerSecond: (state) => state.circuits,
Now let's create some getters to provide resource costs.
We can start with a getter that leverages our generateNextCost
helper function
to use the state
and a name of a resource to calculate the next cost of
something:
// Define our getters:
getters: {
getEnergy: (state) => state.energy,
energyPerClick: (state) => state.capacitors,
energyPerSecond: (state) => state.circuits,
nextResourceCost: (state) =>
(resource:string) => generateNextCost(state, resource),
In the nextResourceCost
getter, we pass along the state and also the name of a
resource into generateNextCost
. While this is a good general-use getter, we
can create explicit getters for the resources we already know we'll need to know
the next cost of (i.e., capacitors and circuits) as well:
// Define our getters:
getters: {
getEnergy: (state) => state.energy,
energyPerClick: (state) => state.capacitors,
energyPerSecond: (state) => state.circuits,
nextResourceCost: (state) =>
(resource:string) => generateNextCost(state, resource),
nextCapacitorCost: (state) => generateNextCost(state, "capacitors"),
nextCircuitCost: (state) => generateNextCost(state, "circuits"),
Just like when we built the helper functions, we not only need to know what the next resource costs are but also whether we can afford the next resource. Let's build our final getter functions that leverage the other helper function:
// Define our getters:
getters: {
getEnergy: (state) => state.energy,
energyPerClick: (state) => state.capacitors,
energyPerSecond: (state) => state.circuits,
nextResourceCost: (state) =>
(resource:string) => generateNextCost(state, resource),
nextCapacitorCost: (state) => generateNextCost(state, "capacitors"),
nextCircuitCost: (state) => generateNextCost(state, "circuits"),
canAffordCircuit: (state) => canAffordNext(state, "circuits"),
canAffordCapacitor: (state) => canAffordNext(state, "capacitors"),
},
All of these getters will be used when we implement the game's Vue components (our presentation—or "view"—layer). But we're not quite ready to move on to our presentation logic just yet!
While getters are used to get data from our game's state, actions are used to mutate—or change—data in our game's state.
In the next section, let's define some actions that our game components will use to modify state values.
Action functions #
The third and final section of our Pinia store's definition is its actions. Actions are functions that modify state values.
There are three actions that can happen within our game's state: energy can be
generated, a capacitor can be purchased, and a circuit can be purchased. Each of
these actions will have their own separate function in our store's actions
property.
Let's go ahead and define them all now:
// Define actions that mutate state values:
actions: {
// If amt given, just generate that much energy.
// Otherwise, assume click
generateEnergy(amt?:number) {
if(amt) {
this.energy += amt
} else {
this.energy += this.energyPerClick
}
},
addCircuit() {
if(this.canAffordCircuit) {
this.energy -= this.nextResourceCost("circuits")?.energy ?? 0
this.capacitors -= this.nextResourceCost("circuits")?.capacitors ?? 0
this.circuits++
}
},
addCapacitor() {
if(this.canAffordCapacitor) {
this.energy -= this.nextResourceCost("capacitors")?.energy ?? 0
this.capacitors++
}
}
}
}) // End of pinia store definition
Notice how actions can call getters by using this
, like how addCircuit()
first checks if the player can even afford a circuit via
this.canAffordCircuit
.
Each of these actions are generally related to a button that the player can
press, i.e., addCapacitor
will be called when the player tries to buy a
capacitor and addCircuit
will be called when the player tries to buy a
circuit. The dual-purpose generateEnergy
function will either generate some
amount that's passed as the amt
parameter or however much we currently
generate per click.
With the state, getters, and actions all defined, our Pinia store is complete—and with it, most of the logic for our game!
In our next and final step, we'll bring all of this game logic onto the screen and build something we can start actually playing in the browser.
Game components #
Our final task is to bring all of what we've created so far together into the presentation layer of our app. To do that, we'll be building two Vue components: one for our main clicker button, and another for the current state of all our resources along with buttons to purchase capacitors and circuits.
By the end of this part, you will have:
- created a
Clicker
component that generates energy when clicked - created a
StoreDisplay
component that displays the current game state - integrated the Pinia store with the game components
Creating the Clicker
component #
Let's begin by creating the first button of the game.
First, go to the src/components
folder and delete the placeholder Vue files
(*.vue
). These are the files that came with the project when we first set it
up.
Then, create a new file called Clicker.vue
in the src/components
directory,
and add the following code:
<script setup lang="ts">
import { gameStateStore } from '@/stores/gamestate';
const gameState = gameStateStore();
</script>
<template>
<div>
<button @click="gameState.generateEnergy()">Generate Energy</button>
</div>
</template>
This creates a new Clicker
component with a button that generates energy when
clicked. Notice how we can invoke the Pinia store's actions in the template
after we import the store in the setup script.
Wait—what are the elements of a Vue component file?
Using TypeScript with Vue's Composition API, our files are going to be split into two parts (technical three, but more on that in a bit): the setup script and the template. They can be in any order. Review the Vue docs for more.
The setup script is defined in a script
tag, like this:
<script setup lang="ts">
//...
</script>
The template is defined in a template
tag, like this:
<template>
//...
</template>
Put them together, and you get a full Vue component file:
<script setup lang="ts">
//...
</script>
<template>
//...
</template>
There's a third, optional part to a Vue component file, which is a scoped style
block. You can include component-specific styling in a style
tag just like you
would on a normal HTML page. We'll be doing this in the last section of this
part.
This component gives us both our starting resource generation button and a template to use when you're ready to add another button like it.
With our primary button finished, let's now create a component to display all the game data variables we're managing with Pinia.
Creating the StoreDisplay
component #
The next component we'll create displays all the state variables we want the player to know about and includes two buttons for making purchases (capacitors and circuits).
Create a new file called StoreDisplay.vue
in the src/components
directory
and add the following code:
<script setup lang="ts">
import { gameStateStore } from '@/stores/gamestate';
const gameState = gameStateStore();
</script>
<template>
<div>
<p>Energy: </p>
<p>Capacitors: </p>
<p>Circuits: </p>
<p>Energy per click: </p>
<p>Energy per second: </p>
<button @click="gameState.addCapacitor" :disabled="!gameState.canAffordCapacitor">Purchase Capacitor (
<span v-if="gameState.nextCapacitorCost.energy !== undefined"> Energy </span>
<span v-if="gameState.nextCapacitorCost.capacitors !== undefined"> Capacitors </span>
<span v-if="gameState.nextCapacitorCost.circuits !== undefined"> Circuits </span>
)</button>
<br/>
<button @click="gameState.addCircuit" :disabled="!gameState.canAffordCircuit">Purchase Circuit (
<span v-if="gameState.nextCircuitCost.energy !== undefined"> Energy </span>
<span v-if="gameState.nextCircuitCost.capacitors !== undefined"> Capacitors </span>
<span v-if="gameState.nextCircuitCost.circuits !== undefined"> Circuits</span>
)</button>
</div>
</template>
Here we being the template by outputing some variables from our game's state,
then we provide two buttons. Each button has a set of span
elements for a
label; the v-if
directive will only display the element if the given condition
true. These spans assemble the full cost of something based on whether or not it
has any energy, capacitors, or circuits costs associated with it.
Let's deconstruct the second button that adds a circuit to see how it works.
<button @click="gameState.addCircuit" :disabled="!gameState.canAffordCircuit">Purchase Circuit (
First we create a button element, assign the addCircuit
action from the store
to a click event, and disable the button if ever canAffordCircuit
is false.
The button's text label starts out with "Purchase Circuit (" and then we move on
to the dynamic span elements for cost:
<span v-if="gameState.nextCircuitCost.energy !== undefined"> Energy </span>
If the next circuit cost's energy property is not undefined (i.e., if there is an energy cost associated with purchasing the next circuit), then output the amount followed by the word "Energy."
This pattern is repeated for the potential capacitor and circuit costs as well:
<span v-if="gameState.nextCircuitCost.capacitors !== undefined"> Capacitors </span>
<span v-if="gameState.nextCircuitCost.circuits !== undefined"> Circuits</span>
)</button>
Then at the end, we close the button element.
Our component implementations are now complete, so now let's register them with our app and do some end-of-tutorial tidying up to finish our game template. We're almost done!
Integrating the components into the game #
With our two components built, let's now integrate them into our Vue app so that we can tinker and interact with what we've built so far.
Open the App.vue
file and replace the existing code with the following:
<script setup lang="ts">
import Clicker from '@/components/Clicker.vue'
import StoreDisplay from '@/components/StoreDisplay.vue'
</script>
<template>
<div id="app">
<Clicker />
<StoreDisplay />
</div>
</template>
<style>
#app {
display: flex;
justify-content: center;
align-items: top;
height: 100vh;
}
#app > * {
margin: 1rem;
}
</style>
The template here is where we lay out how the game appears—which, for the
purpose of being a template, is very basic. This component is used in the
project's main.ts
file to define our app and then mount it to an HTML element.
At this point, you're ready to play your game!
Start the dev server, and open the link to localhost
emitted in your terminal
window. For example, mine was:
npm run dev
VITE v4.3.9 ready in 360 ms
âžś Local: http://localhost:5174/
âžś Network: use --host to expose
âžś press h to show help
Everything should be working as expected—but as soon as you purchase a circuit
(technically, one second after), you'll see we still have some more work left to
do. We need a way to account for changes to energy every second. We can do that
in the browser with
setTimeout()
,
but we're actually going to roll our own version
more appropriate for a game's needs.
Remember back in src/stores/gamestate.ts
when we created those two helper
functions, generateNextCost
and canAffordNext
? I'm about to introduce a
third helper function we'll use instead of setTimeout()
, which means there's
opportunity now to clean up this game template we're writing by creating a
dedicated space for helper functions. I wont put the two we've already created
in there so that you have something to do in case you're out of ideas but still
want to feel productive.
Let's create a new file called src/helpers.ts
, and in it, define
setTimeout2
:
export const setInterval2 = (fn: VoidFunction, time: number) => {
// A place to store the timeout Id (later)
let timeout: any = null;
// calculate the time of the next execution
let nextAt = Date.now() + time;
const wrapper = () => {
// calculate the time of the next execution:
nextAt += time;
// set a timeout for the next execution time:
timeout = setTimeout(wrapper, nextAt - Date.now());
// execute the function:
return fn();
};
// Set the first timeout, kicking off the recursion:
timeout = setTimeout(wrapper, nextAt - Date.now());
// A way to stop all future executions:
const cancel = () => clearTimeout(timeout);
// Return an object with a way to halt executions:
return { cancel };
};
If you're curious about why I wrote a previous blog post about this function here.
To get our circuits generating energy every second, go to App.vue
, import our
new helper, and use Vue's onMounted
method to handle passive energy production
with our helper's help:
<script setup lang="ts">
import Clicker from '@/components/Clicker.vue'
import StoreDisplay from '@/components/StoreDisplay.vue'
import { gameStateStore } from './stores/gamestate';
import { setInterval2 } from '@/helpers';
import { onMounted } from 'vue';
const gameState = gameStateStore();
onMounted( () => {
setInterval2(()=>{
gameState.energy += gameState.energyPerSecond * gameState.circuits
}, 1000);
});
</script>
<template>
<div id="app">
<Clicker/>
<StoreDisplay/>
</div>
</template>
<style>
#app {
display: flex;
justify-content: center;
align-items: top;
height: 100vh;
}
#app > * {
margin: 1rem;
}
</style>
So every 1000ms, add the product of gameState.energyPerSecond
and
gameState.circuits
to gameState.energy
.
Vue's onMounted
lifecycle hook
docs go into
more detail, but basically, when the component is ready to have things done to
it, it invokes this method and our timer starts.
Run the development server again, and buy a circuit. You should see energy accumulating passively.
Congratulations! You finished this tutorial, and in doing so, built your very own incremental game template. What feature will you add to it next? What game idea do you want to prototype and play around with? I'm excited to see what you build, so don't hesitate to reach out and let me know when your game is ready to share.
Where to go from here #
The basic features are here, but there is so much more than just generating resources and purchasing items that makes a great incremental game. What's the next feature to implement? Where do you go from here? Well, I have a few ideas:
- Remember those two helper functions we created in the Pinia store file? Go put those into our dedicated helpers file, and update the imports appropriately so that the game still runs.
- In Action functions, the logic for calculating the costs of both circuits and capacitors is hard-coded to assume that circuits cost some combination of energy and capacitors, and that capacitors only cost zero or more energy. How could you modify so that you don't have to hard-code these cost relationships?
- How would you implement a way to show (or hide) unlockable items based on state conditions (e.g., unlock something when you have X circuits)?
- How would you implement research items, with research time and in-progress updates?
- How would you implement a new resource "Electricity" that costs X energy per
second? (how would you change
Cost
and how it's used?)
Some of these things will be available in the next version of this tutorial—a free online book, just like my other ones.
All source code for this project is available here.
Questions? Comments? Feedback?
Please open an issue or reach out on Mastodon.
If you enjoyed this tutorial, feel free to show your support by buying me a coffee, starring the demo project on GitHub, or saying hello on Mastodon.
Have fun!
--Jesse