Skip to content

Commit 38f5cb3

Browse files
committed
Complete Phase 3: Tactile Junction Overhaul and Stat Ceiling Enforcement
1 parent 8f091f0 commit 38f5cb3

10 files changed

Lines changed: 202 additions & 94 deletions

File tree

Mythril.Blazor/Components/CharacterDisplay.razor

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,18 @@
1313
<div class="character-controls d-flex flex-column gap-1">
1414
@if (assignedCadences.Any())
1515
{
16-
<button class="btn btn-xs btn-outline-primary" data-testid="junction-toggle" @onclick="ToggleJunctionMenu">Junction</button>
16+
<div class="btn-group btn-group-xs w-100">
17+
<button class="btn btn-xs @(_isRemovalMode ? "btn-danger" : "btn-outline-danger")"
18+
title="Remove Junctions"
19+
@onclick="ToggleRemovalMode">
20+
<span class="material-icons" style="font-size: 14px;">link_off</span>
21+
</button>
22+
<button class="btn btn-xs @(_showJunctionMenu ? "btn-primary" : "btn-outline-primary")"
23+
title="Junction Menu"
24+
@onclick="ToggleJunctionMenu">
25+
<span class="material-icons" style="font-size: 14px;">menu</span>
26+
</button>
27+
</div>
1728

1829
@if (resourceManager.CanAutoQuest(Character))
1930
{
@@ -61,8 +72,9 @@
6172
bool canJunction = CanJunction(stat);
6273

6374
<span @key="stat.Name"
64-
class="stat-badge @(junction != null ? "junctioned" : "") @(isHovered ? "hovered" : "") @(canJunction ? "can-junction" : "locked")"
75+
class="stat-badge @(junction != null ? "junctioned" : "") @(isHovered ? "hovered" : "") @(canJunction ? "can-junction" : "locked") @(_isRemovalMode && junction != null ? "removal-target" : "")"
6576
title="@stat.Description @(junction != null ? $"(J: {junction.Magic.Name})" : "")"
77+
@onclick="() => { if(_isRemovalMode && junction != null) ClearJunction(stat); }"
6678
@ondragenter="() => HandleStatDragEnter(stat)"
6779
@ondragleave="HandleStatDragLeave"
6880
@ondragover:preventDefault
@@ -71,7 +83,7 @@
7183
@if (delta != 0)
7284
{
7385
<span class="stat-delta @(delta > 0 ? "text-success" : "text-danger")">
74-
@(delta > 0 ? "+" : "")@delta
86+
@(delta > 0 ? "" : "")@Math.Abs(delta)
7587
</span>
7688
}
7789
</span>

Mythril.Blazor/Components/CharacterDisplay.razor.cs

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public partial class CharacterDisplay : IDisposable
4040
public EventCallback<Cadence> OnUnequip { get; set; }
4141

4242
private bool _showJunctionMenu = false;
43+
private bool _isRemovalMode = false;
4344

4445
protected override void OnInitialized()
4546
{
@@ -61,18 +62,20 @@ private int GetPredictionDelta(Stat stat)
6162
if (DragDropService.HoveredTarget?.character.Name != Character.Name || DragDropService.HoveredTarget?.stat.Name != stat.Name)
6263
return 0;
6364

64-
if (DragDropService.Data is not ItemQuantity spell || spell.Item.ItemType != ItemType.Spell)
65-
return 0;
65+
Item? draggedMagic = null;
66+
if (DragDropService.Data is ItemQuantity iq && iq.Item.ItemType == ItemType.Spell) draggedMagic = iq.Item;
67+
if (DragDropService.Data is Item i && i.ItemType == ItemType.Spell) draggedMagic = i;
6668

67-
if (!CanJunction(stat))
69+
if (draggedMagic == null || !CanJunction(stat))
6870
return 0;
6971

72+
Item activeMagic = (Item)draggedMagic;
7073
int currentVal = JunctionManager.GetStatValue(Character, stat.Name);
7174

7275
// Calculate what it WOULD be
7376
int newVal = 10;
74-
int qty = resourceManager.Inventory.GetQuantity(spell.Item);
75-
var augments = ContentHost.GetContent<StatAugments>()[spell.Item];
77+
int qty = resourceManager.Inventory.GetQuantity(activeMagic);
78+
var augments = ContentHost.GetContent<StatAugments>()[activeMagic];
7679
var augment = augments.FirstOrDefault(a => a.Stat.Name == stat.Name);
7780
if (augment.Stat.Name != null)
7881
{
@@ -83,12 +86,19 @@ private int GetPredictionDelta(Stat stat)
8386
newVal += qty / 10;
8487
}
8588

89+
newVal = Math.Min(255, newVal);
90+
8691
return newVal - currentVal;
8792
}
8893

94+
8995
private void HandleStatDragEnter(Stat stat)
9096
{
91-
if (DragDropService.Data is ItemQuantity spell && spell.Item.ItemType == ItemType.Spell && CanJunction(stat))
97+
bool isSpell = false;
98+
if (DragDropService.Data is ItemQuantity iq && iq.Item.ItemType == ItemType.Spell) isSpell = true;
99+
if (DragDropService.Data is Item i && i.ItemType == ItemType.Spell) isSpell = true;
100+
101+
if (isSpell && CanJunction(stat))
92102
{
93103
DragDropService.SetHoveredTarget(Character, stat);
94104
}
@@ -101,15 +111,35 @@ private void HandleStatDragLeave()
101111

102112
private void HandleStatDrop(Stat stat)
103113
{
104-
if (DragDropService.Data is ItemQuantity spell && spell.Item.ItemType == ItemType.Spell && CanJunction(stat))
114+
Item? droppedMagic = null;
115+
if (DragDropService.Data is ItemQuantity iq && iq.Item.ItemType == ItemType.Spell) droppedMagic = iq.Item;
116+
if (DragDropService.Data is Item i && i.ItemType == ItemType.Spell) droppedMagic = i;
117+
118+
if (droppedMagic != null && CanJunction(stat))
105119
{
106-
JunctionManager.JunctionMagic(Character, stat, spell.Item, resourceManager.UnlockedAbilities);
120+
JunctionManager.JunctionMagic(Character, stat, (Item)droppedMagic, resourceManager.UnlockedAbilities);
107121
DragDropService.ClearHoveredTarget();
108122
DragDropService.Data = null;
109123
}
110124
}
111125

112-
private void ToggleJunctionMenu() => _showJunctionMenu = !_showJunctionMenu;
126+
private void ToggleJunctionMenu()
127+
{
128+
_showJunctionMenu = !_showJunctionMenu;
129+
if (_showJunctionMenu) _isRemovalMode = false;
130+
}
131+
132+
private void ToggleRemovalMode()
133+
{
134+
_isRemovalMode = !_isRemovalMode;
135+
if (_isRemovalMode) _showJunctionMenu = false;
136+
}
137+
138+
private void ClearJunction(Stat stat)
139+
{
140+
JunctionManager.JunctionMagic(Character, stat, new Item(), resourceManager.UnlockedAbilities);
141+
StateHasChanged();
142+
}
113143

114144
private void ToggleAutoQuest()
115145
{

Mythril.Blazor/Components/CharacterDisplay.razor.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@
5757
opacity: 0.5;
5858
cursor: not-allowed;
5959
}
60+
.stat-badge.removal-target {
61+
border: 1px dashed #dc3545;
62+
cursor: pointer;
63+
animation: pulse-red 2s infinite;
64+
}
65+
66+
@keyframes pulse-red {
67+
0% { background-color: rgba(220, 53, 69, 0.1); }
68+
50% { background-color: rgba(220, 53, 69, 0.3); }
69+
100% { background-color: rgba(220, 53, 69, 0.1); }
70+
}
71+
6072
.stat-delta {
6173
font-weight: bold;
6274
margin-left: 2px;

Mythril.Data/JunctionManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public int GetStatValue(Character character, string statName)
103103
}
104104
}
105105

106-
return baseValue;
106+
return Math.Min(255, baseValue);
107107
}
108108

109109
public void RestoreAssignment(Cadence cadence, Character character)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using Bunit;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Mythril.Blazor.Components;
4+
using Mythril.Data;
5+
using System.Linq;
6+
7+
namespace Mythril.Tests;
8+
9+
[TestClass]
10+
public class JunctionOverhaulTests : BunitTestBase
11+
{
12+
[TestMethod]
13+
public void StatValue_EnforcesCeiling()
14+
{
15+
// Arrange
16+
var character = new Character("Hero");
17+
var stat = Stats.All.First(s => s.Name == "Strength");
18+
var magic = new Item("Ultimate Magic", "OP", ItemType.Spell);
19+
20+
// Increase capacity to allow reaching 255
21+
InventoryManager.MagicCapacity = 10000;
22+
InventoryManager.Add(magic, 10000);
23+
24+
// Setup junction
25+
JunctionManager.Junctions.Add(new Junction(character, stat, magic));
26+
27+
// Act
28+
int val = JunctionManager.GetStatValue(character, "Strength");
29+
30+
// Assert
31+
Assert.AreEqual(255, val, "Stat should be capped at 255");
32+
}
33+
34+
[TestMethod]
35+
public void CharacterDisplay_RemovalMode_TogglesCorrectly()
36+
{
37+
// Arrange
38+
var character = new Character("Hero");
39+
var cadence = new Cadence("Warrior", "Desc", []);
40+
JunctionManager.AssignCadence(cadence, character, []);
41+
42+
var cut = RenderComponent<CharacterDisplay>(p => p
43+
.Add(cp => cp.Character, character)
44+
);
45+
46+
// Act - Click removal button (link_off icon)
47+
var removalBtn = cut.Find("button[title='Remove Junctions']");
48+
removalBtn.Click();
49+
50+
// Assert
51+
Assert.IsTrue(removalBtn.ClassList.Contains("btn-danger"));
52+
53+
// Act - Toggle menu should turn off removal
54+
var menuBtn = cut.Find("button[title='Junction Menu']");
55+
menuBtn.Click();
56+
57+
// Assert
58+
Assert.IsFalse(removalBtn.ClassList.Contains("btn-danger"));
59+
}
60+
61+
[TestMethod]
62+
public void CharacterDisplay_ShowsDeltaPreview()
63+
{
64+
// Arrange
65+
var character = new Character("Hero");
66+
var stat = Stats.All.First(s => s.Name == "Strength");
67+
var magic = new Item("Fire", "Burn", ItemType.Spell);
68+
69+
InventoryManager.MagicCapacity = 100;
70+
InventoryManager.Add(magic, 50); // 50 / 10 = +5 Strength
71+
72+
// Ensure CanJunction returns true
73+
var cadence = new Cadence("Warrior", "Desc", [new CadenceUnlock("Warrior", new CadenceAbility("J-Str", "Desc"), [])]);
74+
JunctionManager.AssignCadence(cadence, character, ["Warrior:J-Str"]);
75+
ResourceManager.UnlockedAbilities.Add("Warrior:J-Str");
76+
77+
// Mock dragging the item
78+
DragDropService.Data = magic;
79+
DragDropService.SetHoveredTarget(character, stat);
80+
81+
var cut = RenderComponent<CharacterDisplay>(p => p
82+
.Add(cp => cp.Character, character)
83+
);
84+
85+
// Act & Assert
86+
var delta = cut.Find(".stat-delta");
87+
Assert.IsTrue(delta.TextContent.Contains("↑5"));
88+
Assert.IsTrue(delta.ClassList.Contains("text-success"));
89+
}
90+
}

README.md

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Mythril is an RPG-inspired web application built with **.NET 10** and **Blazor W
2222

2323
### Key Systems
2424
- **Character Core**: Modular system where characters share baseline stats (**Strength, Vitality, Magic, Speed**), differentiated by assigned Cadences and junctioned magic. Features immersive visual feedback for task completion and per-character equipment management.
25-
- **Junctioning**: Assign magic items to character stats to gain powerful bonuses, inspired by classic RPG mechanics.
25+
- **Junctioning**: Assign magic items to character stats to gain powerful bonuses. Features a tactile drag-and-drop system from the inventory directly onto character stats, complete with predictive effect previews and a specialized removal tool.
2626
- **Cadence System**: Progression mechanic where `Cadences` provide `CadenceAbilities`. Unlocking is performed by dragging ability nodes directly onto characters.
2727
- **Journal System**: Persistent historical log tracking the last 50 completed tasks, providing transparency into character activities and resource gains.
2828
- **Quest & Progression**: Real-time asynchronous tick system managing quests, durations, and rewards, with offline progress continuity and interconnected world unlocks.
@@ -36,28 +36,22 @@ Mythril is an RPG-inspired web application built with **.NET 10** and **Blazor W
3636
- **CI/CD**: GitHub Actions for automated deployment and health monitoring.
3737

3838
## 🚀 Recent Updates (March 4, 2026)
39+
- **Tactile Junction Overhaul**: Transitioned to a pure drag-from-inventory model for Junctioning. Added color-coded stat delta previews (↑/↓) and a dedicated "Link Off" removal tool.
40+
- **Stat Ceiling Enforcement**: Implemented a global **255** maximum cap for all character stats to ensure long-term game balance.
41+
- **Tier II Multi-Tasking & Automation**: Expanded character capacity to a 3rd task slot via **Logistics II** and enabled automation for the 2nd slot with **AutoQuest II** (Scholar cadence).
3942
- **Historical Journal**: Integrated a new Journal tab that tracks task completion history across sessions, including character names and specific rewards.
4043
- **Task Sorting**: Added a duration-based sorting toggle to the Locations panel, allowing players to easily prioritize tasks based on their playstyle.
4144
- **Requirement Iconography**: Implemented standardized icons (🛡️ for stats, 📦 for items, 🔑 for prerequisites) across all quest and ability cards for improved readability.
42-
- **Cadence Completion Tracking**: Implemented a progress counter and checkmark system for Cadences, similar to Location tracking. Users can now see how many abilities have been unlocked for each job at a glance.
43-
- **Location Completion Tracking**: Added a quest counter to location expanders, showing progress on one-time quests. A green checkmark now appears when all unique tasks in a region are finished.
44-
- **Logistics I Ability**: Implemented a new progression tier allowing characters to perform two tasks simultaneously. Features automated task cancellation and cost refunding when the ability is lost.
45-
- **Location Gating System**: Refactored the world map to gate major biomes (Whispering Woods, Ancient Ruins, etc.) behind prerequisite story quests, improving early-game focus and sense of discovery.
46-
- **Refined Workshop Reactivity**: Optimized the refinement UI to ensure immediate visual updates when new abilities are learned, powered by reactive parameter binding.
47-
- **Auto-Quest Slot Restriction**: Balanced the Auto-Quest I ability to specifically target only the primary task slot, adding strategic depth to multi-tasking.
48-
- **Monolith Prevention Refactor**: Decomposed the `ResourceManager` into specialized partial classes (`State`, `Discovery`, `Inventory`, `Quests`, `Journal`, `Rewards`) to maintain a lean, maintainable architecture below the 250-line file limit.
49-
- **Persistence Layer Upgrade**: Enhanced the save system to preserve task slot assignments and discovered location names across sessions.
50-
- **UI Streamlining**: Restacked header controls vertically and cleaned up instructional text across all main panels for a more focused gameplay experience.
45+
- **Monolith Prevention Refactor**: Decomposed the `ResourceManager` into specialized partial classes (`State`, `Discovery`, `Inventory`, `Quests`, `Journal`, `Rewards`, `Logistics`) to maintain a lean, maintainable architecture.
5146

5247
## ⚖️ Quality Assurance & Health
5348
We maintain project health through a custom automated suite (`scripts/check_health.py`) which runs on every commit:
5449
- **Monolith Prevention**: Strict 250-line limit for source files (excluding tools).
55-
- **Game Graph Simulation**: Integrated Fixed-Point Iteration engine that mathematically verifies every quest and resource is attainable from a fresh start. Prevents "logic orphans" and provides estimated optimal completion times for the endgame.
56-
- **Automated Balancing**: The reachability simulation is part of the health suite. If a content change makes a quest mathematically impossible (e.g. required stat cannot be reached), the build fails.
50+
- **Game Graph Simulation**: Integrated Fixed-Point Iteration engine that mathematically verifies every quest and resource is attainable from a fresh start.
51+
- **Automated Balancing**: The reachability simulation is part of the health suite. If a content change makes a quest mathematically impossible, the build fails.
5752
- **Coverage**: Mandatory 70% overall line coverage; 25% per-file minimum.
5853
- **Razor Integrity**: All interactive components must have bUnit tests, `@key` usage in loops, and `data-testid` anchors.
5954
- **Documentation Integrity**: Automated staleness tracking via local file modification times.
60-
- **Feedback Integrity**: Monitoring of pending user feedback and runtime errors in `docs/feedback/`.
6155

6256
### 🛡️ Health Shield Guide
6357
All badges are automatically updated by `scripts/check_health.py` on every commit:

0 commit comments

Comments
 (0)