Skip to content

Commit

Permalink
Move to dynamic ticks and requestAnimationFrame
Browse files Browse the repository at this point in the history
  • Loading branch information
dmchurch committed Nov 16, 2023
1 parent eb0937c commit 70f43c8
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 78 deletions.
129 changes: 88 additions & 41 deletions actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,32 @@ function Actions() {
this.currentPos = 0;
this.timeSinceLastUpdate = 0;

this.tick = function() {
this.tick = function(availableMana) {
availableMana ??= 1;

const curAction = this.getNextValidAction();
// out of actions
if (!curAction) {
shouldRestart = true;
return;
return 0;
}
addExpFromAction(curAction);
curAction.ticks++;
curAction.manaUsed++;
curAction.timeSpent += 1 / baseManaPerSecond / getActualGameSpeed();
// restrict to the number of ticks it takes to get to a next level
availableMana = Math.min(availableMana, getMaxTicksForAction(curAction));
// restrict to the number of ticks it takes to finish the current action
availableMana = Math.min(availableMana, Math.ceil(curAction.adjustedTicks - curAction.ticks));
// just in case
if (availableMana < 0) availableMana = 0;

// regardless of what happens, this is the amount of mana we're spending
const manaToSpend = availableMana;

// exp needs to get added AFTER checking multipart progress, since this tick() call may
// represent any number of ticks, all of which process at the existing levels

curAction.ticks += manaToSpend;
curAction.manaUsed += manaToSpend;
curAction.timeSpent += manaToSpend / baseManaPerSecond / getActualGameSpeed();

// only for multi-part progress bars
if (curAction.loopStats) {
let segment = 0;
Expand All @@ -30,48 +45,66 @@ function Actions() {
segment++;
}
// segment is 0,1,2
const toAdd = curAction.tickProgress(segment) * (curAction.manaCost() / curAction.adjustedTicks);
// console.log("using: "+curAction.loopStats[(towns[curAction.townNum][curAction.varName + "LoopCounter"]+segment) % curAction.loopStats.length]+" to add: " + toAdd + " to segment: " + segment + " and part " +towns[curAction.townNum][curAction.varName + "LoopCounter"]+" of progress " + curProgress + " which costs: " + curAction.loopCost(segment));
towns[curAction.townNum][curAction.varName] += toAdd;
curProgress += toAdd;

// thanks to Gustav on the discord for the multipart loop code
let manaLeft = manaToSpend;
const tickMultiplier = (curAction.manaCost() / curAction.adjustedTicks);
let partUpdateRequired = false;
while (curProgress >= curAction.loopCost(segment)) {
curProgress -= curAction.loopCost(segment);
// segment finished
if (segment === curAction.segments - 1) {
// part finished
if (curAction.name === "Dark Ritual" && towns[curAction.townNum][curAction.varName] >= 4000000) unlockStory("darkRitualThirdSegmentReached");
if (curAction.name === "Imbue Mind" && towns[curAction.townNum][curAction.varName] >= 700000000) unlockStory("imbueMindThirdSegmentReached");
towns[curAction.townNum][curAction.varName] = 0;
towns[curAction.townNum][`${curAction.varName}LoopCounter`] += curAction.segments;
towns[curAction.townNum][`total${curAction.varName}`]++;
segment -= curAction.segments;
curAction.loopsFinished();
partUpdateRequired = true;
if (curAction.canStart && !curAction.canStart()) {
this.completedTicks += curAction.ticks;
view.requestUpdate("updateTotalTicks", null);
curAction.loopsLeft = 0;
curAction.ticks = 0;
curAction.manaRemaining = timeNeeded - timer;
curAction.goldRemaining = resources.gold;
curAction.finish();
totals.actions++;
break;

while (manaLeft > 0.01) {
//const toAdd = curAction.tickProgress(segment) * (curAction.manaCost() / curAction.adjustedTicks);
const progressMultiplier = curAction.tickProgress(segment) * tickMultiplier;
const toAdd = Math.min(
manaLeft * progressMultiplier, // how much progress would we make if we spend all available mana?
curAction.loopCost(segment) - curProgress // how much progress would it take to complete this segment?
);
manaLeft -= toAdd / progressMultiplier;
// console.log("using: "+curAction.loopStats[(towns[curAction.townNum][curAction.varName + "LoopCounter"]+segment) % curAction.loopStats.length]+" to add: " + toAdd + " to segment: " + segment + " and part " +towns[curAction.townNum][curAction.varName + "LoopCounter"]+" of progress " + curProgress + " which costs: " + curAction.loopCost(segment));
towns[curAction.townNum][curAction.varName] += toAdd;
curProgress += toAdd;
while (curProgress >= curAction.loopCost(segment)) {
curProgress -= curAction.loopCost(segment);
// segment finished
if (segment === curAction.segments - 1) {
// part finished
if (curAction.name === "Dark Ritual" && towns[curAction.townNum][curAction.varName] >= 4000000) unlockStory("darkRitualThirdSegmentReached");
if (curAction.name === "Imbue Mind" && towns[curAction.townNum][curAction.varName] >= 700000000) unlockStory("imbueMindThirdSegmentReached");
towns[curAction.townNum][curAction.varName] = 0;
towns[curAction.townNum][`${curAction.varName}LoopCounter`] += curAction.segments;
towns[curAction.townNum][`total${curAction.varName}`]++;
segment -= curAction.segments;
curAction.loopsFinished();
partUpdateRequired = true;
if (curAction.canStart && !curAction.canStart()) {
this.completedTicks += curAction.ticks;
view.requestUpdate("updateTotalTicks", null);
curAction.loopsLeft = 0;
curAction.ticks = 0;
curAction.manaRemaining = timeNeeded - timer;
curAction.goldRemaining = resources.gold;
curAction.finish();
totals.actions++;
break;
}
towns[curAction.townNum][curAction.varName] = curProgress;
}
towns[curAction.townNum][curAction.varName] = curProgress;
}
if (curAction.segmentFinished) {
curAction.segmentFinished();
partUpdateRequired = true;
if (curAction.segmentFinished) {
curAction.segmentFinished();
partUpdateRequired = true;
}
segment++;
}
segment++;
}

view.requestUpdate("updateMultiPartSegments", curAction);
if (partUpdateRequired) {
view.requestUpdate("updateMultiPart", curAction);
}
}

// exp gets added here, where it can factor in to adjustTicksNeeded
addExpFromAction(curAction, manaToSpend);

if (curAction.ticks >= curAction.adjustedTicks) {
curAction.ticks = 0;
curAction.loopsLeft--;
Expand Down Expand Up @@ -101,6 +134,8 @@ function Actions() {
this.currentPos++;
}
}

return manaToSpend;
};

this.getNextValidAction = function() {
Expand Down Expand Up @@ -262,8 +297,20 @@ function calcTalentMult(talent) {
return 1 + Math.pow(talent, 0.4) / 3;
}

function addExpFromAction(action) {
const adjustedExp = action.expMult * (action.manaCost() / action.adjustedTicks);
// how many ticks would it take to get to the first level up
function getMaxTicksForAction(action) {
let maxTicks = Number.MAX_SAFE_INTEGER;
const expMultiplier = action.expMult * (action.manaCost() / action.adjustedTicks);
for (const stat in action.stats) {
const expToNext = getExpToLevel(stat);
const statMultiplier = expMultiplier * action.stats[stat] * getTotalBonusXP(stat);
maxTicks = Math.min(maxTicks, Math.ceil(expToNext / statMultiplier));
}
return maxTicks;
}

function addExpFromAction(action, manaCount) {
const adjustedExp = manaCount * action.expMult * (action.manaCost() / action.adjustedTicks);
for (const stat in action.stats) {
const expToAdd = action.stats[stat] * adjustedExp * getTotalBonusXP(stat);
const statExp = `statExp${stat}`;
Expand Down
130 changes: 99 additions & 31 deletions driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ let gameSpeed = 1;
const baseManaPerSecond = 50;

let curTime = new Date();
let gameTicksLeft = 0;
let gameTicksLeft = 0; // actually milliseconds, not ticks
let refund = false;
let radarUpdateTime = 0;
let timeCounter = 0;
Expand Down Expand Up @@ -40,6 +40,43 @@ function getActualGameSpeed() {
return gameSpeed * getSpeedMult() * bonusSpeed;
}

function refreshDungeons(manaSpent) {
for (const dungeon of dungeons) {
for (const level of dungeon) {
const chance = level.ssChance;
if (chance < 1) level.ssChance = Math.min(chance + 0.0000001 * manaSpent, 1);
}
}
}

function singleTick() {
timer++;
timeCounter += 1 / baseManaPerSecond;
effectiveTime += 1 / baseManaPerSecond;

actions.tick();

refreshDungeons(1);

if (shouldRestart || timer >= timeNeeded) {
loopEnd();
prepareRestart();
}
gameTicksLeft -= ((1000 / baseManaPerSecond));
}

let lastAnimationTime = 0;
let animationFrameRequest = 0;

function animationTick(animationTime) {
if (animationTime == lastAnimationTime) {
// double tick in the same frame, drop this one
return;
}
animationFrameRequest = requestAnimationFrame(animationTick);
tick();
}

function tick() {
const newTime = Date.now();
gameTicksLeft += newTime - curTime;
Expand All @@ -60,42 +97,69 @@ function tick() {
return;
}

while (gameTicksLeft > (1000 / baseManaPerSecond)) {
if (gameTicksLeft > 2000) {
console.warn(`too fast! (${gameTicksLeft})`);
statGraph.graphObject.options.animation.duration = 0;
gameTicksLeft = 0;
refund = true;
}
// convert "gameTicksLeft" (actually milliseconds) into equivalent base-mana count, aka actual game ticks
// including the gameSpeed multiplier here because it is effectively constant over the course of a single
// update, and it affects how many actual game ticks pass in a given span of realtime.
let baseManaToBurn = Math.floor(gameTicksLeft * baseManaPerSecond * gameSpeed / 1000);

while (baseManaToBurn * bonusSpeed >= 1) {
if (stop) {
return;
break;
}
timer++;
timeCounter += 1 / baseManaPerSecond / getActualGameSpeed();
effectiveTime += 1 / baseManaPerSecond / getSpeedMult();

actions.tick();
for (const dungeon of dungeons) {
for (const level of dungeon) {
const chance = level.ssChance;
if (chance < 1) level.ssChance = Math.min(chance + 0.0000001, 1);
}
// first, figure out how much *actual* mana is available to get spent. bonusSpeed gets rolled in first,
// since it can change over the course of an update (if offline time runs out)
let manaAvailable = baseManaToBurn;
// totalMultiplier lets us back-convert from manaAvailable (in units of "effective game ticks") to
// baseManaToBurn (in units of "realtime ticks modulated by gameSpeed") once we figure out how much
// of our mana we're using in this cycle
let totalMultiplier = 1;

if (bonusSpeed > 1) {
// can't spend more mana than offline time available
manaAvailable = Math.min(manaAvailable * bonusSpeed, Math.ceil(totalOfflineMs * baseManaPerSecond * gameSpeed * bonusSpeed / 1000));
totalMultiplier *= bonusSpeed;
}

// next, roll in the multiplier from skills/etc
let speedMult = getSpeedMult();
manaAvailable *= speedMult;
totalMultiplier *= speedMult;

// limit to only how much time we have available
manaAvailable = Math.min(manaAvailable, timeNeeded - timer);

// don't run more than 1 tick
if (shouldRestart) {
manaAvailable = Math.min(manaAvailable, 1);
}

// a single action may not use a partial tick, so ceil() to be sure
const manaSpent = Math.ceil(actions.tick(manaAvailable));

// okay, so the current action has used manaSpent effective ticks. figure out how much of our realtime
// that accounts for, in base ticks and in seconds.
const baseManaSpent = manaSpent / totalMultiplier;
const timeSpent = baseManaSpent / gameSpeed / baseManaPerSecond;

// update timers
timer += manaSpent; // number of effective mana ticks
timeCounter += timeSpent; // realtime seconds
effectiveTime += timeSpent * gameSpeed * bonusSpeed; // "seconds" modified only by gameSpeed and offline bonus
baseManaToBurn -= baseManaSpent; // burn spent mana
gameTicksLeft -= timeSpent * 1000;

// spend bonus time for this segment
if (bonusSpeed > 1) {
addOffline(-Math.abs(timeSpent * (bonusSpeed - 1)));
}

refreshDungeons(manaSpent);

if (shouldRestart || timer >= timeNeeded) {
loopEnd();
prepareRestart();
break; // don't span loops within tick()
}
gameTicksLeft -= ((1000 / baseManaPerSecond) / getActualGameSpeed());
}

if (bonusSpeed > 1) {
addOffline(-Math.abs(delta * (bonusSpeed - 1)));
}

if (refund) {
addOffline(delta);
refund = false;
}

if (radarUpdateTime > 1000) {
Expand All @@ -112,8 +176,12 @@ function recalcInterval(fps) {
if (mainTickLoop !== undefined) {
clearInterval(mainTickLoop);
}
doWork.postMessage({ stop: true });
doWork.postMessage({ start: true, ms: (1000 / fps) });
if (window.requestAnimationFrame) {
animationFrameRequest = requestAnimationFrame(animationTick);
mainTickLoop = setInterval(tick, 1000);
} else {
mainTickLoop = setInterval(tick, 1000 / fps);
}
}

function stopGame() {
Expand Down
5 changes: 5 additions & 0 deletions lang/en-EN/game.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
<title>Changelog</title>
<version_prefix>Version</version_prefix>
</meta>
<version verNum="3.0.2"><![CDATA[
<b> Nov 15th, 2023</b>
• Rewritten tick() code uses browser's requestAnimationFrame if available, bypassing the "Visual updates" option entirely. Ticks should be more efficient as well, especially under high game speeds.<br>
• Fix to Actions.tick() from Gustav from Discord, now multi-segment actions that complete multiple segments in a single tick will calculate each segment individually.<br>
]]></version>
<version verNum="3.0.1"><![CDATA[
<b> Nov 14th, 2023</b>
• Yarr, the game has been taken over by dmchurch, where is this boat even going?<br>
Expand Down
7 changes: 1 addition & 6 deletions saving.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
function startGame() {
window.doWork = new Worker("interval.js");
window.doWork.onmessage = function(event) {
if (event.data === "interval.start") {
tick();
}
};
// load calls recalcInterval, which will start the callbacks
load();
setScreenSize();
}
Expand Down
9 changes: 9 additions & 0 deletions stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,15 @@ function addBuffAmt(name, amount) {
view.requestUpdate("updateBuff",name);
}

// how much "addExp" would you have to do to get this stat to the next exp or talent level
function getExpToLevel(name) {
const expToNext = getExpOfLevel(getLevel(name) + 1) - stats[name].exp;
const talentToNext = getExpOfTalent(getTalent(name) + 1) - stats[name].talent;
const aspirantBonus = getBuffLevel("Aspirant") ? getBuffLevel("Aspirant") * 0.01 : 0;
const talentMultiplier = (getSkillBonus("Wunderkind") + aspirantBonus) / 100;
return Math.ceil(Math.min(expToNext, talentToNext / talentMultiplier));
}

function addExp(name, amount) {
stats[name].exp += amount;
const aspirantBonus = getBuffLevel("Aspirant") ? getBuffLevel("Aspirant") * 0.01 : 0;
Expand Down

0 comments on commit 70f43c8

Please sign in to comment.