Apps Home
|
Create an App
TimerDVS
Author:
dvspapi
Description
Source Code
Launch App
Current Users
Created by:
Dvspapi
/* * ============================================================ * BALLOON POP TIMER — Chaturbate Server-Side App * ============================================================ * * Countdown timer that overlays your stream as a big circular * alarm-clock dial. Viewers add time by tipping tokens. * * TOKEN RULES * ----------- * - Tips under 40 tokens: every 10 tokens = +60 seconds * - Tips 40 tokens or more: every 10 tokens = +90 seconds * (so 40 tokens = +360s, 50 = +450s, 100 = +900s, etc.) * - Tips under 10 tokens add nothing. * * MARKERS (posted to chat, invisible to viewers) * ----------------------------------------------- * ||BT|launch|300| <- app launched, 300s (5min) * ||BT|set|240| <- broadcaster set 4min * ||BT|tick|217| <- 1-second tick (rare; only if needed) * ||BT|add|60|user| <- 60s added by user * ||BT|expire|| <- timer hit 0, cam hidden * ||BT|reset|300| <- broadcaster reset to 300s * * INSTALL * ------- * Paste into https://chaturbate.com/apps/upload_app/?slot=0 * Pair with: balloon-pop-timer.html (OBS Browser Source). * ============================================================ */ var MARKER_PREFIX = "||BT|"; var SECONDS_PER_TOKEN_TIER1 = 6; // under 40 tokens: 10 tokens -> 60s -> 6s per token var SECONDS_PER_TOKEN_TIER2 = 9; // 40+ tokens: 10 tokens -> 90s -> 9s per token var TIER2_THRESHOLD = 40; var MIN_TIP_TO_ADD = 10; var TICK_MS = 1000; // ---------- broadcaster settings ---------- cb.settings_choices = [ { name: 'initial_minutes', type: 'int', minValue: 1, maxValue: 120, defaultValue: 5, label: "Initial countdown time (minutes)" }, { name: 'title_text', type: 'str', minLength: 1, maxLength: 100, defaultValue: "I'LL SMILE FOR YOU :)", label: "Text shown at top of the timer card" }, { name: 'subject_prefix', type: 'str', minLength: 1, maxLength: 100, defaultValue: "STAY CLEAR!", label: "Text shown in room subject when cam is unobstructed" }, { name: 'expire_subject', type: 'str', minLength: 1, maxLength: 100, defaultValue: "TIMES UP!", label: "Text shown when timer hits 0" }, { name: 'show_leaderboard', type: 'str', minLength: 1, maxLength: 5, defaultValue: "yes", label: "Show top-tipper leaderboard in panel (yes/no)" }, { name: 'show_remaining', type: 'str', minLength: 1, maxLength: 5, defaultValue: "yes", label: "Show big circular timer overlay (yes/no)" } ]; // ---------- state ---------- var state = { remaining: 0, // seconds left running: false, lastTipper: "", lastAddAt: 0, totalSecondsAdded: 0, totalTokens: 0, leaderboard: {}, // user -> total seconds added timerHandle: null }; // ---------- helpers ---------- function postMarker(payload) { cb.sendNotice(MARKER_PREFIX + payload + "||", "", "#000000", "#000000", "small", ""); } function publicAnnounce(text) { cb.sendNotice(text, "", "#222222", "#FFFFFF", "bold"); } function formatMMSS(secs) { secs = Math.max(0, parseInt(secs) || 0); var m = Math.floor(secs / 60); var s = secs % 60; var mm = ("00" + m).slice(-2); var ss = ("00" + s).slice(-2); return mm + ":" + ss; } function updateSubject() { if (state.running) { cb.changeRoomSubject(cb.settings.subject_prefix + " | " + formatMMSS(state.remaining) + " remaining"); } else { cb.changeRoomSubject(cb.settings.expire_subject + " | Tip to add time"); } } // ---------- timer tick ---------- function tick() { state.remaining -= 1; if (state.remaining <= 0) { state.remaining = 0; expireTimer(); return; } // update subject every 5s, draw panel every 15s if (state.remaining % 5 === 0) updateSubject(); if (state.remaining % 15 === 0) cb.drawPanel(); state.timerHandle = cb.setTimeout(tick, TICK_MS); } function startTimer(initialSeconds) { if (state.timerHandle) { cb.cancelTimeout(state.timerHandle); state.timerHandle = null; } state.remaining = parseInt(initialSeconds) || 0; state.running = state.remaining > 0; updateSubject(); cb.drawPanel(); if (state.running) { state.timerHandle = cb.setTimeout(tick, TICK_MS); } } function expireTimer() { state.running = false; if (state.timerHandle) { cb.cancelTimeout(state.timerHandle); state.timerHandle = null; } postMarker("expire|"); publicAnnounce("TIME'S UP! Cam is now hidden. Tip to add time and bring it back!"); updateSubject(); cb.drawPanel(); // hide cam: limitCam_start requires a reason string in CB API try { cb.limitCam_start("Times up! Tip to add time."); } catch (e) { cb.log("[timer] limitCam_start failed: " + e); } } function resetTimer() { var secs = parseInt(cb.settings.initial_minutes) * 60; state.remaining = secs; state.running = true; postMarker("reset|" + secs); publicAnnounce("Timer reset to " + formatMMSS(secs) + "!"); // unhide cam try { cb.limitCam_stop(); } catch (e) { cb.log("[timer] limitCam_stop failed: " + e); } updateSubject(); cb.drawPanel(); if (state.timerHandle) { cb.cancelTimeout(state.timerHandle); state.timerHandle = null; } state.timerHandle = cb.setTimeout(tick, TICK_MS); } // ---------- tip handler ---------- cb.onTip(function (tip) { var amount = parseInt(tip['amount']); if (!amount || amount < MIN_TIP_TO_ADD) return; var perToken = (amount >= TIER2_THRESHOLD) ? SECONDS_PER_TOKEN_TIER2 : SECONDS_PER_TOKEN_TIER1; var add = Math.floor(amount / 10) * perToken; if (add <= 0) return; var user = tip['from_user'] || "anonymous"; var now = Date.now(); var canAnnounce = (now - state.lastAddAt > 2000) || (state.lastTipper !== user); // if timer was at 0 / cam hidden, restart it if (!state.running) { try { cb.limitCam_stop(); } catch (e) {} state.running = true; publicAnnounce("Cam back online — " + user + " added " + add + " seconds!"); } state.remaining += add; state.totalSecondsAdded += add; state.totalTokens += amount; state.lastTipper = user; state.lastAddAt = now; state.leaderboard[user] = (state.leaderboard[user] || 0) + add; postMarker("add|" + add + "|" + user + "|" + state.remaining + "|" + amount); if (canAnnounce) { var tier = (amount >= TIER2_THRESHOLD) ? " (40+ bonus!)" : ""; publicAnnounce(user + " added " + add + " seconds!" + tier + " Time: " + formatMMSS(state.remaining)); } updateSubject(); cb.drawPanel(); }); // ---------- chat commands (broadcaster + mods) ---------- cb.onMessage(function (msg) { if (!msg || !msg.user || !msg.message) return; if (!msg.user.is_mod) return; var m = String(msg.message).trim().toLowerCase(); if (m === "/reset" || m === "/timer" || m === "/resettimer") { resetTimer(); return; } if (m === "/status") { cb.sendNotice("Timer: " + formatMMSS(state.remaining) + " remaining | +" + state.totalSecondsAdded + "s added | " + state.totalTokens + " tokens", msg.user.user, "#222222", "#FFFFFF", "bold"); return; } // /add <minutes> -> e.g. /add 2 var addMatch = m.match(/^\/add\s+(\d+(?:\.\d+)?)$/); if (addMatch) { var extra = Math.floor(parseFloat(addMatch[1]) * 60); state.remaining += extra; state.totalSecondsAdded += extra; postMarker("add|" + extra + "|broadcaster|" + state.remaining + "|"); publicAnnounce("Broadcaster added " + formatMMSS(extra) + "! Time: " + formatMMSS(state.remaining)); if (!state.running) { state.running = true; try { cb.limitCam_stop(); } catch(e){} } updateSubject(); cb.drawPanel(); if (!state.timerHandle) state.timerHandle = cb.setTimeout(tick, TICK_MS); return; } // /set <minutes> -> set timer to exact value var setMatch = m.match(/^\/set\s+(\d+(?:\.\d+)?)$/); if (setMatch) { var secs = Math.floor(parseFloat(setMatch[1]) * 60); postMarker("set|" + secs + "|"); publicAnnounce("Timer set to " + formatMMSS(secs)); try { cb.limitCam_stop(); } catch(e){} startTimer(secs); return; } // /title <text> -> change the visible card title var titleMatch = m.match(/^\/title\s+(.+)$/); if (titleMatch) { cb.settings.title_text = titleMatch[1]; postMarker("title|" + cb.settings.title_text + "|"); cb.sendNotice("Title set to: " + cb.settings.title_text, msg.user.user, "#222222", "#FFFFFF", "bold"); return; } }); // ---------- info panel ---------- cb.onDrawPanel(function (user) { var rows = []; rows.push({ 'label': 'Time:', 'value': state.running ? formatMMSS(state.remaining) : "EXPIRED" }); rows.push({ 'label': 'Tier (10 tok):','value': "<40: +60s | >=40: +90s" }); rows.push({ 'label': 'Total added:', 'value': "+" + formatMMSS(state.totalSecondsAdded) }); if (state.lastTipper) rows.push({ 'label': 'Last add:', 'value': state.lastTipper }); if (cb.settings.show_leaderboard === "yes") { var top = topAdders(3); for (var i = 0; i < top.length; i++) { rows.push({ 'label': "#" + (i + 1) + " saver:", 'value': top[i].user + " (+" + top[i].secs + "s)" }); } } return { 'template': '3_rows_of_labels', 'row1_label': rows[0] ? rows[0].label : "", 'row1_value': rows[0] ? rows[0].value : "", 'row2_label': rows[1] ? rows[1].label : "", 'row2_value': rows[1] ? rows[1].value : "", 'row3_label': rows[2] ? rows[2].label : "", 'row3_value': rows[2] ? rows[2].value : "" }; }); function topAdders(n) { var arr = []; for (var k in state.leaderboard) { if (Object.prototype.hasOwnProperty.call(state.leaderboard, k)) { arr.push({ user: k, secs: state.leaderboard[k] }); } } arr.sort(function (a, b) { return b.secs - a.secs; }); return arr.slice(0, n); } // ---------- lifecycle ---------- cb.onStart(function () { var secs = parseInt(cb.settings.initial_minutes) * 60; state.remaining = secs; state.running = true; postMarker("launch|" + secs + "|" + cb.settings.show_remaining + "|" + SECONDS_PER_TOKEN_TIER1 + "|" + SECONDS_PER_TOKEN_TIER2 + "|" + TIER2_THRESHOLD + "|"); postMarker("title|" + cb.settings.title_text + "|"); cb.log("[timer] started with " + secs + "s (" + cb.settings.initial_minutes + " min)"); updateSubject(); cb.drawPanel(); state.timerHandle = cb.setTimeout(tick, TICK_MS); }); cb.onBroadcastStart(function () { var secs = parseInt(cb.settings.initial_minutes) * 60; state.remaining = secs; state.running = true; try { cb.limitCam_stop(); } catch(e){} updateSubject(); cb.drawPanel(); if (state.timerHandle) cb.cancelTimeout(state.timerHandle); state.timerHandle = cb.setTimeout(tick, TICK_MS); }); cb.onBroadcastStop(function () { if (state.timerHandle) { cb.cancelTimeout(state.timerHandle); state.timerHandle = null; } state.running = false; });
© Copyright Chaturbate 2011- 2026. All Rights Reserved.