feat: extend equipment and workout models with service tracking
This commit is contained in:
175
internal/templates/handler.go
Normal file
175
internal/templates/handler.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"rideaware/internal/config"
|
||||
"rideaware/internal/middleware"
|
||||
"rideaware/internal/workout"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
workoutRepo *workout.Repository
|
||||
}
|
||||
|
||||
func NewHandler() *Handler {
|
||||
return &Handler{
|
||||
workoutRepo: workout.NewRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
// TemplateSummary is a lighter view of a template for listing.
|
||||
type TemplateSummary struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Duration int `json:"duration"`
|
||||
Difficulty string `json:"difficulty"`
|
||||
Category string `json:"category"`
|
||||
Segments int `json:"segments"`
|
||||
}
|
||||
|
||||
// ListTemplates GET /api/protected/workout-templates
|
||||
func (h *Handler) ListTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
category := r.URL.Query().Get("category")
|
||||
|
||||
var source []Template
|
||||
if category != "" {
|
||||
source = GetByCategory(category)
|
||||
} else {
|
||||
source = All
|
||||
}
|
||||
|
||||
summaries := make([]TemplateSummary, 0, len(source))
|
||||
for _, t := range source {
|
||||
summaries = append(summaries, TemplateSummary{
|
||||
ID: t.ID,
|
||||
Name: t.Name,
|
||||
Description: t.Description,
|
||||
Type: t.Type,
|
||||
Duration: t.Duration,
|
||||
Difficulty: t.Difficulty,
|
||||
Category: t.Category,
|
||||
Segments: len(t.Segments),
|
||||
})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(summaries)
|
||||
}
|
||||
|
||||
// GetTemplate GET /api/protected/workout-templates/detail?id=X
|
||||
func (h *Handler) GetTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
if id == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "template id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
t := GetByID(id)
|
||||
if t == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "template not found"})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(t)
|
||||
}
|
||||
|
||||
// CreateFromTemplate POST /api/protected/workouts/from-template
|
||||
func (h *Handler) CreateFromTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims)
|
||||
if claims == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
ScheduledDate string `json:"scheduled_date"`
|
||||
Title string `json:"title"`
|
||||
Notes string `json:"notes"`
|
||||
EquipmentID *uint `json:"equipment_id"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.TemplateID == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "template_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := GetByID(req.TemplateID)
|
||||
if tmpl == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "template not found"})
|
||||
return
|
||||
}
|
||||
|
||||
scheduledDate := time.Now()
|
||||
if req.ScheduledDate != "" {
|
||||
parsed, err := time.Parse("2006-01-02", req.ScheduledDate)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "invalid scheduled_date format, use YYYY-MM-DD"})
|
||||
return
|
||||
}
|
||||
scheduledDate = parsed
|
||||
}
|
||||
|
||||
title := req.Title
|
||||
if title == "" {
|
||||
title = tmpl.Name
|
||||
}
|
||||
|
||||
newWorkout := &workout.Workout{
|
||||
UserID: claims.UserID,
|
||||
Title: title,
|
||||
Description: tmpl.Description,
|
||||
Type: tmpl.Type,
|
||||
Status: "planned",
|
||||
ScheduledDate: scheduledDate,
|
||||
Duration: tmpl.Duration,
|
||||
EquipmentID: req.EquipmentID,
|
||||
Notes: req.Notes,
|
||||
WorkoutData: workout.WorkoutDataJSON{
|
||||
Name: tmpl.Name,
|
||||
Author: "RideAware",
|
||||
TotalDuration: tmpl.Duration,
|
||||
Segments: tmpl.Segments,
|
||||
},
|
||||
}
|
||||
|
||||
if err := h.workoutRepo.CreateWorkout(newWorkout); err != nil {
|
||||
log.Printf("Create from template error: %v", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "failed to create workout"})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(newWorkout)
|
||||
}
|
||||
304
internal/templates/templates.go
Normal file
304
internal/templates/templates.go
Normal file
@@ -0,0 +1,304 @@
|
||||
package templates
|
||||
|
||||
import "rideaware/internal/workout"
|
||||
|
||||
// Template represents a predefined workout template.
|
||||
type Template struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Duration int `json:"duration"`
|
||||
Difficulty string `json:"difficulty"` // "easy", "moderate", "hard", "very_hard"
|
||||
Category string `json:"category"` // "endurance", "threshold", "vo2max", "recovery", "sprint", "sweet_spot"
|
||||
Segments []workout.WorkoutSegment `json:"segments"`
|
||||
}
|
||||
|
||||
// All returns the full list of workout templates.
|
||||
var All = []Template{
|
||||
{
|
||||
ID: "recovery-spin",
|
||||
Name: "Recovery Spin",
|
||||
Description: "Easy spin to promote blood flow and recovery. Keep it light and conversational.",
|
||||
Type: "Recovery",
|
||||
Duration: 1800, // 30 min
|
||||
Difficulty: "easy",
|
||||
Category: "recovery",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 300, PowerLow: 0.30, PowerHigh: 0.45},
|
||||
{Type: "steadystate", Duration: 1200, Power: 0.45, Cadence: 90},
|
||||
{Type: "cooldown", Duration: 300, PowerLow: 0.45, PowerHigh: 0.30},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "endurance-60",
|
||||
Name: "Endurance Base",
|
||||
Description: "Steady zone 2 ride to build aerobic base. Maintain a comfortable, sustainable effort.",
|
||||
Type: "Endurance",
|
||||
Duration: 3600, // 60 min
|
||||
Difficulty: "easy",
|
||||
Category: "endurance",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.65},
|
||||
{Type: "steadystate", Duration: 2400, Power: 0.65, Cadence: 85},
|
||||
{Type: "cooldown", Duration: 600, PowerLow: 0.65, PowerHigh: 0.40},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "endurance-90",
|
||||
Name: "Long Endurance",
|
||||
Description: "Extended zone 2 ride for deep aerobic adaptation. Pack snacks.",
|
||||
Type: "Endurance",
|
||||
Duration: 5400, // 90 min
|
||||
Difficulty: "moderate",
|
||||
Category: "endurance",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.65},
|
||||
{Type: "steadystate", Duration: 4200, Power: 0.68, Cadence: 85},
|
||||
{Type: "cooldown", Duration: 600, PowerLow: 0.65, PowerHigh: 0.40},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "tempo-45",
|
||||
Name: "Tempo Ride",
|
||||
Description: "Sustained zone 3 effort. Comfortably hard - you can talk in short sentences.",
|
||||
Type: "Tempo",
|
||||
Duration: 2700, // 45 min
|
||||
Difficulty: "moderate",
|
||||
Category: "endurance",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.70},
|
||||
{Type: "steadystate", Duration: 1200, Power: 0.78, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.55},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.78, Cadence: 90},
|
||||
{Type: "cooldown", Duration: 300, PowerLow: 0.65, PowerHigh: 0.40},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "sweet-spot-60",
|
||||
Name: "Sweet Spot",
|
||||
Description: "The most time-efficient training zone. 88-94% FTP intervals with short recovery.",
|
||||
Type: "Threshold",
|
||||
Duration: 3600, // 60 min
|
||||
Difficulty: "moderate",
|
||||
Category: "sweet_spot",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.75},
|
||||
{Type: "steadystate", Duration: 600, Power: 0.90, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.55},
|
||||
{Type: "steadystate", Duration: 600, Power: 0.92, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.55},
|
||||
{Type: "steadystate", Duration: 600, Power: 0.90, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.55},
|
||||
{Type: "cooldown", Duration: 660, PowerLow: 0.65, PowerHigh: 0.40},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "threshold-intervals",
|
||||
Name: "Threshold Intervals",
|
||||
Description: "Classic 2x20 at FTP. The gold standard for raising your threshold.",
|
||||
Type: "Threshold",
|
||||
Duration: 3600, // 60 min
|
||||
Difficulty: "hard",
|
||||
Category: "threshold",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.75},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.80, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 1200, Power: 1.00, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.50},
|
||||
{Type: "steadystate", Duration: 1200, Power: 1.00, Cadence: 95},
|
||||
{Type: "cooldown", Duration: 600, PowerLow: 0.65, PowerHigh: 0.40},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "over-unders",
|
||||
Name: "Over-Under Intervals",
|
||||
Description: "Alternating between just below and just above FTP. Builds lactate tolerance.",
|
||||
Type: "Threshold",
|
||||
Duration: 3600, // 60 min
|
||||
Difficulty: "hard",
|
||||
Category: "threshold",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.75},
|
||||
// Set 1
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.50},
|
||||
// Set 2
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.50},
|
||||
// Set 3
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "cooldown", Duration: 600, PowerLow: 0.65, PowerHigh: 0.40},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "vo2max-intervals",
|
||||
Name: "VO2max Intervals",
|
||||
Description: "5x3min at 115% FTP with equal rest. Expands your aerobic ceiling.",
|
||||
Type: "VO2 Max",
|
||||
Duration: 2700, // 45 min
|
||||
Difficulty: "very_hard",
|
||||
Category: "vo2max",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.75},
|
||||
{Type: "steadystate", Duration: 180, Power: 1.15, Cadence: 100},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.45},
|
||||
{Type: "steadystate", Duration: 180, Power: 1.15, Cadence: 100},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.45},
|
||||
{Type: "steadystate", Duration: 180, Power: 1.15, Cadence: 100},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.45},
|
||||
{Type: "steadystate", Duration: 180, Power: 1.15, Cadence: 100},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.45},
|
||||
{Type: "steadystate", Duration: 180, Power: 1.15, Cadence: 100},
|
||||
{Type: "cooldown", Duration: 420, PowerLow: 0.55, PowerHigh: 0.35},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "vo2max-short",
|
||||
Name: "VO2max Short-Shorts",
|
||||
Description: "30/30s at 120% FTP. Accumulate VO2max time in manageable chunks.",
|
||||
Type: "VO2 Max",
|
||||
Duration: 2400, // 40 min
|
||||
Difficulty: "very_hard",
|
||||
Category: "vo2max",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.75},
|
||||
// Set 1: 10x 30/30
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.40},
|
||||
// Set 2: 10x 30/30
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "cooldown", Duration: 300, PowerLow: 0.50, PowerHigh: 0.35},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "sprint-intervals",
|
||||
Name: "Sprint Power",
|
||||
Description: "Short maximal efforts to build neuromuscular power and sprint capacity.",
|
||||
Type: "VO2 Max",
|
||||
Duration: 2700, // 45 min
|
||||
Difficulty: "very_hard",
|
||||
Category: "sprint",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.75},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.85, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.50},
|
||||
// Sprints: 8x 15s all-out / 105s recovery
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "cooldown", Duration: 480, PowerLow: 0.55, PowerHigh: 0.35},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "ramp-test",
|
||||
Name: "FTP Ramp Test",
|
||||
Description: "Progressive ramp to estimate your FTP. Ride until you can't hold the target.",
|
||||
Type: "Threshold",
|
||||
Duration: 1500, // ~25 min (most people fail around 19-22 min)
|
||||
Difficulty: "very_hard",
|
||||
Category: "threshold",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 300, PowerLow: 0.40, PowerHigh: 0.46},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.46, PowerHigh: 0.52},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.52, PowerHigh: 0.58},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.58, PowerHigh: 0.64},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.64, PowerHigh: 0.70},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.70, PowerHigh: 0.76},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.76, PowerHigh: 0.82},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.82, PowerHigh: 0.88},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.88, PowerHigh: 0.94},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.94, PowerHigh: 1.00},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 1.00, PowerHigh: 1.06},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 1.06, PowerHigh: 1.12},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 1.12, PowerHigh: 1.18},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 1.18, PowerHigh: 1.24},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 1.24, PowerHigh: 1.30},
|
||||
{Type: "cooldown", Duration: 300, PowerLow: 0.50, PowerHigh: 0.30},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// GetByID returns a template by its ID, or nil if not found.
|
||||
func GetByID(id string) *Template {
|
||||
for i := range All {
|
||||
if All[i].ID == id {
|
||||
return &All[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByCategory returns all templates matching a category.
|
||||
func GetByCategory(category string) []Template {
|
||||
var result []Template
|
||||
for _, t := range All {
|
||||
if t.Category == category {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user