Skip to content

Commit e131293

Browse files
committed
add title bar
1 parent c54a487 commit e131293

18 files changed

Lines changed: 410 additions & 34 deletions
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Hero Role Tagging & Role-Aware Goal Suggestions
2+
3+
## Overview
4+
5+
Each hero should be tagged as **core** or **support** in the hero data. Goal suggestions (weekly/daily) should then use this to:
6+
1. Only suggest CS/kill/networth goals for core heroes
7+
2. Only suggest deny/partner networth goals for support heroes
8+
3. Label goals as `core_only`, `support_only`, or `anyone` so the UI can communicate who each goal is for
9+
10+
---
11+
12+
## Part 1 — Hero Role Tagging
13+
14+
### Data Source
15+
16+
OpenDota's hero data includes `primary_attr` and typical role lists. The simplest approach is a static map in `heroes.js` (or `heroes.rs`) marking each hero as core or support based on their most common position.
17+
18+
Rough split:
19+
- **Core** (pos 1/2/3): carries, mids, offlaners — heroes primarily played for farm/damage
20+
- **Support** (pos 4/5): heroes primarily played for utility, healing, disable
21+
22+
A hero can be tagged as **flex** if they're commonly played in both roles (e.g. Invoker, Rubick, Lone Druid).
23+
24+
### Frontend (`src/lib/heroes.js`)
25+
26+
Add a `heroRoles` map:
27+
```js
28+
export const heroRoles = {
29+
1: "core", // Anti-Mage
30+
2: "core", // Axe
31+
5: "support", // Crystal Maiden
32+
// ...
33+
};
34+
35+
export function getHeroRole(heroId) {
36+
return heroRoles[heroId] ?? "flex";
37+
}
38+
```
39+
40+
### Backend (`src-tauri/src/database.rs` or a new `heroes.rs`)
41+
42+
Add a Rust equivalent for use in suggestion generation:
43+
```rust
44+
fn get_hero_role(hero_id: i32) -> &'static str {
45+
match hero_id {
46+
1 => "core",
47+
5 => "support",
48+
// ...
49+
_ => "flex",
50+
}
51+
}
52+
```
53+
54+
---
55+
56+
## Part 2 — Goal Suggestion Role Awareness
57+
58+
### Existing suggestion system
59+
60+
`generate_hero_suggestion()` currently only generates last-hits suggestions — it doesn't differentiate core vs support.
61+
62+
### Changes needed
63+
64+
1. **Skip CS suggestions for support heroes**if `get_hero_role(hero_id) == "support"`, don't suggest a last-hits goal. Instead suggest a deny goal.
65+
2. **Skip deny suggestions for core heroes** — if `get_hero_role(hero_id) == "core"`, don't suggest deny goals.
66+
3. **PartnerNetworth suggestions** — only relevant for support heroes (pos 4/5).
67+
68+
---
69+
70+
## Part 3 — Goal `applicability` Tag
71+
72+
Add an `applicability` field to the `Goal` struct (and DB column) to communicate who the goal is intended for:
73+
74+
```
75+
core_only — only evaluated for core heroes/matches
76+
support_only — only evaluated for support heroes/matches
77+
anyone — evaluated for all heroes (default)
78+
```
79+
80+
This can be:
81+
- **Auto-derived** from `hero_scope` at display time:
82+
- `hero_scope = any_carry / any_core``core_only`
83+
- `hero_scope = any_support``support_only`
84+
- `hero_scope = null` with specific hero → derive from hero role
85+
- Otherwise → `anyone`
86+
- Shown as a small badge on goal cards
87+
88+
---
89+
90+
## Acceptance Criteria
91+
92+
- [ ] All 140+ heroes tagged as core / support / flex in `heroes.js`
93+
- [ ] Weekly goal suggestions only propose CS goals for core heroes
94+
- [ ] Weekly goal suggestions only propose deny goals for support heroes
95+
- [ ] Goal cards show a role applicability badge (Core, Support, or blank for Anyone)
96+
- [ ] Role tag is derived automatically — no manual input required
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Test: Custom Title Bar
2+
3+
## What changed
4+
5+
- `tauri.conf.json`: `"decorations": false` added to the window — hides the native Windows title bar
6+
- `src/lib/TitleBar.svelte`: New component with drag region, title text, and Minimize / Maximize / Close buttons
7+
- `src/routes/+layout.svelte`: TitleBar imported and placed above the main app layout
8+
- `src/app.css`: `#svelte` changed from `display: contents` to `display: flex; flex-direction: column` so the title bar and page content stack vertically
9+
- `.loading-screen`, `.login-screen`, `.app-layout` changed from `height: 100vh` to `flex: 1`
10+
- Update banner adjusted to `top: 32px` so it doesn't overlap the title bar
11+
12+
## Steps to test
13+
14+
1. Build and launch the app
15+
2. **Title bar visible**: A 32px bar appears at the very top with "DOTA KEEPER" label and three window control buttons on the right
16+
3. **Dragging**: Click and drag on the title bar (anywhere except the buttons) — the window should move
17+
4. **Minimize**: Click the `` button — window minimises to taskbar
18+
5. **Maximize/Restore**: Click the `` button — window toggles between maximized and its normal size
19+
6. **Double-click to maximize**: Double-click the title bar drag area — should toggle maximize
20+
7. **Close**: Click the `` button — window closes (app exits)
21+
8. **Close button hover**: Hovering the close button should turn it red (`#c42b1c`)
22+
9. **Content not obscured**: Page content starts below the title bar — no overlap
23+
10. **Login screen**: Log out and verify the login screen also sits below the title bar with no overlap
24+
11. **Update banner (if available)**: If an update is available, the banner appears below the title bar, not behind it
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Test: Font Weight Fix
2+
3+
## What changed
4+
5+
- Added `wght@600;700` to the Barlow font Google Fonts URL (previously only 300/400/500 were loaded, causing browser-synthesized bold which looks heavy)
6+
- Added explicit `font-weight: 400` to `body` in `app.css`
7+
8+
## Steps to test
9+
10+
1. Launch the app
11+
2. Navigate through all pages (Dashboard, Matches, Analysis, Goals, Challenges, Settings)
12+
3. Verify that body text reads cleanly and is not excessively bold or dark
13+
4. Check the Analysis page specifically — it had the most complaints about heavy text
14+
5. Nav items, filter chips, and section labels should look crisp but not overpowering
15+
16+
## Note
17+
18+
This fix requires an internet connection at app startup to download the new font weights from Google Fonts. On first load after the update, the fonts will be cached for subsequent offline use.
File renamed without changes.
File renamed without changes.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Test: Weekly Challenge Already Accepted Bug Fix
2+
3+
## What changed
4+
5+
`get_active_weekly_challenge` in `database.rs` now queries `status IN ('active', 'completed')` instead of `status = 'active'` only. This means a completed challenge is still returned, so the frontend shows the progress view instead of the option-selection view.
6+
7+
## Steps to test
8+
9+
### Scenario A — Challenge already completed
10+
1. Accept a weekly challenge
11+
2. Play enough games to complete it (the status becomes `'completed'`)
12+
3. Navigate away from the Challenges page and back
13+
4. **Expected:** The progress view is shown with "Completed!" status, not the "Choose a challenge" selection cards
14+
15+
### Scenario B — No challenge accepted yet
16+
1. Ensure no challenge has been accepted this week (or use a fresh DB)
17+
2. Navigate to Challenges
18+
3. **Expected:** The three option cards are still shown normally
19+
20+
### Scenario C — Challenge active (in progress)
21+
1. Accept a challenge
22+
2. Before completing it, navigate away and back
23+
3. **Expected:** The progress view is shown with current progress
24+
25+
### Scenario D — Confirm no duplicate acceptance possible
26+
1. With a completed challenge visible in the progress view, verify there is no "Accept" button shown
27+
2. **Expected:** The accept option should not be reachable

