Available for:
#1 Gameplay
Gameplay overview
Overview

The only resource in the game is cards: found inside chests and dropped from defeated enemies.

The game is turn based, with each action (moving, playing or discarding a card, etc.) counting as one turn.

Player movement animation Player nudging animation
Controls

The player moves using the arrow keys. Objects can be interacted by nudging them.

Holding down a key allows for continuous movement to speed up traversing long corridors.

Card play animation Target selection animation
Using cards

Cards are selected and played using drag-and-drop. Targets are picked using left-click for cards that need them. Targeting can be cancelled with right-click.

Cards can be discarded by drag-and-dropping them onto the discard pile.

Slime Spider Voidling Ranger Exterminator
Enemies

The prototype features 4 common enemy types and 1 boss, each having their own logic, core stats, and spawn conditions.

Cardium's lobby and main menu
Interactive main menu

This area serves both as the main menu and the safe zone. The player can interact with objects to perform actions like managing their inventory, changing the difficulty, viewing statistics, or exiting the game.

#2 Technical details

Utils.GenerateLoot(

  amount: 3

  dropRate: new Dictionary<Type, int> {

    { typeof(SmiteCard), 40 },

    { typeof(ScoutCard), 40 },

    { typeof(ChainCard), 20 },

    { typeof(TeleportCard), 20 },

    { typeof(WoodenKeyCard), 5 },

    { typeof(GoldenKeyCard), 5 },

  }

);

Loot

Loot is generated using the GenerateLoot() function, which takes a list of card types and their drop rates, and based on the amount parameter, returns a list of Card instances, where the distribution follows the dictionary values.

This is an elegant way for generating loot for chests and enemies.

Criteria: the input dictionary must contain at least one type that can be cast to Card.

A section of pretty walls from the game
Pretty walls

Walls are just solid cells by default. To make them visually more appealing, one can use a sprite sheet that contains textures for all possible wall orientations.

In some styles (such as in this game), the texture used for a certain cell depends on the surrounding 8 tiles. Since each tile can be either wall (true) or empty (false), the total amount of possible surroundings is 28, which is 256. However, some wall textures can be re-used for multiple scenarios, as their layout does not depend on certain cells.

The figure below illustrates how a certain wall texture can be applied in multiple scenarios. In the top-right corner of the image, surrounding cells are categorized as follows:

  • true: must be wall
  • false: must be empty
  • null: can be either
The wall sprite sheet and an explanation of neighbor rules

To address all 256 possible combinations, a Dictionary can be constructed, where keys are 8-bit masks representing the state of the surrounding tiles (empty ➝ 0, wall ➝ 1), and values are the corresponding wall texture's atlas coordinates.

Instead of populating the dictionary by hand, the above-mentioned patterns, consisting of the true, false, and null values, can be used to describe conditions for each texture found in the sprite sheet. Then, an algorithm can generate the dictionary entries that satisfy the conditions.

AddBitmaskEntries(

  atlasCoords: new Vector2I(3, 1),

  pattern: new List<bool?>() {

    null,  falsenull,

    true,         true,

    falsetrue,  true

  }

);

This is what the AddBitmaskEntries() function does, which takes the atlas coordinates of a texture (atlasCoords) and a pattern of 8 nullable booleans (soon to be converted to a bitmask), where each value represents the state of a surrounding tile in the order shown in the bottom-right corner of the image. The function will then generate all possible surrounding variations that matches the pattern, and add them to the dictionary.

var bitmask = GetWallBitmask(x, y);

var atlasCoords = bitmaskToWallAtlasCoords[bitmask];

WallLayer.SetCell(new Vector2I(x, y), 0, atlasCoords);

Finally, the dictionary can be easily used to access the appropriate texture by providing the bitmask that describes a cell's surroundings.

Animation showing the dungeon generation steps
Dungeon generation

The process is an extended version of the algorithm from Bob Nystrom.

First, a bunch of rectangular rooms are placed, which later get connected via mazes and doorways. Dead ends are removed to improve the exploration experience.

As the final step, enemies and objects are placed into each room. Their number, level, and type depend on the room size, difficulty level, and other factors.