314 lines
8.8 KiB
JavaScript
314 lines
8.8 KiB
JavaScript
|
||
// ==========================
|
||
// VÄRIMYRSKY CMYK – p5.js
|
||
// Kosketusystävällinen refleksipeli messukioskille
|
||
// ==========================
|
||
let state = 'menu'; // menu | playing | gameover
|
||
let duration = 60; // kierros sekunteina
|
||
let startMillis = 0;
|
||
let timeLeft = duration;
|
||
let score = 0;
|
||
let roundN = 0;
|
||
|
||
let target = null;
|
||
let options = [];
|
||
let optionBoxes = [];
|
||
const OPTIONS_COUNT = 6;
|
||
const CMYK_STEPS = [0, 25, 50, 75, 100]; // helpottaa valintoja
|
||
|
||
let clickSound, goodSound, badSound;
|
||
let muted = true;
|
||
|
||
let font;
|
||
|
||
function preload(){
|
||
// Kevyet piippisoundit (WebAudio, luodaan lennossa)
|
||
font = loadFont("FuturaBQ-DemiBold.otf");
|
||
}
|
||
|
||
function setup(){
|
||
createCanvas(windowWidth, windowHeight);
|
||
textFont(font);
|
||
textAlign(CENTER, CENTER);
|
||
noStroke();
|
||
refreshLeaderboard();
|
||
|
||
// UI-napit
|
||
select('#btnStart').mousePressed(() => startGame());
|
||
select('#btnFull').mousePressed(() => toggleFullscreen());
|
||
select('#btnMute').mousePressed(() => toggleMute());
|
||
|
||
print("setup");
|
||
}
|
||
|
||
function windowResized(){
|
||
resizeCanvas(windowWidth, windowHeight);
|
||
}
|
||
|
||
function startGame(){
|
||
score = 0; roundN = 0; state='playing'; startMillis = millis();
|
||
nextRound();
|
||
}
|
||
|
||
function endGame(){
|
||
state='gameover';
|
||
// Nimi- / nimikirjainkysely. Kioskilla voi ohittaa.
|
||
const name = prompt('Syötä nimikirjaimet leaderboardille (valinnainen):', '') || '—';
|
||
pushScore(name.slice(0,10), score);
|
||
refreshLeaderboard();
|
||
}
|
||
|
||
function draw(){
|
||
background('#0b0b0d');
|
||
|
||
if(state==='menu'){
|
||
drawTitle();
|
||
}
|
||
else if(state==='playing'){
|
||
timeLeft = max(0, duration - floor((millis()-startMillis)/1000));
|
||
if(timeLeft<=0){ endGame(); return; }
|
||
drawHUD();
|
||
drawTarget();
|
||
drawOptions();
|
||
}
|
||
else if(state==='gameover'){
|
||
drawGameOver();
|
||
}
|
||
}
|
||
|
||
function drawTitle(){
|
||
fill(255);
|
||
const t1 = 'VÄRIMYRSKY CMYK';
|
||
const t2 = 'Napauta sitä värinäytettä, joka vastaa parhaiten annettua CMYK-arvoa.';
|
||
textSize(min(width, height) * 0.10);
|
||
text(t1, width/2, height*0.33);
|
||
fill(200);
|
||
textSize(min(width, height) * 0.03);
|
||
text(t2, width/2, height*0.45, width*0.9);
|
||
fill(180);
|
||
text('Aloita – 60 s kierros', width/2, height*0.6);
|
||
}
|
||
|
||
function drawHUD(){
|
||
// Yläpalkki: aika ja pisteet
|
||
const pad = 20;
|
||
const barH = 36;
|
||
const timeFrac = timeLeft / duration;
|
||
|
||
// tausta
|
||
fill(20,20,26,220); rect(pad, pad, width - pad*2, barH, 12);
|
||
// aika-progress
|
||
fill(70,140,255,220); rect(pad+2, pad+2, (width - pad*2 - 4) * timeFrac, barH-4, 10);
|
||
|
||
// teksti
|
||
fill(255); textSize(20);
|
||
textAlign(LEFT, CENTER);
|
||
text('Aika: ' + nf(timeLeft,2) + ' s', pad+16, pad+barH/2);
|
||
textAlign(RIGHT, CENTER);
|
||
text('Pisteet: ' + score, width - pad - 16, pad+barH/2);
|
||
textAlign(CENTER, CENTER);
|
||
}
|
||
|
||
function drawTarget(){
|
||
const areaH = height * 0.42;
|
||
const boxW = min(width*0.7, 900);
|
||
const boxH = areaH * 0.7;
|
||
const x = width/2 - boxW/2;
|
||
const y = height*0.12 + 12;
|
||
|
||
// Tausta
|
||
fill(28,28,36); rect(x-12, y-12, boxW+24, boxH+24, 20);
|
||
|
||
// Väri
|
||
fill(rgbFromCMYK(target)); rect(x, y, boxW, boxH, 16);
|
||
|
||
// CMYK-arvo teksti
|
||
fill(255); textSize(min(width, height)*0.035);
|
||
const t = `Etsi: C ${target.c}% M ${target.m}% Y ${target.y}% K ${target.k}%`;
|
||
text(t, width/2, y + boxH + 36);
|
||
}
|
||
|
||
function drawOptions(){
|
||
const gridRows = 2;
|
||
const gridCols = 3;
|
||
const gapX = 16;
|
||
const gapY = 48;
|
||
const areaTop = height*0.6;
|
||
const cellW = (width - gapX*(gridCols+1)) / gridCols;
|
||
const cellH = (height*0.36 - gapY*(gridRows+1)) / gridRows;
|
||
|
||
optionBoxes = [];
|
||
let idx = 0;
|
||
for(let r=0; r<gridRows; r++){
|
||
for(let c=0; c<gridCols; c++){
|
||
const ox = gapX + c*(cellW+gapX);
|
||
const oy = areaTop + gapY + r*(cellH+gapY);
|
||
const opt = options[idx];
|
||
// laatikon tausta
|
||
fill(28,28,36); rect(ox-8, oy-8, cellW+16, cellH+16, 18);
|
||
// väri
|
||
fill(rgbFromCMYK(opt)); rect(ox, oy, cellW, cellH, 14);
|
||
// label
|
||
fill(255); textSize(min(width, height)*0.025);
|
||
text(`C ${opt.c}% M ${opt.m}% Y ${opt.y}% K ${opt.k}%`, ox+cellW/2, oy+cellH+22);
|
||
optionBoxes.push({x:ox, y:oy, w:cellW, h:cellH, idx});
|
||
idx++;
|
||
}
|
||
}
|
||
}
|
||
|
||
function drawGameOver(){
|
||
fill(255);
|
||
textSize(min(width, height) * 0.095);
|
||
text('Aika loppui!', width/2, height*0.35);
|
||
fill(200);
|
||
textSize(min(width, height) * 0.04);
|
||
text('Pisteesi: ' + score, width/2, height*0.48);
|
||
fill(180);
|
||
text('Aloita uudestaan painamalla!', width/2, height*0.6);
|
||
}
|
||
|
||
function mousePressed(){
|
||
if(state==='menu' || state==='gameover'){ startGame(); return; }
|
||
if(state!=='playing') return;
|
||
|
||
// Tarkista osumat laatikoihin
|
||
for(const box of optionBoxes){
|
||
if(mouseX>=box.x && mouseX<=box.x+box.w && mouseY>=box.y && mouseY<=box.y+box.h){
|
||
handleSelection(box.idx);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
function touchStarted(){
|
||
// Estä sivun vieritys kioskissa
|
||
return false;
|
||
}
|
||
|
||
function handleSelection(i){
|
||
const chosen = options[i];
|
||
const distChosen = rgbDist(rgbFromCMYK(chosen), rgbFromCMYK(target));
|
||
const bestIndex = getBestMatchIndex();
|
||
|
||
if(i === bestIndex){
|
||
playGood();
|
||
score += 10;
|
||
} else {
|
||
playBad();
|
||
score = max(0, score - 5);
|
||
}
|
||
roundN++;
|
||
nextRound();
|
||
}
|
||
|
||
function nextRound(){
|
||
target = randomTarget();
|
||
options = buildOptions(target, OPTIONS_COUNT);
|
||
}
|
||
|
||
// ------- Väriapuja -------
|
||
function randomTarget(){
|
||
// Valitaan arvo askelista, jotta numerot pysyvät siisteinä
|
||
return {
|
||
c: randomChoice(CMYK_STEPS),
|
||
m: randomChoice(CMYK_STEPS),
|
||
y: randomChoice(CMYK_STEPS),
|
||
k: randomChoice([0,10,20,30,40,50,60,70,80,90,100])
|
||
};
|
||
}
|
||
|
||
function randomChoice(arr){ return arr[floor(random(arr.length))]; }
|
||
|
||
function buildOptions(target, n){
|
||
let arr = [];
|
||
// sijoita täydellinen vastaus
|
||
arr.push({...target});
|
||
// tee häiritsijöitä:
|
||
while(arr.length < n){
|
||
const delta = [0, 10, 20, -10, -20];
|
||
const c = clamp(target.c + randomChoice(delta), 0, 100);
|
||
const m = clamp(target.m + randomChoice(delta), 0, 100);
|
||
const y = clamp(target.y + randomChoice(delta), 0, 100);
|
||
const k = clamp(target.k + randomChoice([-10, 0, 10, 20, -20]), 0, 100);
|
||
const opt = {c,m,y,k};
|
||
if(!arr.some(o => o.c===opt.c && o.m===opt.m && o.y===opt.y && o.k===opt.k)){
|
||
arr.push(opt);
|
||
}
|
||
}
|
||
// sekoita
|
||
for(let i=arr.length-1;i>0;i--){ const j=floor(random(i+1)); [arr[i],arr[j]]=[arr[j],arr[i]]; }
|
||
return arr;
|
||
}
|
||
|
||
function clamp(v, lo, hi){ return max(lo, min(hi, v)); }
|
||
|
||
function cmykToRgb(cmyk){
|
||
// cmyk prosentteina (0..100)
|
||
const C = cmyk.c/100, M = cmyk.m/100, Y = cmyk.y/100, K = cmyk.k/100;
|
||
const r = 255 * (1-C) * (1-K);
|
||
const g = 255 * (1-M) * (1-K);
|
||
const b = 255 * (1-Y) * (1-K);
|
||
return {r, g, b};
|
||
}
|
||
|
||
function rgbFromCMYK(cmyk){ const {r,g,b} = cmykToRgb(cmyk); return color(r, g, b); }
|
||
|
||
function rgbDist(a, b){
|
||
// a ja b voivat olla p5.Color tai plain rgb-objekti
|
||
const ar = red(a), ag = green(a), ab = blue(a);
|
||
const br = red(b), bg = green(b), bb = blue(b);
|
||
const dr = ar-br, dg = ag-bg, db = ab-bb;
|
||
return dr*dr + dg*dg + db*db;
|
||
}
|
||
|
||
function getBestMatchIndex(){
|
||
let best = Infinity, idx = 0;
|
||
const t = rgbFromCMYK(target);
|
||
for(let i=0;i<options.length;i++){
|
||
const d = rgbDist(rgbFromCMYK(options[i]), t);
|
||
if(d < best){ best = d; idx = i; }
|
||
}
|
||
return idx;
|
||
}
|
||
|
||
// ------- Äänet (valinnaiset, hyvin kevyet) -------
|
||
let ac; // AudioContext
|
||
function ensureAC(){ if(!ac){ const C = window.AudioContext||window.webkitAudioContext; ac = new C(); } }
|
||
function beep(freq, dur, type='sine', vol=0.05){ if(muted) return; ensureAC(); const t=ac.currentTime; const o=ac.createOscillator(); const g=ac.createGain(); o.type=type; o.frequency.value=freq; o.connect(g); g.connect(ac.destination); g.gain.value = vol; o.start(t); o.stop(t+dur); }
|
||
function playGood(){ beep(880, .08, 'square'); beep(1320, .10, 'square'); }
|
||
function playBad(){ beep(130, .12, 'sawtooth'); }
|
||
function toggleMute(){ muted = !muted; if(!muted) ensureAC(); }
|
||
|
||
// ------- Fullscreen -------
|
||
function toggleFullscreen(){ let fs = fullscreen(); fullscreen(!fs); }
|
||
|
||
// ------- Leaderboard (localStorage) -------
|
||
const LB_KEY = 'cmyk_lb_v1';
|
||
function pushScore(name, score){
|
||
const now = new Date();
|
||
const dayKey = now.toISOString().slice(0,10); // YYYY-MM-DD
|
||
const all = JSON.parse(localStorage.getItem(LB_KEY) || '{}');
|
||
if(!all[dayKey]) all[dayKey] = [];
|
||
all[dayKey].push({name, score});
|
||
all[dayKey].sort((a,b)=>b.score-a.score);
|
||
all[dayKey] = all[dayKey].slice(0,10);
|
||
localStorage.setItem(LB_KEY, JSON.stringify(all));
|
||
}
|
||
|
||
function refreshLeaderboard(){
|
||
const lb = document.getElementById('lb');
|
||
lb.innerHTML = '';
|
||
const all = JSON.parse(localStorage.getItem(LB_KEY) || '{}');
|
||
const dayKey = new Date().toISOString().slice(0,10);
|
||
const today = all[dayKey] || [];
|
||
today.forEach((row, i)=>{
|
||
const li = document.createElement('li');
|
||
li.textContent = `${i+1}. ${row.name} — ${row.score}`;
|
||
lb.appendChild(li);
|
||
});
|
||
}
|
||
|
||
// Estä pitkäpainallus- ja kontekstivalikko kioskissa
|
||
window.addEventListener('contextmenu', e=> e.preventDefault());
|