GitHunt
  • CCW Simulator

** Recent Fixes (Latest Update: 8/19/25)
*** Timezone Bug Fix

  • Fixed date selection bug in show booking where selecting a date would shift to the previous day
  • Changed date creation from new Date(dateString) to explicit local timezone constructor new Date(year, month-1, day)
  • This prevents UTC conversion issues that caused date shifting in certain timezones

*** Title History Display Fix

  • Added missing renderTitleHistory function to View object
  • Added call to View.renderTitleHistory() in updateUI() function
  • Title history now properly displays when switching to Title History tab
  • Shows grouped title changes with week, date, previous champion, and new champion information

*** Date Loading Improvements

  • Fixed timezone issues in loadGameState and import functions
  • Improved date handling consistency across the application

** --- MODEL ---
*** Central state management
#+BEGIN_SRC javascript
const Model = {
// Core game state
roster: [],
titles: [],
venues: [],
budget: 12000,
currentWeek: 1,
currentDate: new Date(),
titleHistory: [],
showHistory: [],

// Show management
allShows: [],
selectedShowId: null,

// UI state
currentTab: 'dashboard',
sortColumn: null,
sortDirection: 'asc',
statManagementVisible: false,

// Wrestler records and stats
wrestlerRecords: new Map(),

// Show type to image mapping
showTypeImages: {
tnt: 'assets/shows/thurs.png',
ss: 'assets/shows/sat.png',
sns: 'assets/shows/sun.png',
house: 'assets/shows/house.png',
ppv: 'assets/shows/ppv.png'
}
};
#+END_SRC

** --- UPDATE ---
Pure functions that transform the model
#+BEGIN_SRC javascript
const Update = {
// Tab navigation
showTab: (model, tabId) => ({
...model,
currentTab: tabId
}),

// Wrestler management
updateWrestlerStats: (model, wrestlerId, updates) => {
const roster = model.roster.map(w =>
w.id === wrestlerId ? { ...w, ...updates } : w
);
return { ...model, roster };
},

// Show management
addShow: (model, show) => ({
...model,
allShows: [...model.allShows, show],
selectedShowId: show.id
}),

selectShow: (model, showId) => ({
...model,
selectedShowId: showId
}),

addMatchToShow: (model, showId, match) => {
const allShows = model.allShows.map(show =>
show.id === showId
? { ...show, matchCard: [...show.matchCard, match] }
: show
);
return { ...model, allShows };
},

removeMatchFromShow: (model, showId, matchIndex) => {
const allShows = model.allShows.map(show =>
show.id === showId
? { ...show, matchCard: show.matchCard.filter((_, i) => i !== matchIndex) }
: show
);
return { ...model, allShows };
},

// Sorting
sortRoster: (model, column) => {
const direction = model.sortColumn === column && model.sortDirection === 'asc' ? 'desc' : 'asc';
return { ...model, sortColumn: column, sortDirection: direction };
},

// UI state
toggleStatManagement: (model) => ({
...model,
statManagementVisible: !model.statManagementVisible
}),

// Game progression
advanceWeek: (model) => ({
...model,
currentWeek: model.currentWeek + 1
}),

updateBudget: (model, amount) => ({
...model,
budget: model.budget + amount
})
};
#+END_SRC

