adding abilities, created skill tree template and unlock mechanics
This commit is contained in:
127
public_web/templates/character/partials/skills_container.html
Normal file
127
public_web/templates/character/partials/skills_container.html
Normal file
@@ -0,0 +1,127 @@
|
||||
{# Skills Container Partial - For HTMX updates #}
|
||||
{# Calculate available skill points: level minus unlocked skills count #}
|
||||
{% set skill_points = character.available_skill_points|default(character.level - (character.unlocked_skills|default([])|length), true) %}
|
||||
|
||||
<div class="skills-container" id="skills-container">
|
||||
{# Back Link #}
|
||||
<div class="skills-back-link">
|
||||
<a href="{{ url_for('character_views.view_character', character_id=character.character_id) }}">
|
||||
← Back to {{ character.name }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Header #}
|
||||
<div class="skills-header">
|
||||
<h1>{{ character.name }}'s Skill Trees</h1>
|
||||
<div class="skills-info">
|
||||
<div class="skill-points-display">
|
||||
<span class="skill-points-label">Skill Points:</span>
|
||||
<span class="skill-points-value">{{ skill_points }}</span>
|
||||
</div>
|
||||
{% set respec_cost = character.level * 100 %}
|
||||
<button class="btn-respec"
|
||||
hx-post="{{ url_for('character_views.respec_skills', character_id=character.character_id) }}"
|
||||
hx-target="#skills-container"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Respec will reset all skills and cost {{ respec_cost }} gold. Continue?"
|
||||
{% if character.gold < respec_cost or not character.unlocked_skills %}disabled{% endif %}>
|
||||
Respec ({{ respec_cost }} gold)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Flash Messages #}
|
||||
{% if message %}
|
||||
<div class="skills-message skills-message--success">{{ message }}</div>
|
||||
{% endif %}
|
||||
{% if error %}
|
||||
<div class="skills-message skills-message--error">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{# Skill Trees Grid #}
|
||||
{% if player_class and player_class.skill_trees %}
|
||||
<div class="skill-trees-grid">
|
||||
{% for tree in player_class.skill_trees %}
|
||||
<div class="skill-tree" data-tree-id="{{ tree.tree_id }}">
|
||||
{# Tree Header #}
|
||||
<div class="tree-header">
|
||||
<h2 class="tree-name">{{ tree.name }}</h2>
|
||||
<p class="tree-description">{{ tree.description }}</p>
|
||||
</div>
|
||||
|
||||
{# Tree Diagram (Tiers 5 to 1, top to bottom) #}
|
||||
<div class="tree-diagram">
|
||||
{% for tier in range(5, 0, -1) %}
|
||||
<div class="skill-tier" data-tier="{{ tier }}">
|
||||
<span class="tier-label">Tier {{ tier }}</span>
|
||||
<div class="skill-nodes">
|
||||
{% for node in tree.nodes if node.tier == tier %}
|
||||
{# Determine node state #}
|
||||
{% set is_unlocked = node.skill_id in character.unlocked_skills %}
|
||||
{% set prereqs_met = not node.prerequisites or (node.prerequisites | select('in', character.unlocked_skills) | list | length == node.prerequisites | length) %}
|
||||
{% set has_lower_tier = tier == 1 or (tree.nodes | selectattr('tier', 'equalto', tier - 1) | selectattr('skill_id', 'in', character.unlocked_skills) | list | length > 0) %}
|
||||
{% set can_unlock = not is_unlocked and prereqs_met and has_lower_tier and skill_points > 0 %}
|
||||
{% set has_prereq = node.prerequisites | length > 0 %}
|
||||
|
||||
<div class="skill-node {% if is_unlocked %}skill-node--unlocked{% elif can_unlock %}skill-node--available{% else %}skill-node--locked{% endif %}{% if has_prereq %} skill-node--has-prereq{% endif %}"
|
||||
data-skill-id="{{ node.skill_id }}"
|
||||
data-tree-id="{{ tree.tree_id }}"
|
||||
onmouseenter="showTooltip(event, '{{ node.skill_id }}')"
|
||||
onmouseleave="hideTooltip()">
|
||||
|
||||
<div class="node-icon">
|
||||
{% if is_unlocked %}
|
||||
✓
|
||||
{% elif can_unlock %}
|
||||
◇
|
||||
{% else %}
|
||||
◆
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<span class="node-name">{{ node.name }}</span>
|
||||
|
||||
{% if can_unlock %}
|
||||
<button class="btn-unlock"
|
||||
hx-post="{{ url_for('character_views.unlock_skill', character_id=character.character_id) }}"
|
||||
hx-vals='{"skill_id": "{{ node.skill_id }}"}'
|
||||
hx-target="#skills-container"
|
||||
hx-swap="outerHTML"
|
||||
onclick="event.stopPropagation()">
|
||||
Unlock
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Legend #}
|
||||
<div class="skills-legend">
|
||||
<div class="legend-item">
|
||||
<span class="legend-icon legend-icon--unlocked">✓</span>
|
||||
<span>Unlocked</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-icon legend-icon--available">◇</span>
|
||||
<span>Available</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-icon legend-icon--locked">◆</span>
|
||||
<span>Locked</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="skills-message skills-message--error">
|
||||
Unable to load skill trees for this class.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Tooltip Container #}
|
||||
<div id="skill-tooltip" class="skill-tooltip"></div>
|
||||
</div>
|
||||
151
public_web/templates/character/skills.html
Normal file
151
public_web/templates/character/skills.html
Normal file
@@ -0,0 +1,151 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Skill Trees - {{ character.name }} - Code of Conquest{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/skills.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "character/partials/skills_container.html" %}
|
||||
|
||||
{# Embed skill data as JSON for tooltips #}
|
||||
<script>
|
||||
// Skill data for tooltips (embedded from server)
|
||||
const skillData = {
|
||||
{% if player_class and player_class.skill_trees %}
|
||||
{% for tree in player_class.skill_trees %}
|
||||
{% for node in tree.nodes %}
|
||||
"{{ node.skill_id }}": {
|
||||
name: "{{ node.name }}",
|
||||
description: "{{ node.description | replace('"', '\\"') | replace('\n', ' ') }}",
|
||||
tier: {{ node.tier }},
|
||||
prerequisites: {{ node.prerequisites | tojson }},
|
||||
effects: {{ node.effects | tojson if node.effects else '{}' }}
|
||||
}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
};
|
||||
|
||||
// Character's unlocked skills
|
||||
const unlockedSkills = {{ character.unlocked_skills | tojson }};
|
||||
|
||||
// Tooltip element
|
||||
const tooltip = document.getElementById('skill-tooltip');
|
||||
|
||||
/**
|
||||
* Show tooltip for a skill node
|
||||
*/
|
||||
function showTooltip(event, skillId) {
|
||||
const skill = skillData[skillId];
|
||||
if (!skill) return;
|
||||
|
||||
// Build tooltip HTML
|
||||
let html = `
|
||||
<div class="tooltip-header">
|
||||
<h3 class="tooltip-name">${skill.name}</h3>
|
||||
<span class="tooltip-tier">Tier ${skill.tier}</span>
|
||||
</div>
|
||||
<p class="tooltip-description">${skill.description}</p>
|
||||
`;
|
||||
|
||||
// Stat bonuses
|
||||
if (skill.effects && skill.effects.stat_bonuses) {
|
||||
const bonuses = skill.effects.stat_bonuses;
|
||||
const bonusList = Object.entries(bonuses)
|
||||
.map(([stat, value]) => `<li>+${value} ${formatStatName(stat)}</li>`)
|
||||
.join('');
|
||||
html += `
|
||||
<div class="tooltip-section">
|
||||
<div class="tooltip-section-title">Stat Bonuses</div>
|
||||
<ul class="tooltip-bonuses">${bonusList}</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Abilities unlocked
|
||||
if (skill.effects && skill.effects.abilities) {
|
||||
const abilities = skill.effects.abilities;
|
||||
const abilityList = abilities.map(a => `<li>${formatAbilityName(a)}</li>`).join('');
|
||||
html += `
|
||||
<div class="tooltip-section">
|
||||
<div class="tooltip-section-title">Unlocks Ability</div>
|
||||
<ul class="tooltip-abilities">${abilityList}</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Prerequisites
|
||||
if (skill.prerequisites && skill.prerequisites.length > 0) {
|
||||
const prereqNames = skill.prerequisites.map(p => {
|
||||
const prereqSkill = skillData[p];
|
||||
const met = unlockedSkills.includes(p);
|
||||
return `<span style="color: ${met ? 'var(--accent-green)' : 'var(--accent-red-light)'}">${prereqSkill ? prereqSkill.name : p}</span>`;
|
||||
}).join(', ');
|
||||
html += `
|
||||
<div class="tooltip-section">
|
||||
<p class="tooltip-prerequisite"><strong>Requires:</strong> ${prereqNames}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
tooltip.innerHTML = html;
|
||||
|
||||
// Position tooltip
|
||||
const rect = event.target.closest('.skill-node').getBoundingClientRect();
|
||||
const tooltipRect = tooltip.getBoundingClientRect();
|
||||
|
||||
let left = rect.right + 10;
|
||||
let top = rect.top;
|
||||
|
||||
// Check if tooltip goes off right edge
|
||||
if (left + 300 > window.innerWidth) {
|
||||
left = rect.left - 310;
|
||||
}
|
||||
|
||||
// Check if tooltip goes off bottom edge
|
||||
if (top + tooltipRect.height > window.innerHeight) {
|
||||
top = window.innerHeight - tooltipRect.height - 10;
|
||||
}
|
||||
|
||||
tooltip.style.left = left + 'px';
|
||||
tooltip.style.top = top + 'px';
|
||||
tooltip.classList.add('visible');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide tooltip
|
||||
*/
|
||||
function hideTooltip() {
|
||||
tooltip.classList.remove('visible');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format stat name for display
|
||||
*/
|
||||
function formatStatName(stat) {
|
||||
const names = {
|
||||
strength: 'Strength',
|
||||
dexterity: 'Dexterity',
|
||||
constitution: 'Constitution',
|
||||
intelligence: 'Intelligence',
|
||||
wisdom: 'Wisdom',
|
||||
charisma: 'Charisma',
|
||||
luck: 'Luck',
|
||||
defense: 'Defense',
|
||||
resistance: 'Resistance',
|
||||
mana_points: 'Mana',
|
||||
hit_points: 'Hit Points'
|
||||
};
|
||||
return names[stat] || stat.charAt(0).toUpperCase() + stat.slice(1).replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format ability name for display
|
||||
*/
|
||||
function formatAbilityName(ability) {
|
||||
return ability.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -82,7 +82,7 @@ Displays character stats, resource bars, and action buttons
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Quick Actions (Inventory, Equipment, NPC, Travel) #}
|
||||
{# Quick Actions (Inventory, Equipment, Skills, NPC, Travel) #}
|
||||
<div class="quick-actions">
|
||||
{# Inventory - Opens modal #}
|
||||
<button class="action-btn action-btn--special"
|
||||
@@ -103,6 +103,16 @@ Displays character stats, resource bars, and action buttons
|
||||
⚔️ Equipment & Gear
|
||||
</button>
|
||||
|
||||
{# Skill Trees - Direct link to skills page #}
|
||||
<a class="action-btn action-btn--special"
|
||||
href="{{ url_for('character_views.view_skills', character_id=character.character_id) }}">
|
||||
🌱 Skill Trees
|
||||
{% set skill_pts = character.available_skill_points|default(character.level - (character.unlocked_skills|default([])|length), true) %}
|
||||
{% if skill_pts > 0 %}
|
||||
<span class="action-count action-count--highlight">({{ skill_pts }} pts)</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
{# Talk to NPC - Opens NPC accordion #}
|
||||
<button class="action-btn action-btn--special"
|
||||
hx-get="{{ url_for('game.npcs_accordion', session_id=session_id) }}"
|
||||
|
||||
Reference in New Issue
Block a user