package main
import "core:fmt"
import "core:math/rand"
import os "core:os/os2"
import "core:strconv"
import "core:time"
import "./console"
// TODO: Refactor combat (only use monster array for base values)
// TODO: Colors
// TODO: TUI
// TODO: Refactor message history
// TODO: Add randomised varied messages
// TODO(console): need major cleanup, better color and drawing support (virtual bitmap), functional non-blocking keyboard input, etc
// TODO(console): look over signal management
// TODO: Print final statistics on stop/quit
// TODO: Save progress
/*
// Source - https://stackoverflow.com/a
// Posted by Lucas S.
// Retrieved 2026-01-14, License - CC BY-SA 3.0
int khbit() const
{
struct timeval tv;
fd_set fds;
tv.tv_sec = 0;
tv.tv_usec = 0;
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
select(STDIN_FILENO+1, &fds, NULL, NULL, &tv);
return FD_ISSET(STDIN_FILENO, &fds);
}
void nonblock(int state) const
{
struct termios ttystate;
tcgetattr(STDIN_FILENO, &ttystate);
if ( state == 1)
{
ttystate.c_lflag &= (~ICANON & ~ECHO); //Not display character
ttystate.c_cc[VMIN] = 1;
}
else if (state == 0)
{
ttystate.c_lflag |= ICANON;
}
tcsetattr(STDIN_FILENO, TCSANOW, &ttystate);
}
bool keyState(int key) const //Use ASCII table
{
bool pressed;
int i = khbit(); //Alow to read from terminal
if (i != 0)
{
char c = fgetc(stdin);
if (c == (char) key)
{
pressed = true;
}
else
{
pressed = false;
}
}
return pressed;
}
int main()
{
nonblock(1);
int i = 0;
while (!i)
{
if (cmd.keyState(32)) //32 in ASCII table correspond to Space Bar
{
i = 1;
}
}
nonblock(0);
return 0;
}
*/
Segment_Kind :: enum {
Empty,
Monster,
Treasure,
}
Segment_Data :: union {
Empty,
Monster,
Treasure,
}
Segment :: struct {
data: Segment_Data,
}
Glyph :: struct {
glyph: rune,
// color
// options
}
Empty :: struct {
using _: Glyph,
}
Hero :: struct {
using _: Glyph,
state: Hero_State,
level: int,
hp: int,
max_hp: int,
base_atk: int,
base_def: int,
xp: int,
xp_to_level: int,
weapon_tier: int,
armor_tier: int,
weapon_bonus: int,
armor_bonus: int,
gold: int,
}
Hero_State :: enum {
Walking,
Combat,
Dead,
}
Monster :: struct {
using _: Glyph,
name: string,
hp: int,
atk: int,
def: int,
xp_reward: int,
weight: int,
floor_requirement: int,
}
Treasure :: struct {
using _: Glyph,
kind: Treasure_Kind,
tier: int,
value: int,
}
Treasure_Kind :: enum {
Heal,
Gold,
Weapon,
Armor,
}
MONSTERS := []Monster {
{glyph = 'g', name = "Goblin", hp = 10, atk = 3, def = 0, xp_reward = 5, weight = 30, floor_requirement = 1},
{glyph = 's', name = "Slime", hp = 1, atk = 1, def = 0, xp_reward = 1, weight = 40, floor_requirement = 0},
{glyph = 'o', name = "Orc", hp = 20, atk = 6, def = 2, xp_reward = 12, weight = 5, floor_requirement = 7},
{glyph = 'M', name = "Minotaur", hp = 40, atk = 10, def = 4, xp_reward = 35, weight = 1, floor_requirement = 9},
}
TREASURE := []Treasure {
{glyph = '♡', kind = .Heal},
{glyph = '$', kind = .Gold},
{glyph = 'W', kind = .Weapon},
{glyph = 'A', kind = .Armor},
}
generate_floor :: proc(segments: []Segment, current_floor: int, floor_weights: [Segment_Kind]int) {
floor_total_weights: int
for w in floor_weights {
floor_total_weights += w
}
monster_total_weights: int
for m in MONSTERS {
if current_floor >= m.floor_requirement {
monster_total_weights += m.weight
}
}
for &segment in segments {
segment_kind: Segment_Kind
roll := rand.int_max(floor_total_weights)
running := 0
for w, i in floor_weights {
running += w
if roll < running {
segment_kind = i
break
}
}
switch segment_kind {
case .Empty:
segment.data = Empty {
glyph = '-',
}
case .Monster:
monster: Monster
m_roll := rand.int_max(monster_total_weights)
m_running := 0
for m in MONSTERS {
if current_floor < m.floor_requirement {
continue
}
m_running += m.weight
if m_roll < m_running {
monster = m
break
}
}
monster.hp += current_floor * 2
monster.atk += current_floor
if current_floor % 2 == 0 {
monster.def += current_floor
}
segment.data = monster
case .Treasure:
treasure := TREASURE[rand.int_max(len(TREASURE))]
if roll >= 95 {
#partial switch treasure.kind {
case .Gold:
treasure.value += current_floor * 2
case .Weapon:
treasure.tier = current_floor / (2 + (rand.int_max(2) - 1))
treasure.value = treasure.tier * 3
case .Armor:
treasure.tier = current_floor / (2 + (rand.int_max(2) - 1))
treasure.value = treasure.tier * 2
}
} else {
#partial switch treasure.kind {
case .Gold:
treasure.value += current_floor
case .Weapon:
treasure.tier = current_floor / (3 + (rand.int_max(2) - 1))
treasure.value = treasure.tier * 2
case .Armor:
treasure.tier = current_floor / (3 + (rand.int_max(2) - 1))
treasure.value = treasure.tier
}
}
segment.data = treasure
}
}
}
main :: proc() {
ticks_per_second := 5
if len(os.args) > 1 {
for arg, i in os.args {
if arg == "-ticks" {
if i + 1 >= len(os.args) {
continue
}
ticks := strconv.parse_int(os.args[i + 1]) or_else 5
ticks_per_second = ticks if ticks > 0 else ticks_per_second
}
}
}
con := console.create()
defer console.destroy(&con)
current_floor := 0
floor_weights := [Segment_Kind]int {
.Empty = 60,
.Monster = 25,
.Treasure = 10,
}
floor_segments := [30]Segment{}
generate_floor(floor_segments[:], current_floor, floor_weights)
hero := Hero {
glyph = '@',
state = .Walking,
level = 1,
hp = 20,
max_hp = 20,
base_atk = 3,
base_def = 1,
xp = 0,
xp_to_level = 10,
weapon_tier = 0,
armor_tier = 0,
weapon_bonus = 0,
armor_bonus = 0,
gold = 0,
}
deaths := 0
hero_position := 0
hero_attacking := true
message_history: [128]string
message_history_slot: int
ticks_ms := time.Duration(1000 / ticks_per_second) * time.Millisecond
last_tick := time.now()
running := true
for running {
now := time.now()
elapsed := time.diff(last_tick, now)
if elapsed > ticks_ms {
// LOGIC
switch hero.state {
case .Walking:
hero_position += 1
if hero.hp < hero.max_hp {
hero.hp += 1
}
if hero_position >= len(floor_segments) {
current_floor += 1
hero_position = 0
message_history[message_history_slot] = "Found a stairwell, going deeper..."
message_history_slot = (message_history_slot + 1) % len(message_history)
generate_floor(floor_segments[:], current_floor, floor_weights)
}
switch v in floor_segments[hero_position].data {
case Empty:
case Monster:
hero.state = .Combat
hero_attacking = true
message_history[message_history_slot] = "A monster!"
message_history_slot = (message_history_slot + 1) % len(message_history)
case Treasure:
treasure := &floor_segments[hero_position].data.(Treasure)
switch treasure.kind {
case .Heal:
hero.hp += hero.max_hp / 3
if hero.hp > hero.max_hp {
hero.hp = hero.max_hp
}
message_history[message_history_slot] = "Found a healing potion. Tasty!"
message_history_slot = (message_history_slot + 1) % len(message_history)
case .Gold:
hero.gold += treasure.value
message_history[message_history_slot] = "Picked up gold"
message_history_slot = (message_history_slot + 1) % len(message_history)
case .Weapon:
if treasure.tier > hero.weapon_tier {
hero.weapon_tier = treasure.tier
hero.weapon_bonus = treasure.value
message_history[message_history_slot] = "Found a better weapon"
message_history_slot = (message_history_slot + 1) % len(message_history)
} else {
message_history[message_history_slot] = "Found a worse weapon"
message_history_slot = (message_history_slot + 1) % len(message_history)
}
case .Armor:
if treasure.tier > hero.armor_tier {
hero.armor_tier = treasure.tier
hero.armor_bonus = treasure.value
message_history[message_history_slot] = "Found a better armor"
message_history_slot = (message_history_slot + 1) % len(message_history)
} else {
message_history[message_history_slot] = "Found a worse armor"
message_history_slot = (message_history_slot + 1) % len(message_history)
}
}
floor_segments[hero_position].data = Empty {
glyph = '-',
}
}
case .Combat:
monster := &floor_segments[hero_position].data.(Monster)
if hero_attacking {
message_history[message_history_slot] = "Swing"
message_history_slot = (message_history_slot + 1) % len(message_history)
dmg := (hero.base_atk + hero.weapon_bonus) - monster.def
monster.hp -= dmg if dmg > 1 else 1
if monster.hp <= 0 {
monster.glyph = 'x'
hero.xp += monster.xp_reward
hero.state = .Walking
message_history[message_history_slot] = "I'm victorious!"
message_history_slot = (message_history_slot + 1) % len(message_history)
break
}
} else {
message_history[message_history_slot] = "Ouch!"
message_history_slot = (message_history_slot + 1) % len(message_history)
dmg := monster.atk - (hero.base_def + hero.armor_bonus)
hero.hp -= dmg if dmg > 1 else 1
if hero.hp <= 0 {
message_history[message_history_slot] = "DEATH"
message_history_slot = (message_history_slot + 1) % len(message_history)
hero.state = .Dead
break
}
}
hero_attacking = !hero_attacking
case .Dead:
// fmt.printf("\x1B[5B\x1b[2KYOU DIED!\n")
// running = false
message_history[message_history_slot] = "Respawning..."
message_history_slot = (message_history_slot + 1) % len(message_history)
deaths += 1
current_floor = 0
hero_position = 0
hero.hp = hero.max_hp
hero.gold = 0
hero.weapon_tier = 0
hero.armor_tier = 0
hero.weapon_bonus = 0
hero.armor_bonus = 0
hero.xp = 0
hero.state = .Walking
generate_floor(floor_segments[:], current_floor, floor_weights)
continue
}
// NOTE: loop to add multiple levels if enough xp was gained
if hero.xp >= hero.xp_to_level {
for hero.xp >= hero.xp_to_level {
hero.xp -= hero.xp_to_level
hero.xp_to_level = hero.level * 10
hero.level += 1
hero.max_hp += 5
hero.hp = hero.max_hp
hero.base_atk += 1
if hero.level % 2 == 0 {
hero.base_def += 1
}
}
message_history[message_history_slot] = "☆ Level up! ☆"
message_history_slot = (message_history_slot + 1) % len(message_history)
}
// RENDER (this should probably be stringbuilt instead of multiple printfs)
fmt.printf("\x1b[2KFloor: %d (deaths: %d)\n", current_floor + 1, deaths)
fmt.printf("\x1b[2KLevel: %d", hero.level)
fmt.printf(" [")
progress := int((f32(hero.xp) / f32(hero.xp_to_level)) * 10)
for i in 0 ..< 10 {
if i > progress {
fmt.printf("-")
} else if i < progress {
fmt.printf("=")
} else {
fmt.printf(">")
}
}
fmt.printf("]\n")
fmt.printf("\x1b[2KHP: %d/%d, Gold: %d\n", hero.hp, hero.max_hp, hero.gold)
fmt.printf(
"\x1b[2KW: %d(+%d), A: %d(+%d)\n",
hero.weapon_tier,
hero.weapon_bonus,
hero.armor_tier,
hero.armor_bonus,
)
fmt.printf(
"\x1b[2KDMG: %d, ARM: %d\n",
hero.base_atk + hero.weapon_bonus,
hero.base_def + hero.armor_bonus,
)
fmt.printf("\x1b[2K")
for segment, i in floor_segments {
if i == hero_position {
fmt.printf("%c", hero.glyph)
} else {
switch v in segment.data {
case Empty:
fmt.printf("%c", segment.data.(Empty).glyph)
case Monster:
fmt.printf("%c", segment.data.(Monster).glyph)
case Treasure:
fmt.printf("%c", segment.data.(Treasure).glyph)
}
}
}
fmt.printf("\x1B[5A\x1b[0G")
fmt.printf(
"\x1b[40C\x1b[0m\x1b[38;5;27m╭─⟦ᚠ⟧──────────────────────────────────⟦ᚱ⟧─╮\n",
)
current_message_history_slot := (message_history_slot + len(message_history) - 10) % len(message_history)
for current_message_history_slot != message_history_slot {
fmt.printf(
"\x1b[40C\x1b[38;5;24m│\x1b[0m %40s \x1b[38;5;24m│\n",
message_history[current_message_history_slot],
)
current_message_history_slot = (current_message_history_slot + 1) % len(message_history)
}
fmt.printf(
"\x1b[40C\x1b[38;5;21m╰─⟦ᚦ⟧──────────────────────────────────⟦ᚨ⟧─╯\n\x1b[0m",
)
fmt.printf("\x1B[12A")
last_tick = now
}
running = !console.should_quit()
/*if console.wait_for_input(con) != .none {
fmt.printf("KEY\n")
}*/
time.sleep(time.Millisecond)
}
fmt.printf("\x1b[2KEND\n")
}