** --- VIEW ---
Pure functions that render the UI based on the model
#+BEGIN_SRC javascript
const View = {
// Main tab rendering
renderTab: (model, tabId) => {
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
const tabElement = document.getElementById(tabId);
if (tabElement) {
tabElement.classList.add('active');
}

if (tabId === 'history') {
  View.renderTitleHistory(model);
}

},

// Dashboard rendering
renderDashboard: (model) => {
View.updateCurrentWeek(model.currentWeek);
View.updateBudget(model.budget);
View.updateCurrentChampions(model);
},

// Roster table rendering
renderRosterTable: (model) => {
const tbody = document.getElementById('roster-table-body');
if (!tbody) return;

tbody.innerHTML = '';

// Sort roster if needed
const sortedRoster = [...model.roster];
if (model.sortColumn) {
  sortedRoster.sort((a, b) => {
    let aVal, bVal;
    switch (model.sortColumn) {
      case 'name':
        aVal = a.name.toLowerCase();
        bVal = b.name.toLowerCase();
        break;
      case 'type':
        aVal = (a.type || '').toLowerCase();
        bVal = (b.type || '').toLowerCase();
        break;
      case 'pop':
        aVal = a.pop || a.popularity || 0;
        bVal = b.pop || b.popularity || 0;
        break;
      case 'momentum':
        aVal = a.momentum || 0;
        bVal = b.momentum || 0;
        break;
      default:
        aVal = a[model.sortColumn];
        bVal = b[model.sortColumn];
    }

    if (typeof aVal === 'string' && typeof bVal === 'string') {
      return model.sortDirection === 'asc'
        ? aVal.localeCompare(bVal)
        : bVal.localeCompare(aVal);
    } else {
      return model.sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
    }
  });
}

sortedRoster.forEach(wrestler => {
  const row = document.createElement('tr');
  const record = wrestler.record || { wins: 0, losses: 0, draws: 0 };
  const momentumColor = wrestler.momentum > 0 ? '#4CAF50' : wrestler.momentum < 0 ? '#f44336' : '#F6AA29';

  row.innerHTML = `
    <td><img src="${wrestler.photo}" alt="${wrestler.name}" style="width: 75px; height: 70px;"></td>
    <td>${wrestler.name}</td>
    <td>${wrestler.type}</td>
    <td>${wrestler.pop || wrestler.popularity || 0}</td>
    <td>${wrestler.finisher || 'N/A'}</td>
    <td>${wrestler.alignment}</td>
    <td>${record.wins}-${record.losses}-${record.draws}</td>
    <td style="color: ${momentumColor};">${wrestler.momentum > 0 ? '+' : ''}${wrestler.momentum}</td>
    <td><button onclick="App.viewWrestler('${wrestler.id}')">View</button></td>
  `;
  tbody.appendChild(row);
});

View.updateSortIndicators(model);

},

// Show management rendering
renderBookedShowsTable: (model) => {
const tableDiv = document.getElementById('booked-shows-table');
if (!tableDiv) return;

if (model.allShows.length === 0) {
  tableDiv.innerHTML = '<em>No shows booked yet.</em>';
  return;
}

let html = '<table><thead><tr><th>Show</th><th>Date</th><th>Type</th><th>Venue</th><th>Status</th><th>Actions</th></tr></thead><tbody>';

model.allShows.forEach(show => {
  const showImg = model.showTypeImages[show.type]
    ? `<img src="${model.showTypeImages[show.type]}" alt="${show.type}" style="width:80px;height:70px;vertical-align:middle;margin-right:8px;">`
    : '';

  html += `<tr>
    <td>${showImg}</td>
    <td>${show.date.toLocaleDateString ? show.date.toLocaleDateString() : new Date(show.date).toLocaleDateString()}</td>
    <td>${App.getShowTypeName(show.type)}</td>
    <td>${show.venue}</td>
    <td>${show.status.charAt(0).toUpperCase() + show.status.slice(1)}</td>
    <td>
      <button onclick="App.manageShow('${show.id}')">Manage</button>
      ${show.status === 'booked' ? `<button onclick="App.simulateShow('${show.id}')">Simulate</button>` : ''}
    </td>
  </tr>`;
});

html += '</tbody></table>';
tableDiv.innerHTML = html;

},

// Utility view functions
updateCurrentWeek: (week) => {
const weekElement = document.getElementById('current-week');
if (weekElement) weekElement.textContent = week;
},

updateBudget: (budget) => {
const budgetElement = document.getElementById('budget-display');
if (budgetElement) {
budgetElement.textContent = 💰 Budget: $${budget.toLocaleString()};
}
},

updateCurrentChampions: (model) => {
const championsDiv = document.getElementById('current-champions');
if (!championsDiv) return;

championsDiv.innerHTML = '';

model.titles.forEach(title => {
  const holder = model.roster.find(w => w.id === title.holderId);
  if (holder) {
    const championDiv = document.createElement('div');
    championDiv.style.marginBottom = '10px';
    championDiv.innerHTML = `
      <img src="${title.image}" alt="${title.name}" style="width: 30px; height: 20px; vertical-align: middle; margin-right: 10px;">
      <strong>${title.name}:</strong> ${holder.name}
    `;
    championsDiv.appendChild(championDiv);
  }
});

},

updateSortIndicators: (model) => {
const columns = ['name', 'type', 'pop', 'momentum'];
columns.forEach(col => {
const el = document.getElementById('sort-indicator-' + col);
if (el) {
if (model.sortColumn === col) {
el.textContent = model.sortDirection === 'asc' ? '▲' : '▼';
} else {
el.textContent = '';
}
}
});
},

// Title history rendering
renderTitleHistory: (model) => {
const historyDiv = document.getElementById('title-history');
if (!historyDiv) return;

if (model.titleHistory.length === 0) {
  historyDiv.innerHTML = '<p><em>No title changes have occurred yet. Title history will appear here after championship matches are simulated.</em></p>';
  return;
}

// Group by title
const titleGroups = {};
model.titleHistory.forEach(entry => {
  if (!titleGroups[entry.titleId]) {
    titleGroups[entry.titleId] = [];
  }
  titleGroups[entry.titleId].push(entry);
});

let html = '';

// Sort titles by most recent change
Object.entries(titleGroups).forEach(([titleId, entries]) => {
  const title = model.titles.find(t => t.id === titleId);
  if (!title) return;
  
  // Sort entries by week (most recent first)
  entries.sort((a, b) => b.week - a.week);
  
  html += `<div class="title-history-section" style="margin-bottom: 30px; padding: 15px; border: 1px solid #ddd; border-radius: 8px;">`;
  html += `<h3 style="margin-top: 0; color: #333;">`;
  html += `<img src="${title.image}" alt="${title.name}" style="width: 30px; height: 20px; vertical-align: middle; margin-right: 10px;">`;
  html += `${title.name}</h3>`;
  
  html += '<table style="width: 100%; border-collapse: collapse;">';
  html += '<thead><tr style="background-color: #f5f5f5;">';
  html += '<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">Week</th>';
  html += '<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">Date</th>';
  html += '<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">Previous Champion</th>';
  html += '<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">New Champion</th>';
  html += '</tr></thead><tbody>';
  
  entries.forEach(entry => {
    html += '<tr>';
    html += `<td style="padding: 8px; border-bottom: 1px solid #eee;">${entry.week}</td>`;
    html += `<td style="padding: 8px; border-bottom: 1px solid #eee;">${entry.date}</td>`;
    html += `<td style="padding: 8px; border-bottom: 1px solid #eee;">${entry.oldHolder}</td>`;
    html += `<td style="padding: 8px; border-bottom: 1px solid #eee;"><strong>${entry.newHolder}</strong></td>`;
    html += '</tr>';
  });
  
  html += '</tbody></table>';
  html += '</div>';
});

historyDiv.innerHTML = html;

}
};
#+END_SRC

