initiative-tracker 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lukas Wagenvoort
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: initiative-tracker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple cli based initiative tracker for dnd or other ttrpgs
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Quaeren
|
|
8
|
+
Author-email: ldwagenvoort@hotmail.com
|
|
9
|
+
Requires-Python: >=3.14
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Dist: questionary (>=2.1.1,<3.0.0)
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Initiative Tracker for TTRPGS
|
|
16
|
+
A simple command line interface for running a initiative tracker in the terminal. You can create an encounter, add combatants, roll initiative, track health and save encounters for later use.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
- **Combatant Mangement** Add or remove players, enemies or allies in an encounter
|
|
20
|
+
- **Automatic Turn Tracking** After letting the program roll the initiative it autmatically tracks whoose turn it is
|
|
21
|
+
- **Health Management** Add or remove health via the damage and heal funcitons
|
|
22
|
+
- **Save and Load** Save and load encounters between sessions for quick and easy playthroughs
|
|
23
|
+
|
|
24
|
+
## How to Use
|
|
25
|
+
### Main menu
|
|
26
|
+
When the application starts, you can choose to:
|
|
27
|
+
- **Start new encounter:** Create an empty encounter.
|
|
28
|
+
- **Load** encounter:** Load the saved encounter.
|
|
29
|
+
- **Exit:** Close the application.
|
|
30
|
+
|
|
31
|
+
### Managing an encounter
|
|
32
|
+
Once an encounter has been created or loaded, the encounter menu provides the following options:
|
|
33
|
+
- **Add combatant:** Enter a name, maximum hit points, initiative modifier, and combatant type.
|
|
34
|
+
- **Remove combatant:** Select combatants to remove from the encounter.
|
|
35
|
+
- **View turn order:** Sort combatants by initiative and display their current health and initiative.
|
|
36
|
+
- **Roll initiative:** Roll initiative for every combatant.
|
|
37
|
+
- **Start encounter:** Open the turn-order menu and begin managing combat.
|
|
38
|
+
- **Save encounter:** Save the encounter's current state to the database.
|
|
39
|
+
- **Exit:** Return to the main menu.
|
|
40
|
+
|
|
41
|
+
During an encounter, the currently active combatant is marked with an arrow. You can advance to the next turn, reset the round counter, or select a combatant to perform a health-related action.
|
|
42
|
+
|
|
43
|
+
### Actions on a combatant
|
|
44
|
+
Once you select a combatant you are presented with the option to input a command, the following commands are allowed:
|
|
45
|
+
- **damage** after inputting enter a value and the corresponding health will be removed
|
|
46
|
+
- **heal** after inputting enter a value and the corresponding health will be healed
|
|
47
|
+
- **set** after inputting manually set and initiative value for the combatant
|
|
48
|
+
- **exit** after inputting press enter again and exit this menu
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Initiative Tracker for TTRPGS
|
|
2
|
+
A simple command line interface for running a initiative tracker in the terminal. You can create an encounter, add combatants, roll initiative, track health and save encounters for later use.
|
|
3
|
+
|
|
4
|
+
## Features
|
|
5
|
+
- **Combatant Mangement** Add or remove players, enemies or allies in an encounter
|
|
6
|
+
- **Automatic Turn Tracking** After letting the program roll the initiative it autmatically tracks whoose turn it is
|
|
7
|
+
- **Health Management** Add or remove health via the damage and heal funcitons
|
|
8
|
+
- **Save and Load** Save and load encounters between sessions for quick and easy playthroughs
|
|
9
|
+
|
|
10
|
+
## How to Use
|
|
11
|
+
### Main menu
|
|
12
|
+
When the application starts, you can choose to:
|
|
13
|
+
- **Start new encounter:** Create an empty encounter.
|
|
14
|
+
- **Load** encounter:** Load the saved encounter.
|
|
15
|
+
- **Exit:** Close the application.
|
|
16
|
+
|
|
17
|
+
### Managing an encounter
|
|
18
|
+
Once an encounter has been created or loaded, the encounter menu provides the following options:
|
|
19
|
+
- **Add combatant:** Enter a name, maximum hit points, initiative modifier, and combatant type.
|
|
20
|
+
- **Remove combatant:** Select combatants to remove from the encounter.
|
|
21
|
+
- **View turn order:** Sort combatants by initiative and display their current health and initiative.
|
|
22
|
+
- **Roll initiative:** Roll initiative for every combatant.
|
|
23
|
+
- **Start encounter:** Open the turn-order menu and begin managing combat.
|
|
24
|
+
- **Save encounter:** Save the encounter's current state to the database.
|
|
25
|
+
- **Exit:** Return to the main menu.
|
|
26
|
+
|
|
27
|
+
During an encounter, the currently active combatant is marked with an arrow. You can advance to the next turn, reset the round counter, or select a combatant to perform a health-related action.
|
|
28
|
+
|
|
29
|
+
### Actions on a combatant
|
|
30
|
+
Once you select a combatant you are presented with the option to input a command, the following commands are allowed:
|
|
31
|
+
- **damage** after inputting enter a value and the corresponding health will be removed
|
|
32
|
+
- **heal** after inputting enter a value and the corresponding health will be healed
|
|
33
|
+
- **set** after inputting manually set and initiative value for the combatant
|
|
34
|
+
- **exit** after inputting press enter again and exit this menu
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "initiative-tracker"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A simple cli based initiative tracker for dnd or other ttrpgs"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Quaeren",email = "ldwagenvoort@hotmail.com"}
|
|
7
|
+
]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"questionary (>=2.1.1,<3.0.0)"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
18
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import questionary
|
|
2
|
+
from questionary import Separator
|
|
3
|
+
from .tracker import Database, Encounter, Combatant
|
|
4
|
+
import subprocess
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def clear_screen():
|
|
9
|
+
subprocess.run(
|
|
10
|
+
["cmd", "/c", "cls"] if os.name == "nt" else ["clear"], check=False)
|
|
11
|
+
|
|
12
|
+
def sub_menu_questions():
|
|
13
|
+
clear_screen()
|
|
14
|
+
|
|
15
|
+
return questionary.select(
|
|
16
|
+
"Please choose an action to do:",
|
|
17
|
+
choices=[
|
|
18
|
+
"Add combatant",
|
|
19
|
+
"Remove combatant",
|
|
20
|
+
"View turn order",
|
|
21
|
+
"Roll initiative",
|
|
22
|
+
"Start encounter",
|
|
23
|
+
"Save encounter",
|
|
24
|
+
"Exit"
|
|
25
|
+
], qmark="", instruction=None, use_shortcuts=True).ask() or "do nothing"
|
|
26
|
+
|
|
27
|
+
def remove_combatant_cli(encounter):
|
|
28
|
+
clear_screen()
|
|
29
|
+
|
|
30
|
+
choices = []
|
|
31
|
+
for i, combatant in enumerate(encounter.combatant_list):
|
|
32
|
+
name = f"{combatant}"
|
|
33
|
+
|
|
34
|
+
choices.append(questionary.Choice(
|
|
35
|
+
title=name,
|
|
36
|
+
value=combatant))
|
|
37
|
+
choices.append("Exit")
|
|
38
|
+
|
|
39
|
+
choice = questionary.checkbox("Please select combatants to be removed:", choices=choices, qmark="", instruction=None, use_shortcuts=True).ask() or []
|
|
40
|
+
|
|
41
|
+
if "Exit" in choice:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
for item in choice:
|
|
45
|
+
encounter.remove_combatant(item)
|
|
46
|
+
print("Succesfully removed all selected combatants combatants")
|
|
47
|
+
|
|
48
|
+
def choice_menu(choice, encounter):
|
|
49
|
+
clear_screen()
|
|
50
|
+
|
|
51
|
+
match choice:
|
|
52
|
+
case "Add combatant":
|
|
53
|
+
name = questionary.text("Name: ", qmark="", instruction=None).ask()
|
|
54
|
+
health = int(questionary.text("Health: ", qmark="", instruction=None).ask())
|
|
55
|
+
init_mod = int(questionary.text("Initiative modifier: ", qmark="", instruction=None).ask())
|
|
56
|
+
type = questionary.select(
|
|
57
|
+
"Select combatant tupe: ",
|
|
58
|
+
choices=["player", "enemy", "ally"], qmark="", instruction=None, use_shortcuts=True).ask()
|
|
59
|
+
|
|
60
|
+
encounter.add_combatant(Combatant(name, health, init_mod, type))
|
|
61
|
+
print(f"Combatant: {name} added succesfully")
|
|
62
|
+
return None
|
|
63
|
+
case "Remove combatant":
|
|
64
|
+
remove_combatant_cli(encounter)
|
|
65
|
+
return None
|
|
66
|
+
case "View turn order":
|
|
67
|
+
clear_screen()
|
|
68
|
+
encounter.sort_list()
|
|
69
|
+
encounter.print_turn_order()
|
|
70
|
+
questionary.press_any_key_to_continue().ask()
|
|
71
|
+
return None
|
|
72
|
+
case "Roll initiative":
|
|
73
|
+
encounter.roll_all_initiative()
|
|
74
|
+
return None
|
|
75
|
+
case "Start encounter":
|
|
76
|
+
return "Start"
|
|
77
|
+
case "Save encounter":
|
|
78
|
+
return "Save"
|
|
79
|
+
case "Exit":
|
|
80
|
+
return "Exit"
|
|
81
|
+
|
|
82
|
+
def active_encounter(encounter):
|
|
83
|
+
while True:
|
|
84
|
+
clear_screen()
|
|
85
|
+
choices = []
|
|
86
|
+
|
|
87
|
+
for i, combatant in enumerate(encounter.combatant_list):
|
|
88
|
+
name = f"▶ {combatant}" if i == encounter.active_combatant_index else f" {combatant}"
|
|
89
|
+
|
|
90
|
+
choices.append(questionary.Choice(
|
|
91
|
+
title=name,
|
|
92
|
+
value=combatant
|
|
93
|
+
))
|
|
94
|
+
choices.append(Separator())
|
|
95
|
+
choices.append(" Reset round number")
|
|
96
|
+
choices.append(" End Turn")
|
|
97
|
+
choices.append(" Exit")
|
|
98
|
+
|
|
99
|
+
print(f"Round Number: {encounter.round_number}")
|
|
100
|
+
choice = questionary.select("Turn Order:", choices=choices, qmark="", instruction=None, use_shortcuts=True).ask() or "do nothing"
|
|
101
|
+
|
|
102
|
+
if choice == " End Turn":
|
|
103
|
+
encounter.advance_turn()
|
|
104
|
+
continue
|
|
105
|
+
elif choice == " Exit":
|
|
106
|
+
break
|
|
107
|
+
elif choice == " Reset round number":
|
|
108
|
+
encounter.reset_round_number()
|
|
109
|
+
continue
|
|
110
|
+
else:
|
|
111
|
+
action = input(choice).strip()
|
|
112
|
+
value = input("Value: ").strip()
|
|
113
|
+
if action.lower() == "damage":
|
|
114
|
+
encounter.combatant_list[encounter.combatant_list.index(choice)].damage(int(value))
|
|
115
|
+
elif action.lower() == "heal":
|
|
116
|
+
encounter.combatant_list[encounter.combatant_list.index(choice)].heal(int(value))
|
|
117
|
+
elif action.lower() == "set":
|
|
118
|
+
encounter.combatant_list[encounter.combatant_list.index(choice)].set_init(int(value))
|
|
119
|
+
elif action.lower() == "exit":
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main():
|
|
126
|
+
database = Database()
|
|
127
|
+
database.create_tables()
|
|
128
|
+
clear_screen()
|
|
129
|
+
|
|
130
|
+
while True:
|
|
131
|
+
print("===========================================================================================")
|
|
132
|
+
print(""" ▄▄▄ ▗▄ ▗▖ ▄▄▄ ▗▄▄▄▖ ▄▄▄ ▄ ▗▄▄▄▖ ▄▄▄ ▗▖ ▗▖▗▄▄▄▖ ▗▄▄▄▖▗▄▄▖ ▄ ▄▄ ▗▖ ▄▖▗▄▄▄▖▗▄▄▖
|
|
133
|
+
▀█▀ ▐█ ▐▌ ▀█▀ ▝▀█▀▘ ▀█▀ ▐█▌ ▝▀█▀▘ ▀█▀ ▝█ █▘▐▛▀▀▘ ▝▀█▀▘▐▛▀▜▌ ▐█▌ █▀▀▌▐▌▐▛ ▐▛▀▀▘▐▛▀▜▌
|
|
134
|
+
█ ▐▛▌▐▌ █ █ █ ▐█▌ █ █ █ █ ▐▌ █ ▐▌ ▐▌ ▐█▌ ▐▛ ▐▙█ ▐▌ ▐▌ ▐▌
|
|
135
|
+
█ ▐▌█▐▌ █ █ █ █ █ █ █ █ █ ▐███ █ ▐███ █ █ ▐▌ ▐██ ▐███ ▐███
|
|
136
|
+
█ ▐▌▐▟▌ █ █ █ ███ █ █ ▐█▌ ▐▌ █ ▐▌▝█▖ ███ ▐▙ ▐▌▐▙ ▐▌ ▐▌▝█▖
|
|
137
|
+
▄█▄ ▐▌ █▌ ▄█▄ █ ▄█▄ ▗█ █▖ █ ▄█▄ ▐█▌ ▐▙▄▄▖ █ ▐▌ ▐▌▗█ █▖ █▄▄▌▐▌ █▖▐▙▄▄▖▐▌ ▐▌
|
|
138
|
+
▀▀▀ ▝▘ ▀▘ ▀▀▀ ▀ ▀▀▀ ▝▘ ▝▘ ▀ ▀▀▀ ▝▀▘ ▝▀▀▀▘ ▀ ▝▘ ▝▀▝▘ ▝▘ ▀▀ ▝▘ ▝▘▝▀▀▀▘▝▘ ▝▀ """)
|
|
139
|
+
print("===========================================================================================")
|
|
140
|
+
action = (questionary.select("Welcome to tracker, please select from the options below:",
|
|
141
|
+
choices=[
|
|
142
|
+
"Start new encounter",
|
|
143
|
+
"Load encounter",
|
|
144
|
+
"Exit"], qmark="", instruction=None, use_shortcuts=True).ask() or "do nothing")
|
|
145
|
+
|
|
146
|
+
match action:
|
|
147
|
+
case "Start new encounter":
|
|
148
|
+
encounter = Encounter()
|
|
149
|
+
|
|
150
|
+
while True:
|
|
151
|
+
choice = sub_menu_questions()
|
|
152
|
+
check = choice_menu(choice, encounter)
|
|
153
|
+
if check == None:
|
|
154
|
+
continue
|
|
155
|
+
elif check == "Exit":
|
|
156
|
+
break
|
|
157
|
+
elif check == "Save":
|
|
158
|
+
database.save_encounter(encounter)
|
|
159
|
+
elif check == "Start":
|
|
160
|
+
active_encounter(encounter)
|
|
161
|
+
case "Load encounter":
|
|
162
|
+
encounter = database.load_encounter()
|
|
163
|
+
|
|
164
|
+
while True:
|
|
165
|
+
choice = sub_menu_questions()
|
|
166
|
+
check = choice_menu(choice, encounter)
|
|
167
|
+
if check == None:
|
|
168
|
+
continue
|
|
169
|
+
elif check == "Exit":
|
|
170
|
+
break
|
|
171
|
+
elif check == "Save":
|
|
172
|
+
database.save_encounter(encounter)
|
|
173
|
+
elif check == "Start":
|
|
174
|
+
active_encounter(encounter)
|
|
175
|
+
case "Exit":
|
|
176
|
+
clear_screen()
|
|
177
|
+
break
|
|
178
|
+
case _:
|
|
179
|
+
print("No option chosen")
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
main()
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import sqlite3
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
class Combatant:
|
|
6
|
+
def __init__(self, name, hp, init_mod, type):
|
|
7
|
+
self.name = name.strip()
|
|
8
|
+
self.max_hp = hp
|
|
9
|
+
self.hp = hp
|
|
10
|
+
self.init_mod = init_mod # initiative modifer
|
|
11
|
+
self.init = 0 # set default initiative to zero
|
|
12
|
+
self.type = type
|
|
13
|
+
|
|
14
|
+
def damage(self, value: int) -> None:
|
|
15
|
+
if value <= 0:
|
|
16
|
+
raise ValueError("Only positive numbers above zero accepted")
|
|
17
|
+
else:
|
|
18
|
+
if (self.hp - value) < 0:
|
|
19
|
+
self.hp = 0
|
|
20
|
+
else:
|
|
21
|
+
self.hp -= value
|
|
22
|
+
#print(f"{value} damage done to {self.name}. Current HP: {self.hp}")
|
|
23
|
+
|
|
24
|
+
def heal(self, value:int) -> None:
|
|
25
|
+
if value <= 0:
|
|
26
|
+
raise ValueError("Only positive number above zero accepted")
|
|
27
|
+
else:
|
|
28
|
+
if (self.hp + value) > self.max_hp:
|
|
29
|
+
self.hp = self.max_hp
|
|
30
|
+
else:
|
|
31
|
+
self.hp += value
|
|
32
|
+
#print(f"{value} damage healed for {self.name}. Current HP: {self.hp}")
|
|
33
|
+
|
|
34
|
+
def set_init(self, value:int) -> None:
|
|
35
|
+
if value > 0 and value < 26:
|
|
36
|
+
self.init = value
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError("Value has to be between 1 and 25")
|
|
39
|
+
|
|
40
|
+
def set_init_mod(self, value:int) -> None:
|
|
41
|
+
if value >= -5 and value <= 5:
|
|
42
|
+
self.init_mod = value
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError("Value has to be between -5 and 5")
|
|
45
|
+
|
|
46
|
+
def roll_init(self) -> int:
|
|
47
|
+
init = random.randint(1,20) + self.init_mod
|
|
48
|
+
if init <= 0:
|
|
49
|
+
init = 1
|
|
50
|
+
self.init = init
|
|
51
|
+
return init
|
|
52
|
+
|
|
53
|
+
def change_name(self, name):
|
|
54
|
+
if not isinstance(name, str):
|
|
55
|
+
raise TypeError("Name must be a string")
|
|
56
|
+
|
|
57
|
+
if not name.strip():
|
|
58
|
+
raise ValueError("Name cannnot be empty")
|
|
59
|
+
|
|
60
|
+
self.name = name.strip()
|
|
61
|
+
|
|
62
|
+
def __str__(self):
|
|
63
|
+
return f"{self.name} ({self.hp}/{self.max_hp}) HP, ({self.init})"
|
|
64
|
+
|
|
65
|
+
def __lt__(self, other):
|
|
66
|
+
return self.init > other.init
|
|
67
|
+
|
|
68
|
+
class Encounter:
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self.combatant_list = []
|
|
71
|
+
self.active_combatant_index = 0
|
|
72
|
+
self.round_number = 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def add_combatant(self, combatant):
|
|
76
|
+
self.combatant_list.append(combatant)
|
|
77
|
+
|
|
78
|
+
def remove_combatant(self, removee):
|
|
79
|
+
for i, combatant in enumerate(self.combatant_list):
|
|
80
|
+
if combatant == removee:
|
|
81
|
+
if self.combatant_list.index(removee) == self.active_combatant_index:
|
|
82
|
+
self.active_combatant_index += 1
|
|
83
|
+
if self.active_combatant_index > (len(self.combatant_list) - 1):
|
|
84
|
+
self.active_combatant_index = 0
|
|
85
|
+
self.round_number += 1
|
|
86
|
+
del self.combatant_list[i]
|
|
87
|
+
|
|
88
|
+
def sort_list(self):
|
|
89
|
+
self.combatant_list.sort()
|
|
90
|
+
|
|
91
|
+
def print_turn_order(self):
|
|
92
|
+
for mem in self.combatant_list:
|
|
93
|
+
print(mem)
|
|
94
|
+
|
|
95
|
+
def roll_all_initiative(self):
|
|
96
|
+
for mem in self.combatant_list:
|
|
97
|
+
mem.roll_init()
|
|
98
|
+
|
|
99
|
+
def advance_turn(self):
|
|
100
|
+
self.active_combatant_index += 1
|
|
101
|
+
|
|
102
|
+
if self.active_combatant_index >= len(self.combatant_list):
|
|
103
|
+
self.active_combatant_index = 0
|
|
104
|
+
self.round_number += 1
|
|
105
|
+
|
|
106
|
+
while self.combatant_list[self.active_combatant_index].hp == 0:
|
|
107
|
+
self.active_combatant_index += 1
|
|
108
|
+
|
|
109
|
+
if self.active_combatant_index >= len(self.combatant_list):
|
|
110
|
+
self.active_combatant_index = 0
|
|
111
|
+
self.round_number += 1
|
|
112
|
+
|
|
113
|
+
def reset_round_number(self):
|
|
114
|
+
self.round_number = 0
|
|
115
|
+
|
|
116
|
+
class Database:
|
|
117
|
+
def __init__(self, db_name="tracker.db"):
|
|
118
|
+
self.db_name = db_name
|
|
119
|
+
|
|
120
|
+
def check(self):
|
|
121
|
+
return Path(self.db_name).exists()
|
|
122
|
+
|
|
123
|
+
def connect(self):
|
|
124
|
+
conn = sqlite3.connect(self.db_name)
|
|
125
|
+
conn.execute("PRAGMA foreign_keys = ON")
|
|
126
|
+
return conn
|
|
127
|
+
|
|
128
|
+
def create_tables(self):
|
|
129
|
+
with self.connect() as conn:
|
|
130
|
+
conn.execute("""
|
|
131
|
+
CREATE TABLE IF NOT EXISTS encounters (
|
|
132
|
+
id INTEGER PRIMARY KEY,
|
|
133
|
+
round_number INTEGER NOT NULL DEFAULT 0,
|
|
134
|
+
active_index INTEGER
|
|
135
|
+
)
|
|
136
|
+
""")
|
|
137
|
+
|
|
138
|
+
conn.execute("""
|
|
139
|
+
CREATE TABLE IF NOT EXISTS combatants (
|
|
140
|
+
id INTEGER PRIMARY KEY,
|
|
141
|
+
encounter_id INTEGER NOT NULL,
|
|
142
|
+
name TEXT NOT NULL,
|
|
143
|
+
max_hp INTEGER NOT NULL,
|
|
144
|
+
hp INTEGER NOT NULL,
|
|
145
|
+
init_mod INTEGER NOT NULL,
|
|
146
|
+
init INTEGER NOT NULL,
|
|
147
|
+
type TEXT NOT NULL,
|
|
148
|
+
|
|
149
|
+
FOREIGN KEY (encounter_id)
|
|
150
|
+
REFERENCES encounters(id)
|
|
151
|
+
ON DELETE CASCADE
|
|
152
|
+
)
|
|
153
|
+
""")
|
|
154
|
+
|
|
155
|
+
conn.execute("""
|
|
156
|
+
INSERT OR IGNORE INTO encounters (id, round_number)
|
|
157
|
+
VALUES (1, 0)
|
|
158
|
+
""")
|
|
159
|
+
|
|
160
|
+
def save_encounter(self, encounter):
|
|
161
|
+
with self.connect() as conn:
|
|
162
|
+
conn.execute("PRAGMA foreign_keys = ON")
|
|
163
|
+
|
|
164
|
+
conn.execute("""
|
|
165
|
+
UPDATE encounters
|
|
166
|
+
SET round_number = ?, active_index = ?
|
|
167
|
+
WHERE id = 1
|
|
168
|
+
""", (encounter.round_number, encounter.active_combatant_index))
|
|
169
|
+
|
|
170
|
+
conn.execute("""
|
|
171
|
+
DELETE FROM combatants
|
|
172
|
+
WHERE encounter_id = 1
|
|
173
|
+
""")
|
|
174
|
+
|
|
175
|
+
for combatant in encounter.combatant_list:
|
|
176
|
+
conn.execute("""
|
|
177
|
+
INSERT INTO combatants (
|
|
178
|
+
encounter_id,
|
|
179
|
+
name,
|
|
180
|
+
max_hp,
|
|
181
|
+
hp,
|
|
182
|
+
init_mod,
|
|
183
|
+
init,
|
|
184
|
+
type
|
|
185
|
+
)
|
|
186
|
+
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
187
|
+
""", (combatant.name, combatant.max_hp, combatant.hp, combatant.init_mod, combatant.init, combatant.type))
|
|
188
|
+
|
|
189
|
+
def load_encounter(self):
|
|
190
|
+
with self.connect() as conn:
|
|
191
|
+
conn.row_factory = sqlite3.Row
|
|
192
|
+
|
|
193
|
+
encounter_row = conn.execute("""
|
|
194
|
+
SELECT round_number, active_index
|
|
195
|
+
FROM encounters
|
|
196
|
+
WHERE id = 1
|
|
197
|
+
""").fetchone()
|
|
198
|
+
|
|
199
|
+
if encounter_row is None:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
encounter = Encounter()
|
|
203
|
+
encounter.round_number = encounter_row["round_number"]
|
|
204
|
+
|
|
205
|
+
rows = conn.execute("""
|
|
206
|
+
SELECT name, max_hp, hp, init_mod, init, type
|
|
207
|
+
FROM combatants
|
|
208
|
+
WHERE encounter_id = 1
|
|
209
|
+
ORDER BY id
|
|
210
|
+
""").fetchall()
|
|
211
|
+
|
|
212
|
+
for row in rows:
|
|
213
|
+
combatant = Combatant(
|
|
214
|
+
row["name"],
|
|
215
|
+
row["max_hp"],
|
|
216
|
+
row["init_mod"],
|
|
217
|
+
row["type"]
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
combatant.hp = row["hp"]
|
|
221
|
+
combatant.init = row["init"]
|
|
222
|
+
|
|
223
|
+
encounter.add_combatant(combatant)
|
|
224
|
+
|
|
225
|
+
active_index = encounter_row["active_index"]
|
|
226
|
+
|
|
227
|
+
if active_index is not None:
|
|
228
|
+
if 0 <= active_index < len(encounter.combatant_list):
|
|
229
|
+
encounter.active_combatant_index = active_index
|
|
230
|
+
|
|
231
|
+
return encounter
|