improve jstris skin

This commit is contained in:
2026-03-09 21:03:45 +01:00
parent 4bac3face1
commit 3bb9f302ed
3 changed files with 296 additions and 273 deletions

View File

@@ -11,6 +11,7 @@
background-image: var(--skin-url);
background-size: cover;
background-repeat: no-repeat;
vertical-align: middle;
}
.result {
@@ -20,7 +21,7 @@
.selection {
background-position-x: calc(-1 * var(--sprite-pos) * var(--cell-size));
--nb-sprites: 4;
--sprite-pos: 3;
--sprite-pos: 4;
}
.minoes-table {
@@ -31,7 +32,6 @@
background-image: url(jstris-skin/jstris-grid.png);
background-position: bottom;
background-repeat: no-repeat;
background-blend-mode: screen;
}
tr.matrix td:not(.mino) {
@@ -76,13 +76,19 @@ tr.matrix td:not(.mino) {
}
.ghost {
--sprite-pos: 0;
--sprite-pos: 1;
opacity: 50%;
}
.disabled {
--sprite-pos: 1;
--sprite-pos: 0;
}
.locking.mino {
filter: saturate(60%) brightness(180%);
}
#holdTable .mino,
#nextTable .mino {
box-shadow: 4px 4px 10px #0002;
}

View File

@@ -76,19 +76,10 @@
<label for="dasInput" class="col-2 col-form-label"><abbr title="Delayed AutoShift : délai initial avant répétition">DAS</abbr></label>
</fieldset>
<fieldset class="row g-2 mb-3 align-items-center text-center"><legend class="text-start">Interface</legend>
<label for="sfxVolumeRange" class="col-2 col-form-label">Volume</label>
<div class="col-4 d-flex align-items-baseline"><input name="sfxVolumeRange" id="sfxVolumeRange" class="form-range" type="range" min="0" max="1" step="any" value="0.7"></div>
<div class="col-4">
<div class="form-check form-switch text-start">
<input id="fullscreenCheckbox" type="checkbox" role="switch" class="form-check-input" tabindex="0">
<label for="fullscreenCheckbox" class="form-check-label">Plein écran</label>
</div>
</div>
<div class="col-2"></div>
<label for="stylesheetSelect" class="col-2 col-form-label">Thème</label>
<div class="col-4">
<select name="stylesheet" id="stylesheetSelect" class="form-select"
oninput="selectedStyleSheet.href = this.value; skinURLdiv.style.setProperty('display', this.value === 'css/tetrio-skin.css' || this.value === 'css/jstris-skin.css' ? 'flex' : 'none')">
oninput="selectedStyleSheet.href = this.value; skinURLSelect.disabled = this.value !== 'css/jstris-skin.css';">
<option value="css/classic.css" selected>Classique</option>
<option value="css/neo-classic.css" selected>Néo-classique</option>
<option value="css/synthwave.css">Synthwave</option>
@@ -100,15 +91,21 @@
<option value="css/jstris-skin.css">Sample...</option>
</select>
</div>
<div id="skinURLdiv" class="col-6" style="display: none;">
<div class="col-8">
<select name="skinURL" id="skinURLInput" class="form-select" value="https://i.imgur.com/0l7LFMT.png"
oninput="document.documentElement.style.setProperty('--skin-url', `url(${this.value})`)">
</select>
</script>
</div>
<label for="skinURLInput" class="col-4 col-form-label">Sample</label>
<div class="col-4">
<select name="skinURL" id="skinURLSelect" class="form-select" disabled
oninput="document.documentElement.style.setProperty('--skin-url', `url(${this.value})`)">
</select>
</div>
<label for="skinURLSelect" class="col-2 col-form-label">Sample</label>
<label for="sfxVolumeRange" class="col-2 col-form-label">Volume</label>
<div class="col-4 d-flex align-items-baseline"><input name="sfxVolumeRange" id="sfxVolumeRange" class="form-range" type="range" min="0" max="1" step="any" value="0.7"></div>
<div class="col-4">
<div class="form-check form-switch text-start">
<input id="fullscreenCheckbox" type="checkbox" role="switch" class="form-check-input" tabindex="0">
<label for="fullscreenCheckbox" class="form-check-label">Plein écran</label>
</div>
</div>
<div class="col-2"></div>
</fieldset>
<fieldset class="row g-2 mb-3 align-items-center text-center"><legend class="text-start">Partie</legend>
<label for="levelInput" class="col-2 col-form-label text-center">Niveau</label>

View File

@@ -1,384 +1,404 @@
const KEY_NAMES = new Proxy({
["ArrowLeft"] : "←",
["←"] : "ArrowLeft",
["ArrowRight"] : "→",
["→"] : "ArrowRight",
["ArrowUp"] : "↑",
["↑"] : "ArrowUp",
["ArrowDown"] : "↓",
["↓"] : "ArrowDown",
[" "] : "Espace",
["Espace"] : " ",
["Escape"] : "Échap.",
["Échap."] : "Escape",
["Backspace"] : "Ret. arrière",
["Ret. arrière"]: "Backspace",
["Enter"] : "Entrée",
["Entrée"] : "Enter",
}, {
get(target, key) {
return key in target? target[key] : key
}
})
const KEY_NAMES = new Proxy(
{
['ArrowLeft']: '←',
['←']: 'ArrowLeft',
['ArrowRight']: '→',
['→']: 'ArrowRight',
['ArrowUp']: '↑',
['↑']: 'ArrowUp',
['ArrowDown']: '↓',
['↓']: 'ArrowDown',
[' ']: 'Espace',
['Espace']: ' ',
['Escape']: 'Échap.',
['Échap.']: 'Escape',
['Backspace']: 'Ret. arrière',
['Ret. arrière']: 'Backspace',
['Enter']: 'Entrée',
['Entrée']: 'Enter',
},
{
get(target, key) {
return key in target ? target[key] : key;
},
},
);
class Settings {
constructor() {
this.form = settingsForm
this.load()
this.modal = new bootstrap.Modal('#settingsModal')
settingsModal.addEventListener('shown.bs.modal', () => resumeButton.focus())
this.form = settingsForm;
this.load();
this.modal = new bootstrap.Modal('#settingsModal');
settingsModal.addEventListener('shown.bs.modal', () => resumeButton.focus());
}
load() {
this.form.querySelectorAll("[name]").forEach(element => {
if (element.name in localStorage)
element.value = localStorage[element.name]
})
selectedStyleSheet.href = stylesheetSelect.value
if (stylesheetSelect.value === "css/tetrio-skin.css" || stylesheetSelect.value === "css/jstris-skin.css") {
skinURLdiv.style.setProperty('display', 'flex')
} else {
skinURLdiv.style.setProperty('display', 'none')
}
fetch('https://konsola5.github.io/jstris-customization-database/jstrisCustomizationDatabase.json')
.then(response => response.json())
.then(json => {
for (const group in json) {
const optgroup = document.createElement('optgroup');
optgroup.label = group;
json[group].forEach(skin => {
const option = document.createElement('option');
option.value = skin.link;
option.textContent = `${skin.name} (${skin.author})`;
optgroup.appendChild(option);
});
skinURLInput.appendChild(optgroup);
}
$('#skinURLInput').select2({
templateResult: state => state.id ? $(`<span class="option result" style="--skin-url: url(${state.id})" title="${state.text}"></span>`): state.text,
templateSelection: state => state.id ? $(`<span class="option selection" style="--skin-url: url(${state.id})" title="${state.text}"></span>`): state.text,
theme: 'bootstrap-5',
width: '100%',
selectionCssClass: 'form-select',
dropdownAutoWidth: true,
dropdownParent: $('#settingsModal'),
placeholder: "URL de l'image",
tags: true,
createTag: function(params) {
return {
id: params.term,
text: "Ajouté manuellement",
newTag: true
};
},
})
if (localStorage["skinURL"]) {
if ($('#skinURLInput').find("option[value='" + localStorage["skinURL"] + "']").length) {
$('#skinURLInput').val(localStorage["skinURL"]).trigger('change');
} else {
var option = new Option(localStorage["skinURL"], "Ajouté manuellement", localStorage["skinURL"], true, true);
$('#skinURLInput').append(option).trigger('change');
}
document.documentElement.style.setProperty('--skin-url', `url(${localStorage["skinURL"]})`)
}
this.form.querySelectorAll('[name]').forEach(element => {
if (element.name in localStorage) element.value = localStorage[element.name];
});
selectedStyleSheet.href = stylesheetSelect.value;
skinURLSelect.disabled = stylesheetSelect.value !== 'css/jstris-skin.css';
fetch(
'https://konsola5.github.io/jstris-customization-database/jstrisCustomizationDatabase.json',
)
.then(response => response.json())
.then(json => {
for (const group in json) {
const optgroup = document.createElement('optgroup');
optgroup.label = group;
json[group].forEach(skin => {
const option = document.createElement('option');
option.value = skin.link;
option.textContent = `${skin.name} (${skin.author})`;
optgroup.appendChild(option);
});
skinURLSelect.appendChild(optgroup);
}
$('#skinURLSelect').select2({
templateResult: state =>
state.id
? $(`<img class="option result" src="${state.id}" title="${state.text}" loading="lazy"/>`)
: state.text,
templateSelection: state =>
state.id
? $(`<span class="option selection" style="--skin-url: url(${state.id})" title="${state.text}" loading="lazy"></span>`)
: state.text,
theme: 'bootstrap-5',
width: '100%',
selectionCssClass: 'form-select',
dropdownAutoWidth: true,
dropdownParent: $('#settingsModal'),
placeholder: "URL de l'image",
tags: true,
});
if (localStorage['skinURL']) {
if (
$('#skinURLSelect').find("option[value='" + localStorage['skinURL'] + "']")
.length
) {
$('#skinURLSelect').val(localStorage['skinURL']).trigger('change');
} else {
var option = new Option(
localStorage['skinURL'],
'Ajouté manuellement',
localStorage['skinURL'],
true,
true,
);
$('#skinURLSelect').append(option).trigger('change');
}
document.documentElement.style.setProperty(
'--skin-url',
`url(${localStorage['skinURL']})`,
);
}
});
}
save() {
this.form.querySelectorAll("[name]").forEach(element => localStorage[element.name] = element.value)
this.form
.querySelectorAll('[name]')
.forEach(element => (localStorage[element.name] = element.value));
}
init() {
this.form.onsubmit = newGame
levelInput.name = "startLevel"
levelInput.disabled = false
titleHeader.innerHTML = "QUATUOR"
resumeButton.innerHTML = "Jouer"
this.form.onsubmit = newGame;
levelInput.name = 'startLevel';
levelInput.disabled = false;
titleHeader.innerHTML = 'QUATUOR';
resumeButton.innerHTML = 'Jouer';
}
show() {
resumeButton.disabled = false
settings.form.classList.remove('was-validated')
settings.modal.show()
settings.form.reportValidity()
resumeButton.disabled = false;
settings.form.classList.remove('was-validated');
settings.modal.show();
settings.form.reportValidity();
}
getInputs() {
for (let input of this.form.querySelectorAll("input[type='text']")) {
this[input.name] = KEY_NAMES[input.value]
this[input.name] = KEY_NAMES[input.value];
}
for (let input of this.form.querySelectorAll("input[type='number'], input[type='range']")) {
this[input.name] = input.valueAsNumber
this[input.name] = input.valueAsNumber;
}
for (let input of this.form.querySelectorAll("input[type='checkbox']")) {
this[input.name] = input.checked == true
this[input.name] = input.checked == true;
}
this.keyBind = new Proxy({}, {
get: (target, key) => target[key.toLowerCase()],
set: (target, key, value) => target[key.toLowerCase()] = value,
has: (target, key) => key.toLowerCase() in target
})
this.keyBind = new Proxy(
{},
{
get: (target, key) => target[key.toLowerCase()],
set: (target, key, value) => (target[key.toLowerCase()] = value),
has: (target, key) => key.toLowerCase() in target,
},
);
for (let actionName in playerActions) {
this.keyBind[settings[actionName]] = playerActions[actionName]
this.keyBind[settings[actionName]] = playerActions[actionName];
}
}
}
function changeKey(input) {
prevValue = input.value
input.value = ""
keyInputs = Array.from(input.form.querySelectorAll("input[type='text']"))
prevValue = input.value;
input.value = '';
keyInputs = Array.from(input.form.querySelectorAll("input[type='text']"));
input.onkeydown = function (event) {
event.preventDefault()
input.value = KEY_NAMES[event.key]
event.preventDefault();
input.value = KEY_NAMES[event.key];
keyInputs.forEach(input => {
input.setCustomValidity("")
input.classList.remove("is-invalid")
})
input.setCustomValidity('');
input.classList.remove('is-invalid');
});
keyInputs.sort((input1, input2) => {
if(input1.value == input2.value) {
input1.setCustomValidity("Touche déjà utilisée")
input1.classList.add("is-invalid")
input2.setCustomValidity("Touche déjà utilisée")
input2.classList.add("is-invalid")
if (input1.value == input2.value) {
input1.setCustomValidity('Touche déjà utilisée');
input1.classList.add('is-invalid');
input2.setCustomValidity('Touche déjà utilisée');
input2.classList.add('is-invalid');
}
return input1.value > input2.value
})
return input1.value > input2.value;
});
if (input.checkValidity()) {
input.blur()
input.blur();
}
}
};
input.onblur = function (event) {
if (!input.value) input.value = prevValue
input.onkeydown = null
input.onblur = null
}
if (!input.value) input.value = prevValue;
input.onkeydown = null;
input.onblur = null;
};
}
class Stats {
constructor() {
this.modal = new bootstrap.Modal('#statsModal')
this.load()
this.modal = new bootstrap.Modal('#statsModal');
this.load();
}
load() {
this.highScore = Number(localStorage["highScore"]) || 0
this.highScore = Number(localStorage['highScore']) || 0;
}
init() {
levelInput.value = localStorage["startLevel"] || 1
this.score = 0
this.goal = 0
this.combo = 0
this.b2b = 0
this.startTime = new Date()
this.lockDelay = DELAY.LOCK
this.totalClearedLines = 0
this.nbQuatuors = 0
this.nbTSpin = 0
this.maxCombo = 0
this.maxB2B = 0
levelInput.value = localStorage['startLevel'] || 1;
this.score = 0;
this.goal = 0;
this.combo = 0;
this.b2b = 0;
this.startTime = new Date();
this.lockDelay = DELAY.LOCK;
this.totalClearedLines = 0;
this.nbQuatuors = 0;
this.nbTSpin = 0;
this.maxCombo = 0;
this.maxB2B = 0;
}
set score(score) {
this._score = score
scoreCell.innerText = score.toLocaleString()
this._score = score;
scoreCell.innerText = score.toLocaleString();
if (score > this.highScore) {
this.highScore = score
this.highScore = score;
}
}
get score() {
return this._score
return this._score;
}
set highScore(highScore) {
this._highScore = highScore
highScoreCell.innerText = highScore.toLocaleString()
this._highScore = highScore;
highScoreCell.innerText = highScore.toLocaleString();
}
get highScore() {
return this._highScore
return this._highScore;
}
set level(level) {
this._level = level
this.goal += level * 5
if (level <= 20){
this.fallPeriod = 1000 * Math.pow(0.8 - ((level - 1) * 0.007), level - 1)
this._level = level;
this.goal += level * 5;
if (level <= 20) {
this.fallPeriod = 1000 * Math.pow(0.8 - (level - 1) * 0.007, level - 1);
}
if (level > 15)
this.lockDelay = 500 * Math.pow(0.9, level - 15)
levelInput.value = level
levelCell.innerText = level
messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `<h1>NIVEAU<br/>${this.level}</h1>` })
if (level > 15) this.lockDelay = 500 * Math.pow(0.9, level - 15);
levelInput.value = level;
levelCell.innerText = level;
messagesSpan.addNewChild('div', {
className: 'show-level-animation',
innerHTML: `<h1>NIVEAU<br/>${this.level}</h1>`,
});
}
get level() {
return this._level
return this._level;
}
set goal(goal) {
this._goal = goal
goalCell.innerText = goal
this._goal = goal;
goalCell.innerText = goal;
}
get goal() {
return this._goal
return this._goal;
}
set combo(combo) {
this._combo = combo
if (combo > this.maxCombo) this.maxCombo = combo
this._combo = combo;
if (combo > this.maxCombo) this.maxCombo = combo;
}
get combo() {
return this._combo
return this._combo;
}
set b2b(b2b) {
this._b2b = b2b
if (b2b > this.maxB2B) this.maxB2B = b2b
this._b2b = b2b;
if (b2b > this.maxB2B) this.maxB2B = b2b;
}
get b2b() {
return this._b2b
return this._b2b;
}
set time(time) {
this.startTime = new Date() - time
ticktack()
this.startTime = new Date() - time;
ticktack();
}
get time() {
return new Date() - this.startTime
return new Date() - this.startTime;
}
lockDown(tSpin, nbClearedLines) {
this.totalClearedLines += nbClearedLines
if (nbClearedLines == 4) this.nbQuatuors++
if (tSpin == T_SPIN.T_SPIN) this.nbTSpin++
this.totalClearedLines += nbClearedLines;
if (nbClearedLines == 4) this.nbQuatuors++;
if (tSpin == T_SPIN.T_SPIN) this.nbTSpin++;
// Cleared lines & T-Spin
let awardedLineClears = AWARDED_LINE_CLEARS[tSpin][nbClearedLines]
let patternScore = 100 * this.level * awardedLineClears
if (tSpin) messagesSpan.addNewChild("div", {
className: "rotate-in-animation",
innerHTML: tSpin
})
if (nbClearedLines) messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
innerHTML: CLEARED_LINES_NAMES[nbClearedLines]
})
let awardedLineClears = AWARDED_LINE_CLEARS[tSpin][nbClearedLines];
let patternScore = 100 * this.level * awardedLineClears;
if (tSpin)
messagesSpan.addNewChild('div', {
className: 'rotate-in-animation',
innerHTML: tSpin,
});
if (nbClearedLines)
messagesSpan.addNewChild('div', {
className: 'zoom-in-animation',
innerHTML: CLEARED_LINES_NAMES[nbClearedLines],
});
if (patternScore) {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .2s",
innerHTML: patternScore
})
this.score += patternScore
messagesSpan.addNewChild('div', {
className: 'zoom-in-animation',
style: 'animation-delay: .2s',
innerHTML: patternScore,
});
this.score += patternScore;
}
// Combo
if (nbClearedLines) {
this.combo++
this.combo++;
if (this.combo >= 1) {
let comboScore = (nbClearedLines == 1 ? 20 : 50) * this.combo * this.level
let comboScore = (nbClearedLines == 1 ? 20 : 50) * this.combo * this.level;
if (this.combo == 1) {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `COMBO<br/>${comboScore}`
})
messagesSpan.addNewChild('div', {
className: 'zoom-in-animation',
style: 'animation-delay: .4s',
innerHTML: `COMBO<br/>${comboScore}`,
});
} else {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `COMBO x${this.combo}<br/>${comboScore}`
})
messagesSpan.addNewChild('div', {
className: 'zoom-in-animation',
style: 'animation-delay: .4s',
innerHTML: `COMBO x${this.combo}<br/>${comboScore}`,
});
}
this.score += comboScore
this.score += comboScore;
}
} else {
this.combo = -1
this.combo = -1;
}
// Back to back sequence
if ((nbClearedLines == 4) || (tSpin && nbClearedLines)) {
this.b2b++
if (nbClearedLines == 4 || (tSpin && nbClearedLines)) {
this.b2b++;
if (this.b2b >= 1) {
let b2bScore = patternScore / 2
let b2bScore = patternScore / 2;
if (this.b2b == 1) {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `BOUT À BOUT<br/>${b2bScore}`
})
messagesSpan.addNewChild('div', {
className: 'zoom-in-animation',
style: 'animation-delay: .4s',
innerHTML: `BOUT À BOUT<br/>${b2bScore}`,
});
} else {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `BOUT À BOUT x${this.b2b}<br/>${b2bScore}`
})
messagesSpan.addNewChild('div', {
className: 'zoom-in-animation',
style: 'animation-delay: .4s',
innerHTML: `BOUT À BOUT x${this.b2b}<br/>${b2bScore}`,
});
}
this.score += b2bScore
this.score += b2bScore;
}
} else if (nbClearedLines && !tSpin ) {
} else if (nbClearedLines && !tSpin) {
if (this.b2b >= 1) {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `FIN DU BOUT À BOUT`
})
messagesSpan.addNewChild('div', {
className: 'zoom-in-animation',
style: 'animation-delay: .4s',
innerHTML: `FIN DU BOUT À BOUT`,
});
}
this.b2b = -1
this.b2b = -1;
}
// Sound
if (sfxVolumeRange.value) {
if (nbClearedLines == 4) playSound(quatuorSound, this.combo)
else if (nbClearedLines) playSound(lineClearSound, this.combo)
if (tSpin) playSound(tSpinSound, this.combo)
if (nbClearedLines == 4) playSound(quatuorSound, this.combo);
else if (nbClearedLines) playSound(lineClearSound, this.combo);
if (tSpin) playSound(tSpinSound, this.combo);
}
this.goal -= awardedLineClears
if (this.goal <= 0) this.level++
this.goal -= awardedLineClears;
if (this.goal <= 0) this.level++;
}
show() {
let time = stats.time
statsModalScoreCell.innerText = this.score.toLocaleString()
statsModalHighScoreCell.innerText = this.highScore.toLocaleString()
statsModalLevelCell.innerText = this.level
statsModalTimeCell.innerText = this.timeFormat.format(time)
statsModaltotalClearedLines.innerText = this.totalClearedLines
statsModaltotalClearedLinesPM.innerText = (stats.totalClearedLines * 60000 / time).toFixed(2)
statsModalNbQuatuors.innerText = this.nbQuatuors
statsModalNbTSpin.innerText = this.nbTSpin
statsModalMaxCombo.innerText = this.maxCombo
statsModalMaxB2B.innerText = this.maxB2B
this.modal.show()
let time = stats.time;
statsModalScoreCell.innerText = this.score.toLocaleString();
statsModalHighScoreCell.innerText = this.highScore.toLocaleString();
statsModalLevelCell.innerText = this.level;
statsModalTimeCell.innerText = this.timeFormat.format(time);
statsModaltotalClearedLines.innerText = this.totalClearedLines;
statsModaltotalClearedLinesPM.innerText = (
(stats.totalClearedLines * 60000) /
time
).toFixed(2);
statsModalNbQuatuors.innerText = this.nbQuatuors;
statsModalNbTSpin.innerText = this.nbTSpin;
statsModalMaxCombo.innerText = this.maxCombo;
statsModalMaxB2B.innerText = this.maxB2B;
this.modal.show();
}
save() {
localStorage["highScore"] = this.highScore
localStorage['highScore'] = this.highScore;
}
}
Stats.prototype.timeFormat = new Intl.DateTimeFormat("fr-FR", {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
timeZone: "UTC"
})
Stats.prototype.timeFormat = new Intl.DateTimeFormat('fr-FR', {
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
timeZone: 'UTC',
});
function playSound(sound, note=0) {
sound.currentTime = 0
sound.playbackRate = Math.pow(5/4, note)
sound.play()
function playSound(sound, note = 0) {
sound.currentTime = 0;
sound.playbackRate = Math.pow(5 / 4, note);
sound.play();
}