** --- CONTROLLER ---
Handles user interactions and coordinates updates
#+BEGIN_SRC javascript
const Controller = {
// Tab navigation
showTab: (tabId) => {
App.model = Update.showTab(App.model, tabId);
View.renderTab(App.model, tabId);

if (tabId === 'dashboard') {
  View.renderDashboard(App.model);
} else if (tabId === 'roster') {
  View.renderRosterTable(App.model);
} else if (tabId === 'book') {
  View.renderBookedShowsTable(App.model);
}

},

// Wrestler management
viewWrestler: (id) => {
const wrestler = App.model.roster.find(w => w.id === id);
if (!wrestler) return;

// Populate wrestler details modal
document.getElementById('detail-name').textContent = wrestler.name;
document.getElementById('detail-photo').src = wrestler.photo;
document.getElementById('detail-type').textContent = wrestler.type;
document.getElementById('detail-manager').textContent = wrestler.manager || 'N/A';
document.getElementById('detail-alignment').textContent = wrestler.alignment;
document.getElementById('detail-finisher').textContent = wrestler.finisher || 'N/A';
document.getElementById('detail-moveset').textContent = wrestler.moveset || 'Unknown';

const record = wrestler.record || { wins: 0, losses: 0, draws: 0 };
document.getElementById('detail-record').textContent = `${record.wins}-${record.losses}-${record.draws}`;

// Stats
const statsList = document.getElementById('detail-stats');
statsList.innerHTML = '';
Object.entries(wrestler.stats || {}).forEach(([key, value]) => {
  const li = document.createElement('li');
  li.textContent = `${key.toUpperCase()}: ${value}`;
  statsList.appendChild(li);
});

// Titles held
let heldTitles = [];
let heldTitleObjs = [];
if (App.model.titles && Array.isArray(App.model.titles)) {
  heldTitleObjs = App.model.titles.filter(t => t.holderId === wrestler.id);
  heldTitles = heldTitleObjs.map(t => t.name);
}
document.getElementById('detail-titles').textContent = heldTitles.length ? heldTitles.join(', ') : 'None';

// Belt images
const titleImagesDiv = document.getElementById('detail-title-images');
titleImagesDiv.innerHTML = '';
if (heldTitleObjs.length) {
  heldTitleObjs.forEach(t => {
    const img = document.createElement('img');
    img.src = t.image;
    img.alt = t.name + ' belt';
    img.title = t.name;
    img.style.width = '200px';
    img.style.height = 'auto';
    img.style.marginRight = '10px';
    img.style.verticalAlign = 'middle';
    titleImagesDiv.appendChild(img);
  });
}

document.getElementById('wrestler-details').style.display = 'block';

},

// Show management
bookShow: () => {
const showType = document.getElementById('show-type-select').value;
const dateValue = document.getElementById('show-date-picker').value;
// Fix timezone issue: create date in local timezone to avoid date shifting
const [year, month, day] = dateValue.split('-').map(Number);
const showDate = new Date(year, month - 1, day); // month is 0-indexed in Date constructor
const venue = document.getElementById('venue-select').value;

if (isNaN(showDate)) {
  alert('Please select a valid date for the show.');
  return;
}

const show = {
  id: 'show_' + Date.now(),
  type: showType,
  date: showDate,
  venue,
  matchCard: [],
  status: 'booked'
};

App.model = Update.addShow(App.model, show);
View.renderBookedShowsTable(App.model);
App.showMatchManagement(show);
App.saveGameState();

},

// Roster sorting
sortRoster: (column) => {
App.model = Update.sortRoster(App.model, column);
View.renderRosterTable(App.model);
}
};
#+END_SRC