meta/tasks/bugs/weekly-challenge-already-accepted.md renamed to meta/tasks/to_test/weekly-challenge-already-accepted.md

File renamed without changes.

src-tauri/capabilities/default.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
"permissions": [
77
"core:default",
88
"opener:default",
9-
"updater:default"
9+
"updater:default",
10+
"core:window:allow-minimize",
11+
"core:window:allow-toggle-maximize",
12+
"core:window:allow-close",
13+
"core:window:allow-start-dragging"
1014
]
1115
}

src-tauri/src/database.rs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ impl GoalGameMode {
7070
#[derive(Debug, Serialize, Deserialize, Clone)]
7171
pub struct Goal {
7272
pub id: i64,
73-
pub hero_id: Option<i32>, // None means "any hero" (but required for ItemTiming goals)
73+
pub hero_id: Option<i32>, // None when hero_scope is set or "any hero"
74+
pub hero_scope: Option<String>, // "any_core", "any_carry", "any_support", or None
7475
pub metric: GoalMetric,
7576
pub target_value: i32, // For ItemTiming: target time in seconds
7677
pub target_time_minutes: i32, // Not used for ItemTiming goals
@@ -83,6 +84,7 @@ pub struct Goal {
8384
#[derive(Debug, Serialize, Deserialize, Clone)]
8485
pub struct NewGoal {
8586
pub hero_id: Option<i32>,
87+
pub hero_scope: Option<String>, // "any_core", "any_carry", "any_support", or None
8688
pub metric: GoalMetric,
8789
pub target_value: i32,
8890
pub target_time_minutes: i32,
@@ -301,6 +303,12 @@ pub fn init_db() -> Result<Connection, String> {
301303
[],
302304
);
303305

306+
// Add hero_scope column if it doesn't exist (for role-group goals)
307+
let _ = conn.execute(
308+
"ALTER TABLE goals ADD COLUMN hero_scope TEXT",
309+
[],
310+
);
311+
304312
// Create the hero_favorites table
305313
conn.execute(
306314
"CREATE TABLE IF NOT EXISTS hero_favorites (
@@ -607,8 +615,8 @@ pub fn insert_goal(conn: &Connection, goal: &NewGoal) -> Result<Goal, String> {
607615
.as_secs() as i64;
608616

609617
conn.execute(
610-
"INSERT INTO goals (hero_id, metric, target_value, target_time_minutes, game_mode, item_id, created_at)
611-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
618+
"INSERT INTO goals (hero_id, metric, target_value, target_time_minutes, game_mode, item_id, created_at, hero_scope)
619+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
612620
params![
613621
goal.hero_id,
614622
goal.metric.to_string(),
@@ -617,6 +625,7 @@ pub fn insert_goal(conn: &Connection, goal: &NewGoal) -> Result<Goal, String> {
617625
goal.game_mode.to_string(),
618626
goal.item_id,
619627
now,
628+
goal.hero_scope,
620629
],
621630
).map_err(|e| format!("Failed to insert goal: {}", e))?;
622631

@@ -625,6 +634,7 @@ pub fn insert_goal(conn: &Connection, goal: &NewGoal) -> Result<Goal, String> {
625634
Ok(Goal {
626635
id,
627636
hero_id: goal.hero_id,
637+
hero_scope: goal.hero_scope.clone(),
628638
metric: goal.metric.clone(),
629639
target_value: goal.target_value,
630640
target_time_minutes: goal.target_time_minutes,
@@ -638,7 +648,7 @@ pub fn insert_goal(conn: &Connection, goal: &NewGoal) -> Result<Goal, String> {
638648
pub fn get_all_goals(conn: &Connection) -> Result<Vec<Goal>, String> {
639649
let mut stmt = conn
640650
.prepare(
641-
"SELECT id, hero_id, metric, target_value, target_time_minutes, game_mode, created_at, item_id
651+
"SELECT id, hero_id, metric, target_value, target_time_minutes, game_mode, created_at, item_id, hero_scope
642652
FROM goals ORDER BY created_at DESC",
643653
)
644654
.map_err(|e| format!("Failed to prepare query: {}", e))?;
@@ -656,6 +666,7 @@ pub fn get_all_goals(conn: &Connection) -> Result<Vec<Goal>, String> {
656666
game_mode: GoalGameMode::from_string(&game_mode_str).unwrap_or(GoalGameMode::Ranked),
657667
created_at: row.get(6)?,
658668
item_id: row.get(7)?,
669+
hero_scope: row.get(8).unwrap_or(None),
659670
})
660671
})
661672
.map_err(|e| format!("Failed to query goals: {}", e))?;
@@ -672,14 +683,15 @@ pub fn get_all_goals(conn: &Connection) -> Result<Vec<Goal>, String> {
672683
pub fn update_goal(conn: &Connection, goal: &Goal) -> Result<(), String> {
673684
conn.execute(
674685
"UPDATE goals SET hero_id = ?1, metric = ?2, target_value = ?3,
675-
target_time_minutes = ?4, game_mode = ?5, item_id = ?6 WHERE id = ?7",
686+
target_time_minutes = ?4, game_mode = ?5, item_id = ?6, hero_scope = ?7 WHERE id = ?8",
676687
params![
677688
goal.hero_id,
678689
goal.metric.to_string(),
679690
goal.target_value,
680691
goal.target_time_minutes,
681692
goal.game_mode.to_string(),
682693
goal.item_id,
694+
goal.hero_scope,
683695
goal.id,
684696
],
685697
).map_err(|e| format!("Failed to update goal: {}", e))?;
@@ -721,11 +733,18 @@ pub fn evaluate_goal(conn: &Connection, goal: &Goal, match_data: &Match) -> Opti
721733
return None;
722734
}
723735

724-
// Check if goal applies to this match based on hero
725-
if let Some(goal_hero_id) = goal.hero_id {
726-
if goal_hero_id != match_data.hero_id {
727-
return None; // Goal doesn't apply to this hero
728-
}
736+
// Check if goal applies to this match based on hero / hero scope
737+
let hero_matches = match goal.hero_scope.as_deref() {
738+
Some("any_carry") => match_data.role == 1,
739+
Some("any_core") => matches!(match_data.role, 1 | 2 | 3),
740+
Some("any_support")=> matches!(match_data.role, 4 | 5),
741+
_ => match goal.hero_id {
742+
Some(id) => id == match_data.hero_id,
743+
None => true, // any hero
744+
},
745+
};
746+
if !hero_matches {
747+
return None;
729748
}
730749

731750
// Check if goal applies based on game mode
@@ -1178,7 +1197,7 @@ pub struct MatchDataPoint {
11781197
pub fn get_goal_match_data(conn: &Connection, goal_id: i64) -> Result<Vec<MatchDataPoint>, String> {
11791198
// Get the goal
11801199
let mut stmt = conn
1181-
.prepare("SELECT id, hero_id, metric, target_value, target_time_minutes, game_mode, created_at, item_id FROM goals WHERE id = ?1")
1200+
.prepare("SELECT id, hero_id, metric, target_value, target_time_minutes, game_mode, created_at, item_id, hero_scope FROM goals WHERE id = ?1")
11821201
.map_err(|e| format!("Failed to prepare query: {}", e))?;
11831202

11841203
let goal = stmt
@@ -1194,6 +1213,7 @@ pub fn get_goal_match_data(conn: &Connection, goal_id: i64) -> Result<Vec<MatchD
11941213
game_mode: GoalGameMode::from_string(&game_mode_str).unwrap_or(GoalGameMode::Ranked),
11951214
created_at: row.get(6)?,
11961215
item_id: row.get(7)?,
1216+
hero_scope: row.get(8).unwrap_or(None),
11971217
})
11981218
})
11991219
.map_err(|e| format!("Failed to get goal: {}", e))?;
@@ -1222,7 +1242,7 @@ pub fn get_goal_match_data(conn: &Connection, goal_id: i64) -> Result<Vec<MatchD
12221242
/// Get a single goal by ID
12231243
pub fn get_goal_by_id(conn: &Connection, goal_id: i64) -> Result<Goal, String> {
12241244
let mut stmt = conn
1225-
.prepare("SELECT id, hero_id, metric, target_value, target_time_minutes, game_mode, created_at, item_id FROM goals WHERE id = ?1")
1245+
.prepare("SELECT id, hero_id, metric, target_value, target_time_minutes, game_mode, created_at, item_id, hero_scope FROM goals WHERE id = ?1")
12261246
.map_err(|e| format!("Failed to prepare query: {}", e))?;
12271247

12281248
stmt.query_row(params![goal_id], |row| {
@@ -1237,6 +1257,7 @@ pub fn get_goal_by_id(conn: &Connection, goal_id: i64) -> Result<Goal, String> {
12371257
game_mode: GoalGameMode::from_string(&game_mode_str).unwrap_or(GoalGameMode::Ranked),
12381258
created_at: row.get(6)?,
12391259
item_id: row.get(7)?,
1260+
hero_scope: row.get(8).unwrap_or(None),
12401261
})
12411262
})
12421263
.map_err(|e| format!("Failed to get goal: {}", e))
@@ -2872,7 +2893,7 @@ pub fn get_active_weekly_challenge(conn: &Connection) -> Result<Option<WeeklyCha
28722893
match conn.query_row(
28732894
"SELECT id, week_start_date, challenge_type, challenge_description, challenge_target,
28742895
challenge_target_games, hero_id, metric, status, accepted_at, completed_at, reroll_count
2875-
FROM weekly_challenges WHERE week_start_date = ?1 AND status = 'active'",
2896+
FROM weekly_challenges WHERE week_start_date = ?1 AND status IN ('active', 'completed')",
28762897
params![week_start],
28772898
row_to_weekly_challenge,
28782899
) {

0 commit comments

Comments
 (0)