Newer
Older
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
}
function apply() {
const { sunscreen } = V.player.skin;
if (sunscreen.usesLeft <= 0) return;
sunscreen.lastsUntil = V.timeStamp + getDuration();
sunscreen.usesLeft = Math.max(0, sunscreen.usesLeft - 1);
}
function isApplied() {
const { sunscreen } = V.player.skin;
if (sunscreen.lastsUntil && V.timeStamp < sunscreen.lastsUntil) return true;
delete sunscreen.lastsUntil;
return false;
}
return {
/** Total duration of one use of sunscreen, in seconds */
get duration() {
return getDuration();
},
get bottle() {
return {
price: 1500,
uses: 15,
};
},
apply,
remove() {
delete V.player.skin.sunscreen.lastsUntil;
},
isApplied,
get timeLeft() {
const { lastsUntil } = V.player.skin.sunscreen;
return lastsUntil ? Math.max(0, lastsUntil - V.timeStamp) : 0;
},
/** @returns {number} */
get usesLeft() {
return V.player.skin.sunscreen.usesLeft;
},
/** @param {number} [uses] */
addUses(uses) {
V.player.skin.sunscreen.usesLeft += uses ?? this.bottle.uses;
},
};
})();
/*
Skin.tanningBonus: Value between 0 and 1. The bonus exists until time has been passed.
*/
const Skin = (() => {
const defaultModel = ["main", "sidebar"];
const defaultLayer = { layers: [], slots: {} };
const tanningMultiplier = 6; // Increase to make the tanning function even out more sharply (as the tan level increases)
const scalingFactor = 0.033; // Decrease for slower tanning gain from sun intensity
const tanningLossPerMinute = 0.000695; // ~1 per day - ~100 days from 100% to 0%
// Properties
const cachedLayers = null;
let accumulatedValue = 0;
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* Only run this from time.js
*
* TANNING GAIN/DECAY:
* - Logarithmic gain: Tanning gain slows down the higher it is.
* - If the total tanning value exceeds 100, the gain will be capped at 100, and any excess will be treated as tanning loss (to all groups except the one that gets the tanning gain)
* - Tanning decay is linear over time.
* - If a group gains tanning during the same time - only that group won't lose tanning.
* - If a group loses tanning to below 0, that group will be removed.
* - If tanning loss is higher than a group's value (causing it to be removed), the remainder will be distributed as a loss to the other layers.
* - If more than 60 minutes pass at once - divide the tanning calculations into 60-minute chunks.
* This is to follow the logarithmic curve more closely, and more realistically follow the day/night sun intensity cycle.
* - Limit of 10 layer groups. If 10 groups exist, and we want to add another one, remove the group with the lowest value, and distribute its value to the remaining groups.
*
* RENDERING:
* - Canvas needs to be rendered once in order for the layers to be saved for the tanning masks. (see limitations below)
* - Layer groups are created after the canvas has been readied for renderering (but before its rendered), in a new postprocess function.
* - Each layer group consist of the clothes being worn at the time of tanning - minus head, and handheld.
* - Layer groups are converted to their own canvas masks.
* - Animations are removed for all layers except for arms, and upper. (to avoid moving tan-lines)
* - Canvas masks are cached to avoid reloading them unecessarily.
* - They are re-cached only if the src layer has been changed. (E.g. if hand position goes from idle to cover)
* - If no clothes are applied when tanning - an empty layer group (with an empty mask) will be applied instead - causing the whole body to get tanned.
* - If there are multiple layer groups - apply the one with the highest value first. The remaining layers are applied on top, with their respective alpha value.
*
* @param {number} minutes
*/
function applyTanningGain(minutes) {
// Use event to wait for renderer before applying gain
$(document).one(":passageend", () => {
const model = Renderer.locateModel(...defaultModel);
const savedLayers = V.player.skin.layers;
const nextTime = new DateTime(Time.date);
let selectedLayersIndex = null;
if (!model.tanningLayers?.layers) {
console.warn("applyTanningGain: CanvasModel not found.");
return;
}
// If more than 60 minutes have passed, process tanning in chunks
while (minutes > 0) {
const chunkMinutes = Math.min(60, minutes);
minutes -= chunkMinutes;
const gainAmount = chunkMinutes * getTanningFactor(Skin.tanningBed).result;
if (gainAmount === 0) continue;
const currentTan = getTanningValue(savedLayers);
const current = getCurrentLayers(model, savedLayers);
const selectedLayers = setLayers(savedLayers, current);
const logFactorGain = 1 / Math.log1p(((currentTan + accumulatedValue) / 100) * tanningMultiplier + 1);
let tanningGain = gainAmount * logFactorGain * scalingFactor;
// Handle tanning gain and ensure the total tanning value does not exceed 100
if (currentTan + tanningGain >= 100) {
lowerTanningInLayers(savedLayers, currentTan + tanningGain - 100, savedLayers.indexOf(selectedLayers));
tanningGain = 100 - currentTan;
}
// Apply tanning gain if there's any
if (tanningGain > 0) {
selectedLayers.value += tanningGain;
const trimmedLayers = savedLayers.filter(group => group.layers.length > 0);
selectedLayersIndex = trimmedLayers.indexOf(savedLayers[selectedLayersIndex]);
if (trimmedLayers.length > maxLayerGroups) {
const lowestValueGroup = trimmedLayers.reduce(
(min, group, index) => (index !== selectedLayersIndex && (!min || group.value < min.value) ? group : min),
null
);
const index = savedLayers.indexOf(lowestValueGroup);
if (index !== -1) {
const [removedGroup] = savedLayers.splice(index, 1);
const valueToDistribute = removedGroup.value / savedLayers.length;
savedLayers.forEach(group => (group.value += valueToDistribute));
}
}
// Reset bonus and other modifiers after time passes
Skin.tanningBonus = 0;
Skin.tanningBed = false;
if (accumulatedValue > 0.1) {
Skin.recache();
}
});
}
function applyTanningLoss(minutes) {
const savedLayers = V.player.skin.layers;
const totalTanningLoss = minutes * tanningLossPerMinute;
// Reduce tanning on all layers equally
if (savedLayers.length > 0) {
lowerTanningInLayers(savedLayers, totalTanningLoss);
}
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
}
function lowerTanningInLayers(groups, totalTanningLoss, skipIndex = -1) {
const totalValue = groups.reduce((sum, group, index) => sum + (index !== skipIndex ? group.value : 0), 0);
// If reduction is to 0 or below, remove all layers, unless skipped
if (totalTanningLoss >= totalValue) {
groups.splice(0, groups.length, ...(skipIndex !== -1 ? [groups[skipIndex]] : []));
return;
}
let remainingLoss = totalTanningLoss;
let totalDistributedLoss = 0;
// Distribute the loss among the layers until the remaining loss is zero
// Adjust for floating point errors
while (round(remainingLoss, 8) > 0) {
let layerCount = 0;
const lossPerLayer = remainingLoss / (groups.length - (skipIndex >= 0 ? 1 : 0));
for (let i = groups.length - 1; i >= 0; i--) {
const group = groups[i];
if (groups.indexOf(group) === skipIndex) continue;
if (group.value > 0) {
layerCount++;
const actualLoss = Math.min(lossPerLayer, group.value);
const roundedLoss = round(group.value - actualLoss, 8);
remainingLoss -= actualLoss;
totalDistributedLoss += group.value - roundedLoss;
group.value = roundedLoss;
}
// Remove group if it drops to 0
if (group.value <= 0) {
remainingLoss += -group.value;
groups.splice(i, 1);
} else {
group.value = round(group.value, 8);
}
}
if (layerCount === 0) {
break;
}
}
// Distribute the remaining value from rounding into the first element
const adjustment = round(totalTanningLoss - totalDistributedLoss, 8);
if (groups.length > 0) {
groups[0].value = round(groups[0].value + adjustment, 8);
}
}
function getCurrentLayers(model, savedLayers) {
const index = tryGetMatchingLayer(savedLayers, model.tanningLayers);
if (index !== null) {
return { index, layers: savedLayers[index].layers, slots: savedLayers[index].slots };
}
return { index: null, layers: model.tanningLayers.layers, slots: model.tanningLayers.slots };
}
function tryGetMatchingLayer(groups, targetLayer) {
for (let i = 0; i < groups.length; i++) {
if (groups[i].slots.isEqual(targetLayer.slots)) {
return i;
}
}
return null;
}
function getTanningValue(groups) {
return groups.reduce((sum, obj) => sum + (obj.value ?? 0), 0);
}
function setLayers(savedLayers, currentLayers, index) {
if (!currentLayers) {
console.warn("setTanning: Could not find clothing groups");
return null;
}
if (index == null) {
savedLayers.push({
layers: currentLayers.layers,
slots: currentLayers.slots,
value: 0,
});
index = savedLayers.length - 1;
}
return savedLayers[index];
}
/**
* Returns tanning factor based on:
* sunIntensity (intensity from month of the year)
* weatherModifier (based on weather)
* locationModifier (based on location)
* clothingModifier (based on clothing)
* dayFactor (based on sun position in the sky) - always 0 at night
*
* @param {boolean} ignoreOutside Forces outside check
*/
function getTanningFactor(ignoreOutside) {
const outside = ignoreOutside ? 0 : V.outside;
const sunIntensity = (ignoreOutside ? 1 : Weather.getSunIntensity()) * (1 + Skin.tanningBonus);
// Reduces tanning effect even with only 1 shading clothing item
const clothingModifier = Object.values(V.worn).some(item => item.type.includes("shade")) ? 0.1 : 1;
// sunscreen prevents tanning gains entirely
const sunscreenModifier = Skin.Sunscreen.isApplied() ? 0 : 1;
const skinType = ["gyaru", "ygyaru"].includes(Skin.color.natural) ? 0.3 : 1;
const result = round(sunIntensity * clothingModifier * sunscreenModifier * skinType, 2);
return {
sun: sunIntensity,
month: Weather.genSettings.months[Time.date.month - 1].sunIntensity,
weather: outside ? Weather.current.tanningModifier : 1,
location: V.location === "forest" ? 0.2 : 1,
dayFactor: outside ? Time.date.simplifiedDayFactor : 1,
clothing: clothingModifier,
result,
};
}
function tanningGainOutput(modifier, minutes) {
if (V.statdisable !== "f") return "";
const factor = modifier * minutes;
if (factor === 0) {
return statDisplay.statChange("No tanning effect", 0, "blue");
}
return statDisplay.statChange("Tan", factor >= 50 ? 3 : factor >= 20 ? 2 : 1, "green");
}
function tanningPenaltiesOutput(modifiers) {
const reasons = [];
if (modifiers.sunscreen === 0) {
return `<span class="blue">Sunscreen prevented you from tanning.</span><br>`;
}
if (V.outside) {
const month = modifiers.month <= 0.6;
const dayState = Weather.sky.dayFactor <= 0.6;
const output = month ? Time.monthName : dayState ? "Sun is low" : "weather";
if (modifiers.sun <= 0.3) reasons.push(`Low sun intensity (${output})`);
else if (modifiers.sun <= 0.7) reasons.push(`Reduced sun intensity (${output})`);
if (modifiers.weather < 1) reasons.push("Light clouds");
}
if (modifiers.clothing < 1) reasons.push("Shaded by clothing");
if (reasons.length === 0) return "";
return `<span class="teal">Your tanning gain was reduced due to:</span><br><span class="orange">${reasons.join("<br>")}</span><br>`;
}
return {
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
applyTanningGain,
applyTanningLoss,
getTanningFactor,
tanningGainOutput,
tanningPenaltiesOutput,
get tanningLayers() {
return V.player.skin.layers;
},
get tanningBonus() {
return V.player.skin.tanningBonus ?? 0;
},
set tanningBonus(value) {
V.player.skin.tanningBonus = Math.clamp(value, 0, 1);
},
get tanningBed() {
return V.player.skin.tanningBed ?? false;
},
set tanningBed(value) {
V.player.skin.tanningBed = !!value;
},
get totalTan() {
const layers = V.player.skin.layers;
return getTanningValue(layers);
},
get color() {
return {
get natural() {
return V.player.skin.color || "light";
},
set natural(value) {
V.player.skin.color = value;
},
get tan() {
const layers = V.player.skin.layers;
return getTanningValue(layers);
},
set tan(value) {
Skin.color.setTan(value);
},
setTan(value, wholeBody = true) {
value = Math.clamp(value, 0, 100);
const savedLayers = V.player.skin.layers;
const totalTan = getTanningValue(savedLayers);
if (totalTan === value) return;
if (totalTan > value) {
const tanningLoss = totalTan - value;
lowerTanningInLayers(savedLayers, tanningLoss);
} else {
let group = {};
if (wholeBody) {
group = defaultLayer;
} else {
const model = Renderer.locateModel(...defaultModel);
group = getCurrentLayers(model, savedLayers);
}
const tanningGain = value - totalTan;
const selectedLayer = setLayers(savedLayers, group, tryGetMatchingLayer(savedLayers, group));
selectedLayer.value += tanningGain;
}
},
};
},
recache() {
Skin.cachedLayers = null;
accumulatedValue = 0;
},
// return V.player.skin.layers.reduce((count, layerGroup) => count + layerGroup.groups.length, 0);
},
// todo Only for red images. Remove after combat rework
cssColorFilter(type) {
return setup.colours.getSkinCSSFilter(type ?? Skin.color.natural, Skin.totalTan);
},