** --- MAIN APP ---
Coordinates everything and provides the public API
#+BEGIN_SRC javascript
const App = {
model: { ...Model },

// Public API functions (called from HTML)
showTab: Controller.showTab,
viewWrestler: Controller.viewWrestler,
bookSelectedShow: Controller.bookShow,
sortRosterTable: Controller.sortRoster,

// Show management
manageShow: (showId) => {
App.model = Update.selectShow(App.model, showId);
const show = App.model.allShows.find(s => s.id === showId);
if (show) App.showMatchManagement(show);
},

showMatchManagement: (show) => {
document.getElementById('show-match-management').style.display = '';
document.getElementById('show-match-title').textContent = App.getShowTypeName(show.type);
document.getElementById('show-match-date').textContent = show.date.toLocaleDateString ? show.date.toLocaleDateString() : new Date(show.date).toLocaleDateString();

if (show.status === 'completed') {
  App.showMatchResultsUI(show);
} else {
  App.renderShowMatchList(show);
  App.renderAddMatchForm(show);
}

},

renderShowMatchList: (show) => {
const listDiv = document.getElementById('show-match-list');
if (!listDiv) return;

let html = '';
if (show.matchCard.length === 0) {
  html = '<em>No matches booked yet.</em>';
} else {
  html = '<ul>' + show.matchCard.map((m, i) => {
    const a = App.model.roster.find(w => w.id === m.wrestlerA)?.name || '?';
    const b = App.model.roster.find(w => w.id === m.wrestlerB)?.name || '?';
    const t = App.model.titles.find(t => t.id === m.titleId)?.name || '';
    return `<li>${a} vs. ${b} (${m.stipulation}${t ? ', Title: ' + t : ''}) <button onclick='App.removeMatchFromShow(${i})' title='Remove' style='color:red;font-weight:bold;'>🗑️</button></li>`;
  }).join('') + '</ul>';
}
listDiv.innerHTML = html;

},

renderAddMatchForm: (show) => {
const listDiv = document.getElementById('show-match-list');
if (!listDiv) return;

let html = '';
if (show.matchCard.length === 0) {
  html = '<em>No matches booked yet.</em>';
} else {
  html = '<ul>' + show.matchCard.map((m, i) => {
    const a = App.model.roster.find(w => w.id === m.wrestlerA)?.name || '?';
    const b = App.model.roster.find(w => w.id === m.wrestlerB)?.name || '?';
    const t = App.model.titles.find(t => t.id === m.titleId)?.name || '';
    return `<li>${a} vs. ${b} (${m.stipulation}${t ? ', Title: ' + t : ''}) <button onclick='App.removeMatchFromShow(${i})' title='Remove' style='color:red;font-weight:bold;'>🗑️</button></li>`;
  }).join('') + '</ul>';
}

html += `<div id='add-match-form' style='margin-top:10px;'>
  <label>Wrestler A: <select id='add-match-a'></select></label>
  <label>Wrestler B: <select id='add-match-b'></select></label>
  <label>Stipulation: <select id='add-match-stip'>
    <option value='Standard'>Standard</option>
    <option value='No DQ'>No DQ</option>
    <option value='Tag Team'>Tag Team</option>
  </select></label>
  <label>Title Match: <select id='add-match-title'><option value=''>None</option></select></label>
</div>`;

html += `<div class="show-match-actions" style="margin-top:16px; display:flex; gap:12px;">
  <button onclick="App.confirmAddMatchToShow()">Add Match</button>
  <button onclick="App.simulateShow(App.model.selectedShowId)">Simulate Show</button>
  <button onclick="App.closeMatchManagement()">Close</button>
</div>`;

listDiv.innerHTML = html;

// Populate dropdowns
App.populateMatchFormDropdowns();

},

populateMatchFormDropdowns: () => {
const aSel = document.getElementById('add-match-a');
const bSel = document.getElementById('add-match-b');
const titleSel = document.getElementById('add-match-title');

if (!aSel || !bSel || !titleSel) return;

aSel.innerHTML = '';
bSel.innerHTML = '';
titleSel.innerHTML = '<option value="">None</option>';

App.model.roster.forEach(w => {
  aSel.add(new Option(w.name, w.id));
  bSel.add(new Option(w.name, w.id));
});

App.model.titles.forEach(t => {
  titleSel.add(new Option(t.name, t.id));
});

},

confirmAddMatchToShow: () => {
if (!App.model.selectedShowId) return;

const wrestlerA = document.getElementById('add-match-a').value;
const wrestlerB = document.getElementById('add-match-b').value;
const stip = document.getElementById('add-match-stip').value;
const titleId = document.getElementById('add-match-title').value;

if (!wrestlerA || !wrestlerB || wrestlerA === wrestlerB) {
  alert("Please select two different wrestlers.");
  return;
}

const match = { wrestlerA, wrestlerB, stipulation: stip, titleId };
App.model = Update.addMatchToShow(App.model, App.model.selectedShowId, match);

const show = App.model.allShows.find(s => s.id === App.model.selectedShowId);
if (show) {
  App.renderAddMatchForm(show);
}
App.saveGameState();

},

removeMatchFromShow: (matchIdx) => {
if (!App.model.selectedShowId) return;

App.model = Update.removeMatchFromShow(App.model, App.model.selectedShowId, matchIdx);
const show = App.model.allShows.find(s => s.id === App.model.selectedShowId);
if (show) {
  App.renderAddMatchForm(show);
}
App.saveGameState();

},

closeMatchManagement: () => {
document.getElementById('show-match-management').style.display = 'none';
App.model = Update.selectShow(App.model, null);
},

// Show simulation system
simulateShow: (showId) => {
const show = App.model.allShows.find(s => s.id === showId);
if (!show || !show.matchCard || show.matchCard.length === 0) {
alert('No matches booked for this show!');
return;
}
if (show.status === 'completed') {
alert('This show has already been simulated.');
return;
}

// Start simulation
App.runShowSimulation(show);

},

runShowSimulation: (show) => {
let index = 0;
let totalQuality = 0;
let showResults = [];

// Clear and initialize match list for live results
const listDiv = document.getElementById('show-match-list');
if (listDiv) {
  listDiv.innerHTML = '<ul id="live-match-results"></ul>';
}

function runNextMatch() {
  if (index >= show.matchCard.length) {
    App.finishShowSimulation(show, showResults, totalQuality);
    return;
  }

  const match = show.matchCard[index];
  const wrestlerA = App.model.roster.find(w => w.id === match.wrestlerA);
  const wrestlerB = App.model.roster.find(w => w.id === match.wrestlerB);

  if (!wrestlerA || !wrestlerB) {
    index++;
    runNextMatch();
    return;
  }

  const result = App.simulateMatch(wrestlerA, wrestlerB, match.stipulation);
  totalQuality += result.quality;

  // Update records
  App.updateWrestlerRecord(result.winner, 'win');
  App.updateWrestlerRecord(result.loser, 'loss');

  // Handle title changes
  let titleChange = null;
  if (match.titleId) {
    const title = App.model.titles.find(t => t.id === match.titleId);
    if (title && title.holderId !== result.winner.id) {
      const oldHolder = App.model.roster.find(w => w.id === title.holderId);
      title.holderId = result.winner.id;
      App.addTitleHistoryEntry(match.titleId, oldHolder, result.winner, App.model.currentWeek);
      titleChange = title;
    }
  }

  showResults.push({
    match: match,
    result: result,
    titleChange: titleChange
  });

  // Update live results UI
  const liveUl = document.getElementById('live-match-results');
  if (liveUl) {
    const li = document.createElement('li');
    const winnerImg = result.winner.photo ? `<img src='${result.winner.photo}' alt='${result.winner.name}' style='width:40px;height:40px;object-fit:cover;vertical-align:middle;margin-right:8px;border-radius:6px;'>` : '';
    const beltImg = titleChange && titleChange.image ? `<img src='${titleChange.image}' alt='${titleChange.name}' style='width:40px;height:24px;vertical-align:middle;margin-left:8px;'>` : '';

    li.innerHTML = `${winnerImg}<strong>${wrestlerA.name} vs. ${wrestlerB.name} (${match.stipulation})</strong><br>
      <span style="color: #F6AA29;">Winner: ${result.winner.name} via ${result.method}</span> ${beltImg}<br>
      <span style="color: #CD3E23;">Match Rating: ${result.quality}/100</span>
      ${titleChange ? `<br><span style='color: #ffdd57;'>🏆 NEW ${titleChange.name} CHAMPION!</span>` : ''}`;

    liveUl.appendChild(li);
  }

  index++;
  setTimeout(runNextMatch, Math.random() * 1000 + 800);
}

runNextMatch();

},

finishShowSimulation: (show, showResults, totalQuality) => {
const avgQuality = Math.round(totalQuality / showResults.length);
const profit = App.calculateShowProfit(avgQuality, showResults.length);

// Update model
App.model = Update.updateBudget(App.model, profit);
App.model = Update.advanceWeek(App.model);

// Update show status
const allShows = App.model.allShows.map(s =>
  s.id === show.id ? { ...s, status: 'completed' } : s
);
App.model = { ...App.model, allShows };

// Save show to history
App.model.showHistory.push({
  week: App.model.currentWeek - 1,
  matches: showResults,
  quality: avgQuality,
  profit: profit,
  date: new Date().toLocaleDateString()
});

// Update UI
View.updateCurrentWeek(App.model.currentWeek);
View.updateBudget(App.model.budget);
View.updateCurrentChampions(App.model);
View.renderRosterTable(App.model);

// Show results summary
document.getElementById("show-summary").innerHTML = `
  <div class="card">
    <h3>Show Results</h3>
    <p>📊 Average Match Rating: ${avgQuality}/100</p>
    <p>💰 ${profit >= 0 ? "Earned" : "Lost"}: $${Math.abs(profit).toLocaleString()}</p>
    <p>💰 New Budget: $${App.model.budget.toLocaleString()}</p>
    <p>📅 Week: ${App.model.currentWeek}</p>
  </div>
`;

App.saveGameState();

},

simulateMatch: (wrestlerA, wrestlerB, stipulation) => {
// Calculate match rating based on wrestler stats
const aOverall = App.calculateOverall(wrestlerA);
const bOverall = App.calculateOverall(wrestlerB);

// Add momentum bonus
const aMomentumBonus = wrestlerA.momentum * 0.1;
const bMomentumBonus = wrestlerB.momentum * 0.1;

// Add popularity bonus
const aPopBonus = wrestlerA.pop * 0.05;
const bPopBonus = wrestlerB.pop * 0.05;

// Add stipulation effects
const stipBonus = App.getStipulationBonus(stipulation, wrestlerA, wrestlerB);

const aFinal = aOverall + aMomentumBonus + aPopBonus + stipBonus.a;
const bFinal = bOverall + bMomentumBonus + bPopBonus + stipBonus.b;

// Add some randomness
const aRandom = (Math.random() - 0.5) * 20;
const bRandom = (Math.random() - 0.5) * 20;

const aTotal = aFinal + aRandom;
const bTotal = bFinal + bRandom;

// Determine winner
let winner, loser;
if (aTotal > bTotal) {
  winner = wrestlerA;
  loser = wrestlerB;
} else {
  winner = wrestlerB;
  loser = wrestlerA;
}

// Calculate match quality
const matchQuality = Math.min(100, Math.max(0, (aOverall + bOverall) / 2 + Math.random() * 20));

return {
  winner: winner,
  loser: loser,
  quality: Math.round(matchQuality),
  method: App.determineFinishMethod(winner, loser, stipulation)
};

},

calculateOverall: (wrestler) => {
const stats = wrestler.stats;
return (stats.strength + stats.speed + stats.stamina + stats.charisma + stats.technical) / 5;
},

getStipulationBonus: (stipulation, wrestlerA, wrestlerB) => {
switch (stipulation) {
case 'No DQ':
// Heels get bonus in No DQ matches
return {
a: wrestlerA.alignment === 'Heel' ? 10 : 0,
b: wrestlerB.alignment === 'Heel' ? 10 : 0
};
case 'Tag Team':
// Tag team specialists get bonus
return {
a: wrestlerA.type === 'Tag Team' ? 15 : 0,
b: wrestlerB.type === 'Tag Team' ? 15 : 0
};
default:
return { a: 0, b: 0 };
}
},

determineFinishMethod: (winner, loser, stipulation) => {
const methods = [
${winner.finisher},
'Pinfall',
'Submission',
'Countout',
'Disqualification'
];

if (stipulation === 'No DQ') {
  methods.splice(3, 2); // Remove countout and DQ
}

// Finisher has higher chance
const weights = [0.4, 0.3, 0.2, 0.05, 0.05];
const random = Math.random();
let cumulative = 0;

for (let i = 0; i < weights.length; i++) {
  cumulative += weights[i];
  if (random <= cumulative) {
    return methods[i];
  }
}

return methods[0];

},

updateWrestlerRecord: (wrestler, result) => {
if (!wrestler.record) {
wrestler.record = { wins: 0, losses: 0, draws: 0 };
}

switch (result) {
  case 'win':
    wrestler.record.wins++;
    wrestler.momentum = Math.min(10, wrestler.momentum + 1);
    break;
  case 'loss':
    wrestler.record.losses++;
    wrestler.momentum = Math.max(-10, wrestler.momentum - 1);
    break;
  case 'draw':
    wrestler.record.draws++;
    break;
}

},

calculateShowProfit: (avgQuality, matchCount) => {
const baseProfit = (avgQuality - 50) * 100; // Quality affects profit
const matchBonus = matchCount * 500; // More matches = more profit potential
const randomFactor = (Math.random() - 0.5) * 2000; // Some randomness

return Math.floor(baseProfit + matchBonus + randomFactor);

},

addTitleHistoryEntry: (titleId, oldHolder, newHolder, week) => {
const title = App.model.titles.find(t => t.id === titleId);
if (!title) return;

App.model.titleHistory.push({
  titleId: titleId,
  titleName: title.name,
  oldHolder: oldHolder ? oldHolder.name : 'Vacant',
  newHolder: newHolder.name,
  week: week,
  date: new Date().toLocaleDateString()
});

},

showMatchResultsUI: (show) => {
// Show completed show results
const listDiv = document.getElementById('show-match-list');
if (!listDiv) return;

// Find the show in history
const showResult = App.model.showHistory.find(h => h.week === show.week);
if (!showResult) {
  listDiv.innerHTML = '<em>No results found for this show.</em>';
  return;
}

let html = '<h4>Show Results</h4>';
html += `<p><strong>Average Rating:</strong> ${showResult.quality}/100</p>`;
html += `<p><strong>Profit:</strong> $${showResult.profit.toLocaleString()}</p>`;
html += '<h5>Match Results:</h5><ul>';

showResult.matches.forEach(match => {
  const a = App.model.roster.find(w => w.id === match.match.wrestlerA)?.name || '?';
  const b = App.model.roster.find(w => w.id === match.match.wrestlerB)?.name || '?';
  const winner = match.result.winner.name;
  const method = match.result.method;
  const rating = match.result.quality;

  html += `<li><strong>${a} vs. ${b}</strong><br>`;
  html += `Winner: ${winner} via ${method}<br>`;
  html += `Rating: ${rating}/100`;

  if (match.titleChange) {
    html += `<br><span style='color: #ffdd57;'>🏆 ${match.titleChange.name} Title Change!</span>`;
  }

  html += '</li>';
});

html += '</ul>';
listDiv.innerHTML = html;

},

// Utility functions
getShowTypeName: (type) => {
const names = {
'tnt': 'Thursday Night Throwdown (TNT)',
'ss': 'Saturday Slamfest (SS)',
'sns': 'Sunday Night Stampede (SNS)',
'house': 'House Show',
'ppv': 'PPV'
};
return names[type] || type;
},

// Game state management
saveGameState: () => {
const gameState = {
roster: App.model.roster,
titles: App.model.titles,
budget: App.model.budget,
currentWeek: App.model.currentWeek,
currentDate: App.model.currentDate.toISOString(),
titleHistory: App.model.titleHistory,
showHistory: App.model.showHistory,
allShows: App.model.allShows,
selectedShowId: App.model.selectedShowId
};

localStorage.setItem('ccwGameState', JSON.stringify(gameState));
alert('Game saved successfully!');

},

loadGameState: () => {
const saved = localStorage.getItem('ccwGameState');
if (!saved) {
alert('No saved game found.');
return;
}

try {
  const gameState = JSON.parse(saved);
  App.model = {
    ...App.model,
    roster: gameState.roster || [],
    titles: gameState.titles || [],
    budget: gameState.budget || 12000,
    currentWeek: gameState.currentWeek || 1,
    currentDate: gameState.currentDate ? new Date(gameState.currentDate) : new Date(1978, 0, 1), // month is 0-indexed
    titleHistory: gameState.titleHistory || [],
    showHistory: gameState.showHistory || [],
    allShows: gameState.allShows || [],
    selectedShowId: gameState.selectedShowId || null
  };

  App.initializeWrestlerRecords();
  App.updateUI();
  alert('Game loaded successfully!');
} catch (e) {
  console.error('Failed to load game state:', e);
  alert('Failed to load game state. The save file may be corrupted.');
}

},

exportGameState: () => {
const gameState = {
roster: App.model.roster,
titles: App.model.titles,
budget: App.model.budget,
currentWeek: App.model.currentWeek,
currentDate: App.model.currentDate.toISOString(),
titleHistory: App.model.titleHistory,
showHistory: App.model.showHistory,
allShows: App.model.allShows,
selectedShowId: App.model.selectedShowId
};

const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(gameState, null, 2));
const dlAnchor = document.createElement('a');
dlAnchor.setAttribute('href', dataStr);
dlAnchor.setAttribute('download', `ccw_save_${new Date().toISOString().split('T')[0]}.json`);
document.body.appendChild(dlAnchor);
dlAnchor.click();
document.body.removeChild(dlAnchor);

},

