(https://eliasdaler.github.io/assets/tomb_painter_first_dev_log/tomb_painter_logo.png)
Site: https://eliasdaler.github.io/
Developer: Elias Daler (https://twitter.com/EliasDaler) - everything
Dev logs: https://eliasdaler.github.io/tags/#Tomb+Painter
Twitter: @eliasdaler (http://twitter.com/eliasdaler)
Platforms: PC, Mac, Linux
Start of development: August 2017
Release date: 202X (let's hope it's not 203X)
Hello, everyone! My name is Elias Daler and I'm making a game called Tomb Painter.
You may know me from Re:creation (https://en.sfml-dev.org/forums/index.php?topic=18062.0) (which is now in hiatus), and here I am with another game.
In the game, a painter arrives on mysterious island, which has strange things going on with it. Blob-shaped monsters made of paint start to appear from an ancient tomb. The only way to stop this is to paint beautiful patterns which were washed away by a flood that happened some time ago.
The main mechanic is that you can draw on floors and paint stuff by hitting it with your brush. By painting on the floor, you solve different puzzles.
(https://eliasdaler.github.io/assets/tomb_painter_first_dev_log/gameplay.gif)
(https://i.imgur.com/evee0Fa.gif)
(https://i.imgur.com/muvTDv6.gif)
The game is done in 4 colors (technically it's sometimes 8, because it has two palettes from time to time) and in 160x144 resolution, just like Game Boy!
(https://i.imgur.com/XzQA3CN.gif)
Spikes! I'm starting to work on some dungeon puzzles, yay :)
Here's their script. Pretty cool how easy they were to make with all the systems my engine has.
local EntityState = require("EntityState")
local coroutineManager = require 'Managers'.coroutineManager
local actions = require 'actions.wrappers'
local delay = actions.delay
local waitForAnimationEnd = actions.waitForAnimationEnd
local SPIKES_DELAY_TIME = 2.0
-- SpikesHiddenState
local SpikesHiddenState = EntityState:subclass("SpikesHiddenState")
function SpikesHiddenState:enter(e, args)
e:setAnimation("hidden")
e:setComponentEnabled("damage", false)
coroutineManager:launch(
function()
delay(SPIKES_DELAY_TIME)
e:changeState("SpikesRisingState")
end
)
end
-- SpikesRisingState
local SpikesRisingState = EntityState:subclass("SpikesRisingState")
function SpikesRisingState:enter(e)
e:setComponentEnabled("damage", true)
coroutineManager:launch(
function()
e:setAnimation("rising")
waitForAnimationEnd(e)
e:changeState("SpikesRisedState")
end
)
end
-- SpikesRisedState
local SpikesRisedState = EntityState:subclass("SpikesRisedState")
function SpikesRisedState:enter(e)
e:setAnimation("rised")
coroutineManager:launch(
function()
delay(SPIKES_DELAY_TIME)
e:changeState("SpikesDescendingState")
end
)
end
-- SpikesDescendingState
local SpikesDescendingState = EntityState:subclass("SpikesDescendingState")
function SpikesDescendingState:enter(e)
coroutineManager:launch(
function()
e:setAnimation("descending")
waitForAnimationEnd(e)
e:changeState("SpikesHiddenState")
end
)
end
return {
SpikesHiddenState,
SpikesRisingState,
SpikesRisedState,
SpikesDescendingState,
}
But with coroutine system I can also write something like this:
coroutineManager:launch(
function()
while true do
e:setAnimation("hidden")
e:setComponentEnabled("damage", false)
delay(SPIKES_DELAY_TIME)
e:setComponentEnabled("damage", true)
e:setAnimation("rising")
waitForAnimation(e)
e:setAnimation("rised")
delay(SPIKES_DELAY_TIME)
e:setAnimation("descending")
waitForAnimation(e)
end
end
)
... but for now I think I'll go with good old state based approach, which I think it more readable.
Added filtering for prefab names. Now I can find prefabs and create them faster:
(https://i.imgur.com/E9deNxj.gif)
ImGui is so awesome - I only had to write this code to have this work:
static ImGuiTextFilter filter;
filter.Draw("Filter");
if (ImGui::ListBoxHeader("Prefab")) {
for (const auto& prefabName : prefabNames) {
if (filter.PassFilter(prefabName.c_str())) {
if (ImGui::Selectable(prefabName.c_str(), selectedPrefabName == prefabName)) {
selectedPrefabName = prefabName;
selected = true;
}
}
}
ImGui::ListBoxFooter();
}
Working on quests - tried to simplify stuff as much as possible, and put it in one script per quest. And it works well! Here's an example of quest state:
InHouseState.entityStates = {
luna = function(e, quest)
setActionListOnInteraction(e, beforeKill, quest)
end
}
InHouseState.sceneEnter = {
luna_house_basement = function(quest)
saveManager:setQuestData(QUEST_TAG, "went_to_basement", true)
spawnByTag("basement_slime") -- spawn a boss
end
}
InHouseState.sceneExit = {
luna_house_basement = function(quest)
despawnByTag("basement_slime") -- despawn a boss
end
}
entityStates is used to describe what function should be called on an entity when it appears on scene. In this case, when "luna" appears on the scene, she gets a new interaction function which will launch a specific cutscene.
sceneEnter/sceneExit are used to describe what happens when you enter/exit the scene in the specific quest state. In this case, it's used to spawn a boss in "luna_house_basement" until you kill it (and then the quest state changes and different script plays each time you enter the basement)
Here's what's good about this mechanism - entities and scenes don't know anything about quests - quest itself does the things when it's in corresponding state, so I can easily add more and more logic to them without touching scene/NPC script - it's all encapsulated into quest script!
Another thing I've added in an event logger. It allows me to subscribe to events of needed type and then inspect the data in each event. Very useful for debugging!
(https://i.imgur.com/zjPyji8.png)
Here it is in action:
https://gfycat.com/sardonicmenacinggrackle