Import gender tees + front/back split ratings; front/back/9 hole selection
Course import now fetches both male and female tees, each with their own per-tee stroke indices extracted from the holes array. Front and back 9 slope/rating are stored per tee for more accurate WHS handicap calculation. New game creation replaces the 9/18 radio with a course-aware selector: 18-hole courses offer 18 holes, Front 9, or Back 9; 9-hole courses auto- select. start_hole is stored on the game and used throughout scoring, handicap calculation, and scorecard rendering. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
<button @click="view === 'score' ? prevHole() : cardPrev()" class="icon-btn"
|
||||
:disabled="view === 'score' ? currentHole <= 1 : cardView === 'strokes'">‹</button>
|
||||
<div style="text-align:center; flex:1;">
|
||||
<div class="hole-label" x-text="view === 'score' ? 'Hole ' + currentHole + ' / {{ game.holes_count }}' : (cardView === 'strokes' ? 'Strokes' : cardView === 'points' ? 'Points' : 'Skins')"></div>
|
||||
<div class="hole-label" x-text="view === 'score' ? 'Hole ' + currentHole + ' / ' + (START_HOLE + GAME_HOLES - 1) : (cardView === 'strokes' ? 'Strokes' : cardView === 'points' ? 'Points' : 'Skins')"></div>
|
||||
<div style="font-size:0.72rem; color:var(--text-muted); margin-top:0.1rem;">
|
||||
<template x-if="view === 'score'">
|
||||
<span x-text="(HOLE_PARS[currentHole] ? 'Par ' + HOLE_PARS[currentHole] : '') + (HOLE_SIS[currentHole] ? ' · SI ' + HOLE_SIS[currentHole] : '')"></span>
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<button @click="view === 'score' ? nextHole() : cardNext()" class="icon-btn"
|
||||
:disabled="view === 'score' ? currentHole >= {{ game.holes_count }} : cardView === (GAME_FORMAT === 'skins' || SIDE_SKINS_ENABLED ? 'skins' : 'points')">›</button>
|
||||
:disabled="view === 'score' ? currentHole >= START_HOLE + GAME_HOLES - 1 : cardView === (GAME_FORMAT === 'skins' || SIDE_SKINS_ENABLED ? 'skins' : 'points')">›</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,6 +282,7 @@ const SKIN_VALUE = {{ skin_value | tojson }};
|
||||
const SKIN_DOUBLE_BACK9 = {{ skin_double_back9 | tojson }};
|
||||
const SKINS_NET = {{ skins_net | tojson }};
|
||||
const SIDE_SKINS_ENABLED = {{ side_skins_enabled | tojson }};
|
||||
const START_HOLE = {{ start_hole | tojson }};
|
||||
const HOLE_PARS = {{ hole_pars | tojson }};
|
||||
const HOLE_SIS = {{ hole_stroke_indices | tojson }};
|
||||
const STROKES_PER_HOLE = {{ strokes_per_hole | tojson }};
|
||||
@@ -296,7 +297,7 @@ function gameApp() {
|
||||
return {
|
||||
view: 'score',
|
||||
cardView: 'strokes',
|
||||
currentHole: 1,
|
||||
currentHole: START_HOLE,
|
||||
players: [],
|
||||
scores: {}, // { hole: { uid: {strokes,bingo,bango,bongo} } }
|
||||
totals: {},
|
||||
@@ -447,7 +448,7 @@ function gameApp() {
|
||||
},
|
||||
|
||||
allHolesDone() {
|
||||
for (let h = 1; h <= GAME_HOLES; h++) {
|
||||
for (let h = START_HOLE; h <= START_HOLE + GAME_HOLES - 1; h++) {
|
||||
if (this.isSkipped(h)) continue;
|
||||
const holeScores = this.scores[h];
|
||||
if (!holeScores || Object.keys(holeScores).length === 0) return false;
|
||||
@@ -455,8 +456,8 @@ function gameApp() {
|
||||
return true;
|
||||
},
|
||||
|
||||
prevHole() { if (this.currentHole > 1) { this.saveHole(this.currentHole); this.currentHole--; } },
|
||||
nextHole() { if (this.currentHole < GAME_HOLES) { this.saveHole(this.currentHole); this.currentHole++; } },
|
||||
prevHole() { if (this.currentHole > START_HOLE) { this.saveHole(this.currentHole); this.currentHole--; } },
|
||||
nextHole() { if (this.currentHole < START_HOLE + GAME_HOLES - 1) { this.saveHole(this.currentHole); this.currentHole++; } },
|
||||
|
||||
cardTabs() {
|
||||
if (GAME_FORMAT === 'skins') return ['strokes', 'skins'];
|
||||
@@ -508,7 +509,7 @@ function gameApp() {
|
||||
const holes = {};
|
||||
let pot = 0;
|
||||
let moneyPot = 0;
|
||||
for (let hole = 1; hole <= GAME_HOLES; hole++) {
|
||||
for (let hole = START_HOLE; hole <= START_HOLE + GAME_HOLES - 1; hole++) {
|
||||
if (this.isSkipped(hole)) continue;
|
||||
const holeScores = this.scores[hole] || {};
|
||||
const scored = PLAYERS_DATA
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
skinDoubleBackNine: false,
|
||||
skinsNet: false,
|
||||
sideSkinsEnabled: false,
|
||||
holesCount: '',
|
||||
holesMode: '',
|
||||
selectedPlayers: ['{{ user.id }}'],
|
||||
selectedTees: {},
|
||||
playerSearch: '',
|
||||
@@ -49,6 +49,7 @@
|
||||
this.courseLabel = c.name + (c.location ? ' · ' + c.location : '') + ' (' + c.holes + ' holes)';
|
||||
this.courseOpen = false;
|
||||
this.courseSearch = '';
|
||||
this.holesMode = c.holes === 9 ? 'all9' : '';
|
||||
},
|
||||
togglePlayer(id) {
|
||||
const idx = this.selectedPlayers.indexOf(id);
|
||||
@@ -66,11 +67,19 @@
|
||||
const player = this.getPlayer(uid);
|
||||
const tee = this.getTee(uid);
|
||||
const course = COURSES_DATA.find(c => c.id === this.courseId);
|
||||
if (!course || !tee || player.handicap == null) return null;
|
||||
const nineOfEighteen = parseInt(this.holesCount) === 9 && course.holes === 18;
|
||||
const nineHoleDedicated = parseInt(this.holesCount) === 9 && course.holes === 9;
|
||||
// For 9-of-18: use par_9×2 as the 18-hole par, compute full CH then halve.
|
||||
// For dedicated 9-hole: WHS formula uses HI÷2 directly with 9-hole ratings.
|
||||
if (!course || !tee || player.handicap == null || !this.holesMode) return null;
|
||||
const nineOfEighteen = this.holesMode === 'front9' || this.holesMode === 'back9';
|
||||
const nineHoleDedicated = this.holesMode === 'all9';
|
||||
if (this.holesMode === 'front9' && tee.front_slope && tee.front_rating) {
|
||||
if (!course.par_9) return null;
|
||||
return Math.round(player.handicap * (tee.front_slope / 113) + (tee.front_rating - course.par_9));
|
||||
}
|
||||
if (this.holesMode === 'back9' && tee.back_slope && tee.back_rating) {
|
||||
const parBack9 = course.par - course.par_9;
|
||||
if (!parBack9) return null;
|
||||
return Math.round(player.handicap * (tee.back_slope / 113) + (tee.back_rating - parBack9));
|
||||
}
|
||||
// Fallback: for 9-of-18 use par_9×2 as 18-hole par then halve; for dedicated 9-hole use HI÷2.
|
||||
const par = nineOfEighteen ? course.par_9 * 2 : course.par;
|
||||
if (!par) return null;
|
||||
const hcForFormula = nineHoleDedicated ? player.handicap / 2 : player.handicap;
|
||||
@@ -83,6 +92,13 @@
|
||||
if (this.format === 'skins') return 'Skins';
|
||||
if (this.sideSkinsEnabled) return 'Bingo Bango Bongo + Skins';
|
||||
return 'Bingo Bango Bongo';
|
||||
},
|
||||
holesModeLabel() {
|
||||
if (this.holesMode === '18') return '18 holes';
|
||||
if (this.holesMode === 'front9') return 'Front 9 (holes 1–9)';
|
||||
if (this.holesMode === 'back9') return 'Back 9 (holes 10–18)';
|
||||
if (this.holesMode === 'all9') return '9 holes';
|
||||
return '—';
|
||||
}
|
||||
}">
|
||||
|
||||
@@ -144,7 +160,7 @@
|
||||
placeholder="—"
|
||||
style="width:5rem; padding:0.4rem 0.5rem; border-radius:0.4rem; border:1px solid var(--border); background:var(--surface2); color:var(--text); font-size:1rem; text-align:center;">
|
||||
</div>
|
||||
<div x-show="skinValue && holesCount == 18">
|
||||
<div x-show="skinValue && holesMode === '18'">
|
||||
<label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
|
||||
<input type="checkbox" name="skin_double_back_nine" value="1" style="width:auto;"
|
||||
@change="skinDoubleBackNine = $event.target.checked">
|
||||
@@ -201,19 +217,22 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{# ── Holes selection — only for 18-hole courses ── #}
|
||||
<div class="form-group" x-show="courseId && (COURSES_DATA.find(c=>c.id===courseId)||{}).holes===18">
|
||||
<label>Holes</label>
|
||||
<div style="display:flex; gap:0.75rem;">
|
||||
<label class="radio-card">
|
||||
<input type="radio" name="holes_count" value="9" required
|
||||
@change="holesCount = $event.target.value">
|
||||
<span>9 holes</span>
|
||||
</label>
|
||||
<label class="radio-card">
|
||||
<input type="radio" name="holes_count" value="18"
|
||||
@change="holesCount = $event.target.value">
|
||||
<label class="radio-card" style="flex:1;">
|
||||
<input type="radio" name="_holes_mode" value="18" @change="holesMode = '18'">
|
||||
<span>18 holes</span>
|
||||
</label>
|
||||
<label class="radio-card" style="flex:1;">
|
||||
<input type="radio" name="_holes_mode" value="front9" @change="holesMode = 'front9'">
|
||||
<span>Front 9</span>
|
||||
</label>
|
||||
<label class="radio-card" style="flex:1;">
|
||||
<input type="radio" name="_holes_mode" value="back9" @change="holesMode = 'back9'">
|
||||
<span>Back 9</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -249,8 +268,8 @@
|
||||
</div>
|
||||
|
||||
<div style="position:sticky; bottom:calc(var(--nav-height) + env(safe-area-inset-bottom)); background:var(--bg); padding:0.75rem 0; margin-top:0.5rem;">
|
||||
<button type="button" x-show="hasTees()" class="btn btn-primary" @click="step = 2">Next →</button>
|
||||
<button type="button" x-show="!hasTees()" class="btn btn-primary" @click="goToConfirm()">Next →</button>
|
||||
<button type="button" x-show="hasTees()" class="btn btn-primary" @click="step = 2" :disabled="!holesMode">Next →</button>
|
||||
<button type="button" x-show="!hasTees()" class="btn btn-primary" @click="goToConfirm()" :disabled="!holesMode">Next →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -273,7 +292,7 @@
|
||||
style="width:100%; padding:0.65rem 0.75rem; border-radius:0.5rem; border:1px solid var(--border); background:var(--surface); color:var(--text); font-size:1rem;">
|
||||
<option value="" disabled selected>Select tee…</option>
|
||||
<template x-for="t in tees()" :key="t.id">
|
||||
<option :value="t.id" x-text="t.name + ' (Slope ' + t.slope + ' · Rating ' + t.rating + ')'"></option>
|
||||
<option :value="t.id" x-text="(t.gender ? t.gender.charAt(0).toUpperCase() + t.gender.slice(1) + ' – ' : '') + t.name + ' (Slope ' + t.slope + ' · Rating ' + t.rating + ')'"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
@@ -319,7 +338,7 @@
|
||||
</div>
|
||||
<div class="confirm-row">
|
||||
<span class="confirm-label">Holes</span>
|
||||
<span class="confirm-value" x-text="holesCount ? holesCount + ' holes' : '—'"></span>
|
||||
<span class="confirm-value" x-text="holesModeLabel()"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -343,7 +362,7 @@
|
||||
<div style="font-size:0.8rem; color:var(--text-muted);">
|
||||
<span x-text="'HI\u00a0' + (getPlayer(uid).handicap != null ? getPlayer(uid).handicap : '—')"></span>
|
||||
<template x-if="getTee(uid)">
|
||||
<span x-text="' · ' + getTee(uid).name + ' (Slope\u00a0' + getTee(uid).slope + ' · Rating\u00a0' + getTee(uid).rating + ')'"></span>
|
||||
<span x-text="' · ' + (getTee(uid).gender ? getTee(uid).gender.charAt(0).toUpperCase() + getTee(uid).gender.slice(1) + ' – ' : '') + getTee(uid).name + ' (Slope\u00a0' + getTee(uid).slope + ' · Rating\u00a0' + getTee(uid).rating + ')'"></span>
|
||||
</template>
|
||||
<template x-if="getCourseHandicap(uid) != null">
|
||||
<span style="color:var(--accent); font-weight:600;" x-text="' · HC\u00a0' + getCourseHandicap(uid)"></span>
|
||||
@@ -367,6 +386,8 @@
|
||||
<template x-for="pid in selectedPlayers" :key="pid">
|
||||
<input type="hidden" name="players" :value="pid">
|
||||
</template>
|
||||
<input type="hidden" name="holes_count" :value="holesMode === '18' ? 18 : 9">
|
||||
<input type="hidden" name="start_hole" :value="holesMode === 'back9' ? 10 : 1">
|
||||
<input type="hidden" name="side_skins_enabled" :value="sideSkinsEnabled ? '1' : '0'">
|
||||
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user