// Initialization
initializeWrestlerRecords: () => {
App.model.roster.forEach(wrestler => {
if (!wrestler.record) {
wrestler.record = { wins: 0, losses: 0, draws: 0 };
}
if (typeof wrestler.momentum !== 'number') {
wrestler.momentum = 0;
}
if (typeof wrestler.pop !== 'number') {
wrestler.pop = 50;
}
if (typeof wrestler.heat !== 'number') {
wrestler.heat = 50;
}
if (typeof wrestler.popularity !== 'number') {
wrestler.popularity = 50;
}
if (typeof wrestler.earnings !== 'number') {
wrestler.earnings = 0;
}
if (typeof wrestler.draw_rating !== 'number') {
wrestler.draw_rating = 50;
}
if (!wrestler.injury) {
wrestler.injury = 'active';
wrestler.injury_weeks = 0;
}
});
},

updateUI: () => {
View.renderDashboard(App.model);
View.renderRosterTable(App.model);
View.renderBookedShowsTable(App.model);
View.renderTitleHistory(App.model);
App.renderEditableRosterTable();
},

// Data loading
loadData: async () => {
try {
// Load roster
const rosterResponse = await fetch('data/roster.json');
App.model.roster = await rosterResponse.json();

  // Load titles
  const titlesResponse = await fetch('data/titles.json');
  App.model.titles = await titlesResponse.json();

  // Load venues
  const venuesResponse = await fetch('data/venues.json');
  App.model.venues = await venuesResponse.json();

  // Initialize
  App.initializeWrestlerRecords();
  App.populateVenueSelect();
  App.updateUI();

} catch (error) {
  console.error('Failed to load data:', error);
}

},

populateVenueSelect: () => {
const select = document.getElementById('venue-select');
if (!select) return;

select.innerHTML = '';
App.model.venues.forEach(venue => {
  const opt = new Option(venue.name, venue.name);
  select.add(opt);
});

if (App.model.venues.length > 0) {
  App.updateVenueInfo(App.model.venues[0].name);
  select.value = App.model.venues[0].name;
}

select.onchange = function() {
  App.updateVenueInfo(this.value);
};

},

updateVenueInfo: (venueName) => {
const venue = App.model.venues.find(v => v.name === venueName);
const infoDiv = document.getElementById('venue-info');
if (!venue || !infoDiv) {
if (infoDiv) infoDiv.innerHTML = '';
return;
}

infoDiv.innerHTML = `
  <div style="display:flex;align-items:flex-start;gap:18px;">
    <img src="${venue.image}" alt="${venue.name}" style="width:500px;height:250px;object-fit:cover;border-radius:8px;border:2px solid #F6AA29;box-shadow:0 2px 12px #053452;">
    <div>
      <h4 style='margin:0 0 8px 0;'>${venue.name}</h4>
      <div><strong>City:</strong> ${venue.city}, ${venue.state}</div>
      <div><strong>Region:</strong> ${venue.region}</div>
      <div><strong>Capacity:</strong> ${venue.capacity.toLocaleString()}</div>
      <div><strong>Prestige:</strong> ${venue.prestige}</div>
      <div><strong>Cost:</strong> $${venue.cost.toLocaleString()}</div>
      <div><strong>Sponsor:</strong> ${venue.sponsors || 'N/A'}</div>
      <div><strong>Unlock Year:</strong> ${venue.unlockYear || 'N/A'}</div>
    </div>
  </div>
`;

}
};
#+END_SRC
** --- INITIALIZATION ---
#+BEGIN_SRC javascript
document.addEventListener('DOMContentLoaded', () => {
// Set up import functionality
const importInput = document.getElementById('import-game-state');
if (importInput) {
importInput.onchange = (e) => {
if (e.target.files && e.target.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
try {
const gameState = JSON.parse(e.target.result);
App.model = {
...App.model,
roster: gameState.roster || [],
titles: gameState.titles || [],
budget: gameState.budget || 12000,
currentWeek: gameState.currentWeek || 1,
currentDate: gameState.currentDate ? new Date(gameState.currentDate) : new Date(1978, 0, 1), // month is 0-indexed
titleHistory: gameState.titleHistory || [],
showHistory: gameState.showHistory || [],
allShows: gameState.allShows || [],
selectedShowId: gameState.selectedShowId || null
};

        App.initializeWrestlerRecords();
        App.updateUI();
        alert('Game imported successfully!');
      } catch (err) {
        alert('Failed to import game: ' + err);
      }
    };
    reader.readAsText(e.target.files[0]);
  }
};

}

// Load data and initialize
App.loadData();

// Hide show management initially
document.getElementById('show-match-management').style.display = 'none';
});
#+END_SRC

** --- UTILITY FUNCTIONS ---
These are pure utility functions that don't depend on the model
#+BEGIN_SRC javascript
const Utils = {
clamp: (val, min, max) => Math.max(min, Math.min(max, val)),
randInt: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
};

// Make App globally available
window.App = App;

// Add the editable roster table rendering function to App
App.renderEditableRosterTable = function() {
const container = document.getElementById('editable-roster-table');
if (!container) return;

let html = '

' +
'' +
'';

App.model.roster.forEach((w, i) => {
html += <tr> <td>${w.name}</td> <td><input type="number" min="0" max="100" value="${w.pop ?? ''}" data-field="pop" data-idx="${i}" style="width:60px"></td> <td><input type="number" min="0" max="100" value="${w.heat ?? ''}" data-field="heat" data-idx="${i}" style="width:60px"></td> <td><input type="number" min="-10" max="10" value="${w.momentum ?? 0}" data-field="momentum" data-idx="${i}" style="width:60px"></td> <td><input type="number" min="0" max="100" value="${w.popularity ?? ''}" data-field="popularity" data-idx="${i}" style="width:60px"></td> <td><input type="number" min="0" value="${w.earnings ?? 0}" data-field="earnings" data-idx="${i}" style="width:80px"></td> <td><input type="number" min="0" max="100" value="${w.draw_rating ?? ''}" data-field="draw_rating" data-idx="${i}" style="width:60px"></td> <td><input type="text" value="${w.injury ?? ''}" data-field="injury" data-idx="${i}" style="width:100px"></td> </tr>;
});

html += '

NamePopHeatMomentumPopularityEarningsDraw RatingInjury
';
container.innerHTML = html;
};

// Add stat management functions to App
App.simulateStatUpdate = function() {
App.model.roster.forEach(w => {
// Pop, Heat, Popularity: -3 to +3
w.pop = Utils.clamp((w.pop ?? 0) + Utils.randInt(-3, 3), 0, 100);
w.heat = Utils.clamp((w.heat ?? 0) + Utils.randInt(-3, 3), 0, 100);
w.popularity = Utils.clamp((w.popularity ?? 0) + Utils.randInt(-2, 2), 0, 100);

// Momentum: -1 to +1
w.momentum = Utils.clamp((w.momentum ?? 0) + Utils.randInt(-1, 1), -10, 10);

// Earnings: +$500 to +$2500 based on draw_rating
let draw = w.draw_rating ?? Math.round(((w.popularity ?? 0) + (w.pop ?? 0) + (w.heat ?? 0)) / 3);
w.draw_rating = draw;
let earning = 500 + Math.round(draw * Math.random() * 20);
w.earnings = (w.earnings ?? 0) + earning;

// Injury: 5% chance
if (!w.injury || w.injury === '' || w.injury === 'active') {
  if (Math.random() < 0.05) {
    let weeks = Utils.randInt(1, 8);
    w.injury = `injured (${weeks}w)`;
    w.injury_weeks = weeks;
  } else {
    w.injury = 'active';
    w.injury_weeks = 0;
  }
} else if (w.injury.startsWith('injured')) {
  // Decrement injury weeks
  w.injury_weeks = (w.injury_weeks ?? 1) - 1;
  if (w.injury_weeks <= 0) {
    w.injury = 'active';
    w.injury_weeks = 0;
  } else {
    w.injury = `injured (${w.injury_weeks}w)`;
  }
}

});

// Update UI and save
View.renderRosterTable(App.model);
App.renderEditableRosterTable();
App.saveGameState();
alert('Stats updated!');
};

App.saveManualChanges = function() {
const inputs = document.querySelectorAll('#editable-roster-table input');
inputs.forEach(input => {
const idx = parseInt(input.dataset.idx);
const field = input.dataset.field;
let value = input.value;

if (["pop","heat","momentum","popularity","earnings","draw_rating"].includes(field)) {
  value = Number(value);
}

App.model.roster[idx][field] = value;

if (field === 'injury' && value === 'active') {
  App.model.roster[idx].injury_weeks = 0;
}

});

// Update UI and save
View.renderRosterTable(App.model);
App.renderEditableRosterTable();
App.saveGameState();
alert('Manual changes saved!');
};

App.exportRoster = function() {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(App.model.roster, null, 2));
const dlAnchor = document.createElement('a');
dlAnchor.setAttribute('href', dataStr);
dlAnchor.setAttribute('download', 'ccw_roster_export.json');
document.body.appendChild(dlAnchor);
dlAnchor.click();
document.body.removeChild(dlAnchor);
};

App.importRoster = function() {
const importInput = document.getElementById('import-roster');
if (importInput) {
importInput.click();
}
};

// Toggle stat management visibility
App.toggleStatManagement = function() {
const statCard = document.getElementById('stat-management-card');
const toggle = document.getElementById('toggle-stat-management');

if (statCard && toggle) {
if (statCard.style.display === 'none' || statCard.style.display === '') {
statCard.style.display = 'block';
toggle.textContent = 'Hide Stat Management & Manual Override';
} else {
statCard.style.display = 'none';
toggle.textContent = 'Show Stat Management & Manual Override';
}
}
};

// Initialize stat management toggle
document.addEventListener('DOMContentLoaded', () => {
const statToggle = document.getElementById('toggle-stat-management');
const statCard = document.getElementById('stat-management-card');

if (statToggle && statCard) {
statToggle.onclick = App.toggleStatManagement;
// Start hidden
statCard.style.display = 'none';
statToggle.textContent = 'Show Stat Management & Manual Override';
}

// Set up roster import functionality
const importRosterInput = document.getElementById('import-roster');
if (importRosterInput) {
importRosterInput.onchange = (e) => {
if (e.target.files && e.target.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = JSON.parse(e.target.result);
if (Array.isArray(data)) {
App.model.roster = data;
App.initializeWrestlerRecords();
App.renderEditableRosterTable();
View.renderRosterTable(App.model);
App.saveGameState();
alert('Roster imported successfully!');
} else {
alert('Invalid roster file.');
}
} catch (err) {
alert('Failed to import roster: ' + err);
}
};
reader.readAsText(e.target.files[0]);
}
};
}
});
#+END_SRC

Languages

JavaScript81.2%HTML10.2%CSS8.6%

Contributors

GNU General Public License v3.0
Created July 28, 2025
Updated August 20, 2025
mistersaturn/ccwsim | GitHunt