update
This commit is contained in:
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
+6
-3
@@ -46,7 +46,8 @@ export function createFamily() {
|
||||
shape(torso, new THREE.CylinderGeometry(.075 * scale, .09 * scale, .18 * scale, 12), skin, [0, 1.59 * scale, 0]);
|
||||
const seam = shape(torso, new THREE.BoxGeometry(.014 * scale, .5 * scale, .014 * scale), mat('#bda77f'), [0, 1.2 * scale, .19 * scale]);
|
||||
for (const side of [-1, 1]) {
|
||||
const collar=shape(torso,new THREE.BoxGeometry(.12*scale,.09*scale,.025*scale),cloth,[side*.07*scale,1.49*scale,.15*scale]);collar.rotation.z=side*.4;
|
||||
const collar = shape(torso, new THREE.BoxGeometry(.12 * scale, .09 * scale, .025 * scale), cloth, [side * .07 * scale, 1.49 * scale, .15 * scale]);
|
||||
collar.rotation.z = side * .4;
|
||||
}
|
||||
const head = new THREE.Group();
|
||||
head.position.y = 1.77 * scale;
|
||||
@@ -59,8 +60,10 @@ export function createFamily() {
|
||||
for (const side of [-1, 1]) {
|
||||
sphere(head, .06 * scale, skin, [side * .26 * scale, 0, 0]);
|
||||
sphere(head, .037 * scale, mat('#f6f1e7'), [side * .09 * scale, .03 * scale, .241 * scale], [1, .65, .5]);
|
||||
const eye=sphere(head,.022*scale,shoes,[side*.09*scale,.03*scale,.258*scale],[.8,1,.45]);eyes.push(eye);
|
||||
const brow=shape(head,new THREE.BoxGeometry(.075*scale,.015*scale,.015*scale),hair,[side*.09*scale,.087*scale,.237*scale]);brow.rotation.z=side*-.08;
|
||||
const eye = sphere(head, .022 * scale, shoes, [side * .09 * scale, .03 * scale, .258 * scale], [.8, 1, .45]);
|
||||
eyes.push(eye);
|
||||
const brow = shape(head, new THREE.BoxGeometry(.075 * scale, .015 * scale, .015 * scale), hair, [side * .09 * scale, .087 * scale, .237 * scale]);
|
||||
brow.rotation.z = side * -.08;
|
||||
sphere(head, .04 * scale, mat('#d79379'), [side * .15 * scale, -.055 * scale, .215 * scale]);
|
||||
}
|
||||
const smile = shape(head, new THREE.TorusGeometry(.068 * scale, .012 * scale, 6, 14, Math.PI), shoes, [0, -.075 * scale, .252 * scale]);
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@ export function movementDirection(pressed, held) {
|
||||
return Number(pressed.has('arrowright') || pressed.has('d') || held.has(1))
|
||||
- Number(pressed.has('arrowleft') || pressed.has('a') || held.has(-1));
|
||||
}
|
||||
|
||||
export function canWalk({shopOpen = false, notebookOpen = false, endingOpen = false, busy = false, finished = false}) {
|
||||
return !shopOpen && !notebookOpen && !endingOpen && !busy && !finished;
|
||||
}
|
||||
|
||||
+9
-1
@@ -17,7 +17,15 @@ const assortment = [
|
||||
// All 60 physical channels exist; only 21–60 are available in this scene.
|
||||
export const channels = Array.from({length: 60}, (_, index) => {
|
||||
const channel = index + 1, base = assortment[(channel - 22 + 63) % assortment.length];
|
||||
return {...base,baseId:base.id,id:`channel-${channel}`,code:String(channel),channel,cost:base.cost/1000,available:channel>=21};
|
||||
return {
|
||||
...base,
|
||||
baseId: base.id,
|
||||
id: `channel-${channel}`,
|
||||
code: String(channel),
|
||||
channel,
|
||||
cost: base.cost / 1000,
|
||||
available: channel >= 21
|
||||
};
|
||||
});
|
||||
export const products = channels.filter(channel => channel.available);
|
||||
|
||||
|
||||
+560
-103
@@ -73,6 +73,7 @@ function random() {
|
||||
}
|
||||
|
||||
const trees = [];
|
||||
|
||||
function tree(x, z, s) {
|
||||
const g = new THREE.Group();
|
||||
g.position.set(x, 0, z);
|
||||
@@ -118,23 +119,43 @@ box(1.13, 1.72, .1, material('#223f3b'), machine, [-.13, 1.55, .65]);
|
||||
const bottleMats = ['#e5ae47', '#badd81', '#c8704e'].map(c => material(c));
|
||||
const steel = material('#aeb8b6', {metalness: .8, roughness: .3});
|
||||
for (let row = 0; row < 6; row++) {
|
||||
const shelfY=.86+row*.275;box(1.17,.025,.28,steel,machine,[-.13,shelfY,.73]);
|
||||
const shelfY = .86 + row * .275;
|
||||
box(1.17, .025, .28, steel, machine, [-.13, shelfY, .73]);
|
||||
for (let col = 0; col < 10; col++) {
|
||||
const channel = (5 - row) * 10 + col + 1, px = -.65 + col * .115;
|
||||
if(channel%3){mesh(new THREE.CylinderGeometry(.033,.04,.155,10),bottleMats[channel%3],machine,[px,shelfY+.105,.73]);box(.035,.035,.035,dark,machine,[px,shelfY+.195,.73]);}
|
||||
else box(.068,.14,.07,bottleMats[channel%3],machine,[px,shelfY+.09,.73]);
|
||||
if (channel % 3) {
|
||||
mesh(new THREE.CylinderGeometry(.033, .04, .155, 10), bottleMats[channel % 3], machine, [px, shelfY + .105, .73]);
|
||||
box(.035, .035, .035, dark, machine, [px, shelfY + .195, .73]);
|
||||
} else box(.068, .14, .07, bottleMats[channel % 3], machine, [px, shelfY + .09, .73]);
|
||||
// Each slot has its own dispensing spiral.
|
||||
const spiral = mesh(new THREE.TorusGeometry(.035, .006, 4, 10), steel, machine, [px, shelfY + .045, .79]);
|
||||
}
|
||||
}
|
||||
const slots=document.createElement('canvas');slots.width=1000;slots.height=900;
|
||||
const slotContext=slots.getContext('2d');slotContext.textAlign='center';slotContext.font='bold 25px sans-serif';
|
||||
const slots = document.createElement('canvas');
|
||||
slots.width = 1000;
|
||||
slots.height = 900;
|
||||
const slotContext = slots.getContext('2d');
|
||||
slotContext.textAlign = 'center';
|
||||
slotContext.font = 'bold 25px sans-serif';
|
||||
for (let row = 0; row < 6; row++) for (let col = 0; col < 10; col++) {
|
||||
const n=row*10+col+1;slotContext.fillStyle=n>=21?'#d1f0b3':'#9ca6a0';slotContext.fillRect(col*100+8,row*150+120,84,30);slotContext.fillStyle='#172c2a';slotContext.fillText(String(n),col*100+50,row*150+144);
|
||||
const n = row * 10 + col + 1;
|
||||
slotContext.fillStyle = n >= 21 ? '#d1f0b3' : '#9ca6a0';
|
||||
slotContext.fillRect(col * 100 + 8, row * 150 + 120, 84, 30);
|
||||
slotContext.fillStyle = '#172c2a';
|
||||
slotContext.fillText(String(n), col * 100 + 50, row * 150 + 144);
|
||||
}
|
||||
const slotLabels=mesh(new THREE.PlaneGeometry(1.16,1.65),new THREE.MeshBasicMaterial({map:new THREE.CanvasTexture(slots),transparent:true}),machine,[-.13,1.56,.82]);
|
||||
const slotLabels = mesh(new THREE.PlaneGeometry(1.16, 1.65), new THREE.MeshBasicMaterial({
|
||||
map: new THREE.CanvasTexture(slots),
|
||||
transparent: true
|
||||
}), machine, [-.13, 1.56, .82]);
|
||||
for (const px of [-.77, .48]) box(.025, 1.85, .08, steel, machine, [px, 1.55, .82]);
|
||||
const glass=mesh(new THREE.PlaneGeometry(1.24,1.86),material('#d5eee9',{transparent:true,opacity:.1,metalness:.3,roughness:.08,depthWrite:false}),machine,[-.13,1.55,.84]);
|
||||
const glass = mesh(new THREE.PlaneGeometry(1.24, 1.86), material('#d5eee9', {
|
||||
transparent: true,
|
||||
opacity: .1,
|
||||
metalness: .3,
|
||||
roughness: .08,
|
||||
depthWrite: false
|
||||
}), machine, [-.13, 1.55, .84]);
|
||||
box(.04, .5, .06, steel, machine, [.52, 1.4, .88]);
|
||||
for (let r = 0; r < 4; r++) for (let c = 0; c < 3; c++) box(.045, .045, .035, steel, machine, [.60 + c * .065, 1.43 - r * .07, .64]);
|
||||
for (let i = 0; i < 6; i++) box(.24, .014, .025, steel, machine, [.67, .55 + i * .045, .58]);
|
||||
@@ -156,117 +177,344 @@ const sign = material('#ffffff', {map: new THREE.CanvasTexture(label), emissive:
|
||||
box(1.5, .38, .05, sign, machine, [0, 2.68, .58]);
|
||||
// A separate card, animated through the reader only after an explicit swipe.
|
||||
const bankCard = box(.24, .15, .014, material('#ddd4b4', {metalness: .2}), machine, [.9, 1.8, .8]);
|
||||
box(.2,.03,.016,dark,bankCard,[0,.035,.008]);bankCard.visible=false;
|
||||
box(.2, .03, .016, dark, bankCard, [0, .035, .008]);
|
||||
bankCard.visible = false;
|
||||
const pad = mesh(new THREE.CylinderGeometry(2.3, 2.4, .12, 32), material('#9caa80'), scene, [9, .05, stationZ]);
|
||||
const delivery = new THREE.Group(); scene.add(delivery);
|
||||
const delivery = new THREE.Group();
|
||||
scene.add(delivery);
|
||||
const deliveryBody = mesh(new THREE.CylinderGeometry(.1, .1, .28, 10), bottleMats[0], delivery);
|
||||
box(.09, .05, .09, dark, delivery, [0, .16, 0]);
|
||||
delivery.visible = false;
|
||||
|
||||
// Picnic clearing, checked blanket, basket and a recycling bin.
|
||||
const picnic=new THREE.Group();picnic.position.set(23,1.8,pathZ(22));scene.add(picnic);
|
||||
const picnic = new THREE.Group();
|
||||
picnic.position.set(23, 1.8, pathZ(22));
|
||||
scene.add(picnic);
|
||||
for (let i = 0; i < 6; i++) for (let j = 0; j < 5; j++) box(.5, .035, .5, material((i + j) % 2 ? '#f3dca9' : '#b96f51'), picnic, [(i - 2.5) * .5, .08, (j - 2) * .5]);
|
||||
const picnicFood=new THREE.Group();picnic.add(picnicFood);
|
||||
const picnicFood = new THREE.Group();
|
||||
picnic.add(picnicFood);
|
||||
box(.5, .3, .35, material('#ac8252'), picnic, [-1, .25, -.8]);
|
||||
mesh(new THREE.TorusGeometry(.22, .035, 6, 16, Math.PI), bark, picnic, [-1, .5, -.8]);
|
||||
const bin = box(.55, .8, .55, material('#39735b'), scene, [24, 2.2, pathZ(22) - 2.3]);
|
||||
box(.65, .08, .65, dark, scene, [24, 2.64, pathZ(22) - 2.3]);
|
||||
const flap=new THREE.Group();flap.position.set(-.13,.53,.69);machine.add(flap);
|
||||
const flap = new THREE.Group();
|
||||
flap.position.set(-.13, .53, .69);
|
||||
machine.add(flap);
|
||||
box(.87, .28, .035, material('#527268', {metalness: .45}), flap, [0, -.14, 0]);
|
||||
const glow=new THREE.PointLight('#a8ffc5',2,7);glow.position.set(9,2,stationZ+1);scene.add(glow);
|
||||
const butterfly=new THREE.Group();scene.add(butterfly);
|
||||
const wings=[-1,1].map(side=>{const wing=mesh(new THREE.SphereGeometry(.16,8,6),material('#efb75b'),butterfly,[side*.14,0,0]);wing.scale.set(1,.12,1.4);return wing;});
|
||||
const glow = new THREE.PointLight('#a8ffc5', 2, 7);
|
||||
glow.position.set(9, 2, stationZ + 1);
|
||||
scene.add(glow);
|
||||
const butterfly = new THREE.Group();
|
||||
scene.add(butterfly);
|
||||
const wings = [-1, 1].map(side => {
|
||||
const wing = mesh(new THREE.SphereGeometry(.16, 8, 6), material('#efb75b'), butterfly, [side * .14, 0, 0]);
|
||||
wing.scale.set(1, .12, 1.4);
|
||||
return wing;
|
||||
});
|
||||
// A raised footbridge crosses a shallow stream before the vending stop.
|
||||
const bridgeX = 0;
|
||||
const water=mesh(new THREE.PlaneGeometry(3.4,40),material('#68b4b6',{metalness:.3,roughness:.25}),scene,[bridgeX,.035,0]);water.rotation.x=-Math.PI/2;
|
||||
const bridge=new THREE.Group();scene.add(bridge);
|
||||
const water = mesh(new THREE.PlaneGeometry(3.4, 40), material('#68b4b6', {
|
||||
metalness: .3,
|
||||
roughness: .25
|
||||
}), scene, [bridgeX, .035, 0]);
|
||||
water.rotation.x = -Math.PI / 2;
|
||||
const bridge = new THREE.Group();
|
||||
scene.add(bridge);
|
||||
const wood = material('#a57b50');
|
||||
for(let i=0;i<13;i++){const bx=-2+i/3;box(.29,.14,2.8,wood,bridge,[bx,.22,pathZ(bx)]);}
|
||||
for (let i = 0; i < 13; i++) {
|
||||
const bx = -2 + i / 3;
|
||||
box(.29, .14, 2.8, wood, bridge, [bx, .22, pathZ(bx)]);
|
||||
}
|
||||
for (const side of [-1, 1]) {
|
||||
for (const bx of [-2, 0, 2]) box(.12, 1, .12, bark, bridge, [bx, .65, pathZ(bx) + side * 1.3]);
|
||||
const rail=box(4.3,.1,.1,wood,bridge,[0,1.08,side*1.3]);rail.rotation.y=-.25;
|
||||
const rail = box(4.3, .1, .1, wood, bridge, [0, 1.08, side * 1.3]);
|
||||
rail.rotation.y = -.25;
|
||||
}
|
||||
function pathHeight(at){return Math.max(0,Math.min(1,(2.6-Math.abs(at))/.6))*.3+THREE.MathUtils.smoothstep(at,17,22)*1.8;}
|
||||
const rabbit=new THREE.Group();scene.add(rabbit);
|
||||
|
||||
function pathHeight(at) {
|
||||
return Math.max(0, Math.min(1, (2.6 - Math.abs(at)) / .6)) * .3 + THREE.MathUtils.smoothstep(at, 17, 22) * 1.8;
|
||||
}
|
||||
|
||||
const rabbit = new THREE.Group();
|
||||
scene.add(rabbit);
|
||||
const fur = material('#eee2ca'), pink = material('#d5a193');
|
||||
const rabbitBody=ball(.28,fur,rabbit,[0,.3,0]);rabbitBody.scale.set(1.3,1,1);
|
||||
ball(.22,fur,rabbit,[.24,.55,0]);ball(.12,fur,rabbit,[-.35,.4,0]);
|
||||
for(const side of [-1,1]){const ear=ball(.1,fur,rabbit,[.23,.86,side*.1]);ear.scale.set(.75,2.5,.75);const inset=ball(.065,pink,rabbit,[.29,.88,side*.1]);inset.scale.set(.4,2.6,.7);ball(.025,dark,rabbit,[.43,.58,side*.13]);ball(.09,fur,rabbit,[.1,.1,side*.2]);}
|
||||
const lostPaper=box(.6,.025,.43,material('#fff3d5'),scene,[14,.35,Math.sin(14*.17)*1.5+1.5]);lostPaper.rotation.z=.15;
|
||||
const rabbitBody = ball(.28, fur, rabbit, [0, .3, 0]);
|
||||
rabbitBody.scale.set(1.3, 1, 1);
|
||||
ball(.22, fur, rabbit, [.24, .55, 0]);
|
||||
ball(.12, fur, rabbit, [-.35, .4, 0]);
|
||||
for (const side of [-1, 1]) {
|
||||
const ear = ball(.1, fur, rabbit, [.23, .86, side * .1]);
|
||||
ear.scale.set(.75, 2.5, .75);
|
||||
const inset = ball(.065, pink, rabbit, [.29, .88, side * .1]);
|
||||
inset.scale.set(.4, 2.6, .7);
|
||||
ball(.025, dark, rabbit, [.43, .58, side * .13]);
|
||||
ball(.09, fur, rabbit, [.1, .1, side * .2]);
|
||||
}
|
||||
const lostPaper = box(.6, .025, .43, material('#fff3d5'), scene, [14, .35, Math.sin(14 * .17) * 1.5 + 1.5]);
|
||||
lostPaper.rotation.z = .15;
|
||||
const bush = ball(.55, material('#688a4b'), scene, [14, .3, Math.sin(14 * .17) * 1.5 + 2]);
|
||||
const oldTree=new THREE.Group();oldTree.position.set(19,0,-5);scene.add(oldTree);mesh(new THREE.CylinderGeometry(.5,.8,4,9),bark,oldTree,[0,2,0]);ball(2,material('#54734a'),oldTree,[0,4.8,0]);
|
||||
const hill=mesh(new THREE.SphereGeometry(5,24,16),green,scene,[23,-1.3,0]);hill.scale.set(1,.65,1.3);
|
||||
const oldTree = new THREE.Group();
|
||||
oldTree.position.set(19, 0, -5);
|
||||
scene.add(oldTree);
|
||||
mesh(new THREE.CylinderGeometry(.5, .8, 4, 9), bark, oldTree, [0, 2, 0]);
|
||||
ball(2, material('#54734a'), oldTree, [0, 4.8, 0]);
|
||||
const hill = mesh(new THREE.SphereGeometry(5, 24, 16), green, scene, [23, -1.3, 0]);
|
||||
hill.scale.set(1, .65, 1.3);
|
||||
let branchPath = null;
|
||||
|
||||
function rebuildRoute() {
|
||||
if(branchPath){scene.remove(branchPath);branchPath.geometry.dispose();branchPath.material.dispose();}
|
||||
const pts=[];for(let at=11;at<=24;at+=.15)pts.push(new THREE.Vector3(at,pathHeight(at)+.045,pathZ(at)));
|
||||
if (branchPath) {
|
||||
scene.remove(branchPath);
|
||||
branchPath.geometry.dispose();
|
||||
branchPath.material.dispose();
|
||||
}
|
||||
const pts = [];
|
||||
for (let at = 11; at <= 24; at += .15) pts.push(new THREE.Vector3(at, pathHeight(at) + .045, pathZ(at)));
|
||||
branchPath = mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 80, .8, 8, false), material('#cbb889'));
|
||||
// Flatten only the cross-section, retaining the hill's elevation.
|
||||
const positions = branchPath.geometry.attributes.position;
|
||||
for(let i=0;i<positions.count;i++){const px=positions.getX(i);positions.setY(i,pathHeight(px)+.045);}
|
||||
positions.needsUpdate=true;branchPath.geometry.computeVertexNormals();
|
||||
for (let i = 0; i < positions.count; i++) {
|
||||
const px = positions.getX(i);
|
||||
positions.setY(i, pathHeight(px) + .045);
|
||||
}
|
||||
positions.needsUpdate = true;
|
||||
branchPath.geometry.computeVertexNormals();
|
||||
}
|
||||
|
||||
rebuildRoute();
|
||||
const fishes=[];for(let i=0;i<4;i++){const fish=ball(.09,material('#e8b365'),scene,[.4,.11,-.6+i*.4]);fish.scale.set(2,.5,1);fishes.push(fish);}
|
||||
const fishes = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const fish = ball(.09, material('#e8b365'), scene, [.4, .11, -.6 + i * .4]);
|
||||
fish.scale.set(2, .5, 1);
|
||||
fishes.push(fish);
|
||||
}
|
||||
let encounterTime = 0, rabbitSeen = false, encounterActive = false, facing = Math.PI / 2;
|
||||
const $ = s => document.querySelector(s), shop = $('#shop'), ending = $('#ending');
|
||||
const storyDialog = $('#story-dialog'), notebook = $('#notebook');
|
||||
|
||||
function renderChapter() {
|
||||
const chapter=chapters[chapterId];$('#chapter-title').textContent=chapter.title;$('#chapter-line').textContent=chapter.lines[chapterLine];$('#chapter-count').textContent=`${chapterLine+1} / ${chapter.lines.length}`;$('#chapter-actions').replaceChildren();
|
||||
const chapter = chapters[chapterId];
|
||||
$('#chapter-title').textContent = chapter.title;
|
||||
$('#chapter-line').textContent = chapter.lines[chapterLine];
|
||||
$('#chapter-count').textContent = `${chapterLine + 1} / ${chapter.lines.length}`;
|
||||
$('#chapter-actions').replaceChildren();
|
||||
const last = chapterLine === chapter.lines.length - 1;
|
||||
if(last&&chapter.action){const b=document.createElement('button');b.type='button';b.textContent=`${chapter.action.label} · F`;b.onclick=performStoryAction;$('#chapter-actions').append(b);}
|
||||
if (last && chapter.action) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.textContent = `${chapter.action.label} · F`;
|
||||
b.onclick = performStoryAction;
|
||||
$('#chapter-actions').append(b);
|
||||
}
|
||||
chapterTimer = Math.max(2.5, chapter.lines[chapterLine].length * .055);
|
||||
}
|
||||
|
||||
function performStoryAction() {
|
||||
if (!chapterId || shop.open || notebook.open || ending.open || state.busy) return;
|
||||
const chapter = chapters[chapterId];
|
||||
if (chapter.action && chapterLine === chapter.lines.length - 1) finishChapter(chapter.action.value);
|
||||
}
|
||||
function showChapter(id){chapterId=id;chapterLine=0;storyDialog.hidden=false;renderChapter();}
|
||||
|
||||
function showChapter(id) {
|
||||
chapterId = id;
|
||||
chapterLine = 0;
|
||||
storyDialog.hidden = false;
|
||||
renderChapter();
|
||||
}
|
||||
|
||||
function finishChapter(choice) {
|
||||
const id=chapterId;if(!completeChapter(story,id,choice))return;
|
||||
if(id==='rabbit'){rabbitSeen=true;encounterTime=0;}
|
||||
const id = chapterId;
|
||||
if (!completeChapter(story, id, choice)) return;
|
||||
if (id === 'rabbit') {
|
||||
rabbitSeen = true;
|
||||
encounterTime = 0;
|
||||
}
|
||||
if (id === 'fork' || id === 'wind') rebuildRoute();
|
||||
storyDialog.hidden=true;chapterId=null;syncHUD();
|
||||
if(id==='summit'){drawMemory($('#memory'));$('#ending-discoveries').textContent=discoveries(story).join(' · ');ending.showModal();}
|
||||
storyDialog.hidden = true;
|
||||
chapterId = null;
|
||||
syncHUD();
|
||||
if (id === 'summit') {
|
||||
drawMemory($('#memory'));
|
||||
$('#ending-discoveries').textContent = discoveries(story).join(' · ');
|
||||
ending.showModal();
|
||||
}
|
||||
}
|
||||
|
||||
function drawMemory(canvas) {
|
||||
const c=canvas.getContext('2d');c.clearRect(0,0,600,360);c.fillStyle=story.sky==='gold'?'#f5d18b':story.sky==='rose'?'#eeb9ba':'#b9dfeb';c.fillRect(0,0,600,360);c.fillStyle='#fff0af';c.beginPath();c.arc(500,65,30,0,Math.PI*2);c.fill();c.fillStyle='#84a36b';c.beginPath();c.moveTo(0,240);c.quadraticCurveTo(160,90,320,235);c.quadraticCurveTo(460,120,600,210);c.lineTo(600,360);c.lineTo(0,360);c.fill();
|
||||
for(let i=0;i<5;i++){c.fillStyle='#6a553e';c.fillRect(30+i*115,180,8,75);c.fillStyle='#456b47';c.beginPath();c.moveTo(5+i*115,205);c.lineTo(34+i*115,125);c.lineTo(63+i*115,205);c.fill();}
|
||||
c.strokeStyle='#8a674d';c.lineWidth=8;c.beginPath();c.moveTo(75,278);c.lineTo(210,278);c.moveTo(85,263);c.lineTo(200,263);c.stroke();
|
||||
function person(px,py,size,color){c.fillStyle='#edbb94';c.beginPath();c.arc(px,py,size*.18,0,Math.PI*2);c.fill();c.strokeStyle=color;c.lineWidth=size*.16;c.beginPath();c.moveTo(px,py+size*.22);c.lineTo(px,py+size*.65);c.moveTo(px,py+size*.3);c.lineTo(px+size*.3,py+size*.48);c.moveTo(px,py+size*.65);c.lineTo(px-size*.15,py+size);c.moveTo(px,py+size*.65);c.lineTo(px+size*.15,py+size);c.stroke();}
|
||||
person(300,200,90,'#b28b48');person(347,235,58,'#c56f4d');c.strokeStyle='#d4a77d';c.lineWidth=5;c.beginPath();c.moveTo(327,243);c.lineTo(347,252);c.stroke();c.font='20px Tahoma';c.textAlign='center';c.fillStyle='#294b3c';c.fillText(story.sky?'من و بابا، تپهٔ آرزوها':'من و بابا… ادامه دارد',300,338);
|
||||
const c = canvas.getContext('2d');
|
||||
c.clearRect(0, 0, 600, 360);
|
||||
c.fillStyle = story.sky === 'gold' ? '#f5d18b' : story.sky === 'rose' ? '#eeb9ba' : '#b9dfeb';
|
||||
c.fillRect(0, 0, 600, 360);
|
||||
c.fillStyle = '#fff0af';
|
||||
c.beginPath();
|
||||
c.arc(500, 65, 30, 0, Math.PI * 2);
|
||||
c.fill();
|
||||
c.fillStyle = '#84a36b';
|
||||
c.beginPath();
|
||||
c.moveTo(0, 240);
|
||||
c.quadraticCurveTo(160, 90, 320, 235);
|
||||
c.quadraticCurveTo(460, 120, 600, 210);
|
||||
c.lineTo(600, 360);
|
||||
c.lineTo(0, 360);
|
||||
c.fill();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
c.fillStyle = '#6a553e';
|
||||
c.fillRect(30 + i * 115, 180, 8, 75);
|
||||
c.fillStyle = '#456b47';
|
||||
c.beginPath();
|
||||
c.moveTo(5 + i * 115, 205);
|
||||
c.lineTo(34 + i * 115, 125);
|
||||
c.lineTo(63 + i * 115, 205);
|
||||
c.fill();
|
||||
}
|
||||
$('#notebook-open').onclick=()=>{clearInput();$('#discoveries').textContent=discoveries(story).join(' · ')||'هنوز اول راهیم؛ کشفهای امروز اینجا ثبت میشن.';drawMemory($('#drawing'));notebook.showModal();};$('#notebook-close').onclick=()=>notebook.close();notebook.addEventListener('close',clearInput);
|
||||
c.strokeStyle = '#8a674d';
|
||||
c.lineWidth = 8;
|
||||
c.beginPath();
|
||||
c.moveTo(75, 278);
|
||||
c.lineTo(210, 278);
|
||||
c.moveTo(85, 263);
|
||||
c.lineTo(200, 263);
|
||||
c.stroke();
|
||||
|
||||
function person(px, py, size, color) {
|
||||
c.fillStyle = '#edbb94';
|
||||
c.beginPath();
|
||||
c.arc(px, py, size * .18, 0, Math.PI * 2);
|
||||
c.fill();
|
||||
c.strokeStyle = color;
|
||||
c.lineWidth = size * .16;
|
||||
c.beginPath();
|
||||
c.moveTo(px, py + size * .22);
|
||||
c.lineTo(px, py + size * .65);
|
||||
c.moveTo(px, py + size * .3);
|
||||
c.lineTo(px + size * .3, py + size * .48);
|
||||
c.moveTo(px, py + size * .65);
|
||||
c.lineTo(px - size * .15, py + size);
|
||||
c.moveTo(px, py + size * .65);
|
||||
c.lineTo(px + size * .15, py + size);
|
||||
c.stroke();
|
||||
}
|
||||
|
||||
person(300, 200, 90, '#b28b48');
|
||||
person(347, 235, 58, '#c56f4d');
|
||||
c.strokeStyle = '#d4a77d';
|
||||
c.lineWidth = 5;
|
||||
c.beginPath();
|
||||
c.moveTo(327, 243);
|
||||
c.lineTo(347, 252);
|
||||
c.stroke();
|
||||
c.font = '20px Tahoma';
|
||||
c.textAlign = 'center';
|
||||
c.fillStyle = '#294b3c';
|
||||
c.fillText(story.sky ? 'من و بابا، تپهٔ آرزوها' : 'من و بابا… ادامه دارد', 300, 338);
|
||||
}
|
||||
|
||||
$('#notebook-open').onclick = () => {
|
||||
clearInput();
|
||||
$('#discoveries').textContent = discoveries(story).join(' · ') || 'هنوز اول راهیم؛ کشفهای امروز اینجا ثبت میشن.';
|
||||
drawMemory($('#drawing'));
|
||||
notebook.showModal();
|
||||
};
|
||||
$('#notebook-close').onclick = () => notebook.close();
|
||||
notebook.addEventListener('close', clearInput);
|
||||
let payment = createPayment(), paymentTime = 0;
|
||||
let state = createState(), x = -8, moving = 0, gait = 0, deliveryTime = 0, activeProduct = null, elapsed = 0;
|
||||
const pressed = new Set(), held = new Set();
|
||||
const fa = n => n.toLocaleString('fa-IR');
|
||||
let audio = null, soundEnabled = false, nextBird = 0, zoomDistance = 1;
|
||||
function tone(frequency,duration=.12,volume=.04){if(!soundEnabled||!audio)return;const oscillator=audio.createOscillator(),gain=audio.createGain();oscillator.frequency.setValueAtTime(frequency,audio.currentTime);oscillator.frequency.exponentialRampToValueAtTime(frequency*.7,audio.currentTime+duration);gain.gain.setValueAtTime(volume,audio.currentTime);gain.gain.exponentialRampToValueAtTime(.001,audio.currentTime+duration);oscillator.connect(gain).connect(audio.destination);oscillator.start();oscillator.stop(audio.currentTime+duration);}
|
||||
$('#sound').onclick=async()=>{try{audio??=new AudioContext();await audio.resume();soundEnabled=!soundEnabled;$('#sound').textContent=soundEnabled?'صدا: روشن ♫':'صدا: خاموش ♫';$('#sound').setAttribute('aria-pressed',String(soundEnabled));tone(700);}catch{$('#sound').textContent='صدا در دسترس نیست';}};
|
||||
|
||||
function tone(frequency, duration = .12, volume = .04) {
|
||||
if (!soundEnabled || !audio) return;
|
||||
const oscillator = audio.createOscillator(), gain = audio.createGain();
|
||||
oscillator.frequency.setValueAtTime(frequency, audio.currentTime);
|
||||
oscillator.frequency.exponentialRampToValueAtTime(frequency * .7, audio.currentTime + duration);
|
||||
gain.gain.setValueAtTime(volume, audio.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(.001, audio.currentTime + duration);
|
||||
oscillator.connect(gain).connect(audio.destination);
|
||||
oscillator.start();
|
||||
oscillator.stop(audio.currentTime + duration);
|
||||
}
|
||||
|
||||
$('#sound').onclick = async () => {
|
||||
try {
|
||||
audio ??= new AudioContext();
|
||||
await audio.resume();
|
||||
soundEnabled = !soundEnabled;
|
||||
$('#sound').textContent = soundEnabled ? 'صدا: روشن ♫' : 'صدا: خاموش ♫';
|
||||
$('#sound').setAttribute('aria-pressed', String(soundEnabled));
|
||||
tone(700);
|
||||
} catch {
|
||||
$('#sound').textContent = 'صدا در دسترس نیست';
|
||||
}
|
||||
};
|
||||
|
||||
function syncHUD() {
|
||||
$('#balance').textContent = fa(state.balance);
|
||||
$('#basket').textContent = state.basket.length ? '🧺 ' + state.basket.map(p => p.icon).join(' ') : '🧺 سبد پیکنیک خالی است';
|
||||
$('#mission').textContent = !story.done.includes('courage') ? 'مسیر تپه را پیدا کنید' : !story.done.includes('paper') && story.done.includes('wind') ? 'نقاشی را کنار بوته پیدا کنید' : story.done.includes('paper') && !story.route ? 'برای نقاشی یک جای قشنگ پیدا کنید' : readyForPicnic(state) ? 'سبد آماده است؛ به زیرانداز برسید' : state.basket.some(p => p.type === 'drink') ? 'یک خوراکی هم برای پیکنیک بردارید' : 'یک نوشیدنی و یک خوراکی تهیه کنید';
|
||||
}
|
||||
|
||||
function selectChannel(code) {
|
||||
$('#product-code').value=code;$('#product-code').removeAttribute('aria-invalid');$('#shop-status').textContent='';
|
||||
$('#product-code').value = code;
|
||||
$('#product-code').removeAttribute('aria-invalid');
|
||||
$('#shop-status').textContent = '';
|
||||
document.querySelectorAll('[data-channel]').forEach(node => node.classList.toggle('selected', node.dataset.channel === code));
|
||||
const row=$(`#products [data-channel="${code}"]`);row?.scrollIntoView({block:'nearest'});
|
||||
const row = $(`#products [data-channel="${code}"]`);
|
||||
row?.scrollIntoView({block: 'nearest'});
|
||||
}
|
||||
|
||||
function renderProducts() {
|
||||
syncHUD();$('#products').replaceChildren();$('#channels').replaceChildren();
|
||||
products.forEach(p=>{const button=document.createElement('button');button.type='button';button.textContent=fa(p.channel);button.dataset.channel=p.code;button.disabled=!p.available;button.setAttribute('aria-label',`کانال ${fa(p.channel)}${p.available?'':'؛ غیرفعال'}`);button.onclick=()=>selectChannel(p.code);$('#channels').append(button);});
|
||||
products.forEach(p=>{const card=document.createElement('button');card.type='button';card.className='product-code-card';card.dataset.channel=p.code;card.innerHTML=`<span class="icon">${p.icon}</span><b>کد ${fa(p.channel)} · ${p.name}<small>محصول فرضی کانال ${fa(p.channel)}</small></b><span>${formatHetoman(p.cost)}</span>`;card.onclick=()=>selectChannel(p.code);$('#products').append(card);});
|
||||
syncHUD();
|
||||
$('#products').replaceChildren();
|
||||
$('#channels').replaceChildren();
|
||||
products.forEach(p => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.textContent = fa(p.channel);
|
||||
button.dataset.channel = p.code;
|
||||
button.disabled = !p.available;
|
||||
button.setAttribute('aria-label', `کانال ${fa(p.channel)}${p.available ? '' : '؛ غیرفعال'}`);
|
||||
button.onclick = () => selectChannel(p.code);
|
||||
$('#channels').append(button);
|
||||
});
|
||||
products.forEach(p => {
|
||||
const card = document.createElement('button');
|
||||
card.type = 'button';
|
||||
card.className = 'product-code-card';
|
||||
card.dataset.channel = p.code;
|
||||
card.innerHTML = `<span class="icon">${p.icon}</span><b>کد ${fa(p.channel)} · ${p.name}<small>محصول فرضی کانال ${fa(p.channel)}</small></b><span>${formatHetoman(p.cost)}</span>`;
|
||||
card.onclick = () => selectChannel(p.code);
|
||||
$('#products').append(card);
|
||||
});
|
||||
}
|
||||
|
||||
function deliver(p) {
|
||||
activeProduct=p;deliveryTime=3.4;delivery.visible=true;deliveryBody.material=bottleMats[p.type==='snack'?2:p.baseId==='water'?1:0];
|
||||
renderProducts();shop.close();pressed.clear();held.clear();tone(480,.3);
|
||||
activeProduct = p;
|
||||
deliveryTime = 3.4;
|
||||
delivery.visible = true;
|
||||
deliveryBody.material = bottleMats[p.type === 'snack' ? 2 : p.baseId === 'water' ? 1 : 0];
|
||||
renderProducts();
|
||||
shop.close();
|
||||
pressed.clear();
|
||||
held.clear();
|
||||
tone(480, .3);
|
||||
$('#speech').textContent = 'پدر: ببین، دستگاه داره انتخابت رو آماده میکنه!';
|
||||
}
|
||||
function openShop(){if(Math.abs(x-stationX)<2.5&&!shop.open&&!notebook.open&&!state.busy&&!state.finished){pressed.clear();held.clear();$('#shop-status').textContent='بچه: خودم میتونم انتخاب کنم؟ پدر: حتماً! برای نوشیدنی و خوراکی پول کنار بگذار.';renderProducts();$('#product-code').value='';$('#product-code').removeAttribute('aria-invalid');shop.showModal();$('#product-code').focus();tone(750);}}
|
||||
|
||||
function openShop() {
|
||||
if (Math.abs(x - stationX) < 2.5 && !shop.open && !notebook.open && !state.busy && !state.finished) {
|
||||
pressed.clear();
|
||||
held.clear();
|
||||
$('#shop-status').textContent = 'بچه: خودم میتونم انتخاب کنم؟ پدر: حتماً! برای نوشیدنی و خوراکی پول کنار بگذار.';
|
||||
renderProducts();
|
||||
$('#product-code').value = '';
|
||||
$('#product-code').removeAttribute('aria-invalid');
|
||||
shop.showModal();
|
||||
$('#product-code').focus();
|
||||
tone(750);
|
||||
}
|
||||
}
|
||||
|
||||
$('#code-form').onsubmit = e => {
|
||||
e.preventDefault();
|
||||
if (state.busy || state.finished || !shop.open) return;
|
||||
@@ -275,84 +523,261 @@ $('#code-form').onsubmit=e=>{
|
||||
if (!field.value.trim()) error = 'کد محصول را وارد کن.';
|
||||
else if (!product) error = 'کانال قابل خرید نیست؛ عددی از ۲۱ تا ۶۰ وارد کن.';
|
||||
else if (state.balance < product.cost) error = 'برای این محصول موجودی کافی نداری.';
|
||||
else if(!purchase({...state,basket:[...state.basket]},product.id))error='برای تکمیل پیکنیک، هزینهٔ نوشیدنی و خوراکی را کنار بگذار؛ محصول دیگری انتخاب کن.';
|
||||
if(error){$('#shop-status').textContent=error;field.setAttribute('aria-invalid','true');field.focus();tone(200,.2);return;}
|
||||
else if (!purchase({
|
||||
...state,
|
||||
basket: [...state.basket]
|
||||
}, product.id)) error = 'برای تکمیل پیکنیک، هزینهٔ نوشیدنی و خوراکی را کنار بگذار؛ محصول دیگری انتخاب کن.';
|
||||
if (error) {
|
||||
$('#shop-status').textContent = error;
|
||||
field.setAttribute('aria-invalid', 'true');
|
||||
field.focus();
|
||||
tone(200, .2);
|
||||
return;
|
||||
}
|
||||
field.removeAttribute('aria-invalid');
|
||||
if (beginPayment(state, payment, product)) {
|
||||
paymentTime=0;shop.close();clearInput();$('#payment-panel').hidden=false;$('#cancel-payment').hidden=false;
|
||||
paymentTime = 0;
|
||||
shop.close();
|
||||
clearInput();
|
||||
$('#payment-panel').hidden = false;
|
||||
$('#cancel-payment').hidden = false;
|
||||
$('#payment-message').textContent = `کانال ${fa(product.channel)} · ${product.name} · ${formatHetoman(product.cost)}`;
|
||||
$('#swipe-card').disabled=true;$('#payment-result').textContent='در حال نزدیک شدن به کارتخوان…';
|
||||
$('#swipe-card').disabled = true;
|
||||
$('#payment-result').textContent = 'در حال نزدیک شدن به کارتخوان…';
|
||||
}
|
||||
};
|
||||
|
||||
function swipeCard() {
|
||||
if (!swipePayment(state, payment)) return;
|
||||
paymentTime=0;bankCard.visible=true;$('#swipe-card').disabled=true;$('#cancel-payment').hidden=true;
|
||||
$('#payment-result').textContent='خرید موفق ✓ محصول در حال تحویل است';tone(1100,.3);syncHUD();
|
||||
paymentTime = 0;
|
||||
bankCard.visible = true;
|
||||
$('#swipe-card').disabled = true;
|
||||
$('#cancel-payment').hidden = true;
|
||||
$('#payment-result').textContent = 'خرید موفق ✓ محصول در حال تحویل است';
|
||||
tone(1100, .3);
|
||||
syncHUD();
|
||||
}
|
||||
|
||||
$('#swipe-card').onclick = swipeCard;
|
||||
$('#cancel-payment').onclick=()=>{if(cancelPayment(state,payment)){$('#payment-panel').hidden=true;bankCard.visible=false;clearInput();}};
|
||||
$('#product-code').addEventListener('input',()=>{document.querySelectorAll('[data-channel]').forEach(node=>node.classList.remove('selected'));$('#product-code').removeAttribute('aria-invalid');$('#shop-status').textContent='';});
|
||||
$('#interact').onclick=openShop;$('#close').onclick=()=>shop.close();
|
||||
window.addEventListener('keydown',e=>{if(e.key.toLowerCase()==='f'){if(!e.repeat)performStoryAction();return;}if(e.key.toLowerCase()==='c'&&payment.phase==='ready'){e.preventDefault();swipeCard();return;}if(notebook.open||ending.open)return;if(['ArrowRight','ArrowLeft','ArrowUp','ArrowDown',' '].includes(e.key)&&!shop.open&&!ending.open)e.preventDefault();pressed.add(e.key.toLowerCase());if(e.key.toLowerCase()==='e')openShop();});
|
||||
$('#cancel-payment').onclick = () => {
|
||||
if (cancelPayment(state, payment)) {
|
||||
$('#payment-panel').hidden = true;
|
||||
bankCard.visible = false;
|
||||
clearInput();
|
||||
}
|
||||
};
|
||||
$('#product-code').addEventListener('input', () => {
|
||||
document.querySelectorAll('[data-channel]').forEach(node => node.classList.remove('selected'));
|
||||
$('#product-code').removeAttribute('aria-invalid');
|
||||
$('#shop-status').textContent = '';
|
||||
});
|
||||
$('#interact').onclick = openShop;
|
||||
$('#close').onclick = () => shop.close();
|
||||
window.addEventListener('keydown', e => {
|
||||
if (e.key.toLowerCase() === 'f') {
|
||||
if (!e.repeat) performStoryAction();
|
||||
return;
|
||||
}
|
||||
if (e.key.toLowerCase() === 'c' && payment.phase === 'ready') {
|
||||
e.preventDefault();
|
||||
swipeCard();
|
||||
return;
|
||||
}
|
||||
if (notebook.open || ending.open) return;
|
||||
if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', ' '].includes(e.key) && !shop.open && !ending.open) e.preventDefault();
|
||||
pressed.add(e.key.toLowerCase());
|
||||
if (e.key.toLowerCase() === 'e') openShop();
|
||||
});
|
||||
window.addEventListener('keyup', e => pressed.delete(e.key.toLowerCase()));
|
||||
function clearInput(){pressed.clear();held.clear();}window.addEventListener('blur',clearInput);document.addEventListener('visibilitychange',clearInput);shop.addEventListener('close',clearInput);
|
||||
for(const [id,dir] of [['back',-1],['forward',1],['zoom-in','in'],['zoom-out','out']]){const b=$('#'+id);b.onpointerdown=e=>{if((state.busy||state.finished)&&typeof dir==='number')return;b.setPointerCapture(e.pointerId);held.add(dir);};b.onpointerup=b.onpointercancel=b.onlostpointercapture=()=>held.delete(dir);}
|
||||
function reset(){payment=createPayment();paymentTime=0;$('#payment-panel').hidden=true;bankCard.visible=false;story=createStory();chapterId=null;storyDialog.hidden=true;chapterTimer=0;if(notebook.open)notebook.close();rebuildRoute();state=createState();x=-8;deliveryTime=0;delivery.visible=false;activeProduct=null;flap.rotation.x=0;encounterTime=0;rabbitSeen=false;encounterActive=false;facing=Math.PI/2;zoomDistance=1;picnicFood.clear();clearInput();if(shop.open)shop.close();if(ending.open)ending.close();$('#badge').textContent='';$('#recycle').disabled=false;$('#recycle').textContent='بستهها را در سطل بینداز ♻';$('#speech').textContent='بچه: بابا، برای پیکنیک چی بخریم؟';syncHUD();}
|
||||
$('#restart').onclick=reset;$('#again').onclick=reset;
|
||||
$('#recycle').onclick=()=>{state.recycled=true;$('#badge').textContent='🏅 نشان دوست جنگل را گرفتی!';$('#recycle').disabled=true;$('#recycle').textContent='جنگل تمیز ماند ✓';$('#speech').textContent='بچه: چیزی جا نذاشتیم؟ پدر: فقط رد پامون.';tone(880,.4);};
|
||||
function finish(){state.finished=true;clearInput();state.basket.slice(0,10).forEach((p,i)=>{const item=mesh(p.type==='drink'?new THREE.CylinderGeometry(.09,.09,.3,8):new THREE.BoxGeometry(.25,.09,.2),bottleMats[p.type==='drink'?0:2],picnicFood,[-.5+(i%5)*.28,.25,Math.floor(i/5)*.3]);});$('#ending-text').textContent=`با ${fa(state.basket.length)} خوراکی و ${formatHetoman(state.balance)} باقیمانده، وقت یک استراحت دونفره است.`;$('#ending-basket').textContent=state.basket.map(p=>p.icon).join(' ');$('#speech').textContent='بچه: بهترین قسمت گردش، با تو بودن بود بابا!';$('#mission').textContent='پیکنیک آماده است؛ یک خاطرهٔ خوب ساختید';tone(660,.5);showChapter('summit');}
|
||||
|
||||
function clearInput() {
|
||||
pressed.clear();
|
||||
held.clear();
|
||||
}
|
||||
|
||||
window.addEventListener('blur', clearInput);
|
||||
document.addEventListener('visibilitychange', clearInput);
|
||||
shop.addEventListener('close', clearInput);
|
||||
for (const [id, dir] of [['back', -1], ['forward', 1], ['zoom-in', 'in'], ['zoom-out', 'out']]) {
|
||||
const b = $('#' + id);
|
||||
b.onpointerdown = e => {
|
||||
if ((state.busy || state.finished) && typeof dir === 'number') return;
|
||||
b.setPointerCapture(e.pointerId);
|
||||
held.add(dir);
|
||||
};
|
||||
b.onpointerup = b.onpointercancel = b.onlostpointercapture = () => held.delete(dir);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
payment = createPayment();
|
||||
paymentTime = 0;
|
||||
$('#payment-panel').hidden = true;
|
||||
bankCard.visible = false;
|
||||
story = createStory();
|
||||
chapterId = null;
|
||||
storyDialog.hidden = true;
|
||||
chapterTimer = 0;
|
||||
if (notebook.open) notebook.close();
|
||||
rebuildRoute();
|
||||
state = createState();
|
||||
x = -8;
|
||||
deliveryTime = 0;
|
||||
delivery.visible = false;
|
||||
activeProduct = null;
|
||||
flap.rotation.x = 0;
|
||||
encounterTime = 0;
|
||||
rabbitSeen = false;
|
||||
encounterActive = false;
|
||||
facing = Math.PI / 2;
|
||||
zoomDistance = 1;
|
||||
picnicFood.clear();
|
||||
clearInput();
|
||||
if (shop.open) shop.close();
|
||||
if (ending.open) ending.close();
|
||||
$('#badge').textContent = '';
|
||||
$('#recycle').disabled = false;
|
||||
$('#recycle').textContent = 'بستهها را در سطل بینداز ♻';
|
||||
$('#speech').textContent = 'بچه: بابا، برای پیکنیک چی بخریم؟';
|
||||
syncHUD();
|
||||
}
|
||||
|
||||
$('#restart').onclick = reset;
|
||||
$('#again').onclick = reset;
|
||||
$('#recycle').onclick = () => {
|
||||
state.recycled = true;
|
||||
$('#badge').textContent = '🏅 نشان دوست جنگل را گرفتی!';
|
||||
$('#recycle').disabled = true;
|
||||
$('#recycle').textContent = 'جنگل تمیز ماند ✓';
|
||||
$('#speech').textContent = 'بچه: چیزی جا نذاشتیم؟ پدر: فقط رد پامون.';
|
||||
tone(880, .4);
|
||||
};
|
||||
|
||||
function finish() {
|
||||
state.finished = true;
|
||||
clearInput();
|
||||
state.basket.slice(0, 10).forEach((p, i) => {
|
||||
const item = mesh(p.type === 'drink' ? new THREE.CylinderGeometry(.09, .09, .3, 8) : new THREE.BoxGeometry(.25, .09, .2), bottleMats[p.type === 'drink' ? 0 : 2], picnicFood, [-.5 + (i % 5) * .28, .25, Math.floor(i / 5) * .3]);
|
||||
});
|
||||
$('#ending-text').textContent = `با ${fa(state.basket.length)} خوراکی و ${formatHetoman(state.balance)} باقیمانده، وقت یک استراحت دونفره است.`;
|
||||
$('#ending-basket').textContent = state.basket.map(p => p.icon).join(' ');
|
||||
$('#speech').textContent = 'بچه: بهترین قسمت گردش، با تو بودن بود بابا!';
|
||||
$('#mission').textContent = 'پیکنیک آماده است؛ یک خاطرهٔ خوب ساختید';
|
||||
tone(660, .5);
|
||||
showChapter('summit');
|
||||
}
|
||||
|
||||
// The ending opens on request so the picnic remains visible before the overlay.
|
||||
const celebrate=document.createElement('button');celebrate.id='celebrate';celebrate.textContent='پیکنیک و نشان دوست جنگل 🌿';celebrate.hidden=true;document.body.append(celebrate);celebrate.onclick=()=>{if(!story.done.includes('summit'))showChapter('summit');else ending.showModal();};
|
||||
const celebrate = document.createElement('button');
|
||||
celebrate.id = 'celebrate';
|
||||
celebrate.textContent = 'پیکنیک و نشان دوست جنگل 🌿';
|
||||
celebrate.hidden = true;
|
||||
document.body.append(celebrate);
|
||||
celebrate.onclick = () => {
|
||||
if (!story.done.includes('summit')) showChapter('summit'); else ending.showModal();
|
||||
};
|
||||
ending.addEventListener('cancel', clearInput);
|
||||
function resize(){renderer.setSize(innerWidth,innerHeight);camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();}window.addEventListener('resize',resize);resize();
|
||||
const camTarget=new THREE.Vector3(),look=new THREE.Vector3(x+1,.8,pathZ(x)),lookTarget=new THREE.Vector3();camera.position.set(x+7,10,14);
|
||||
|
||||
function resize() {
|
||||
renderer.setSize(innerWidth, innerHeight);
|
||||
camera.aspect = innerWidth / innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
const camTarget = new THREE.Vector3(), look = new THREE.Vector3(x + 1, .8, pathZ(x)), lookTarget = new THREE.Vector3();
|
||||
camera.position.set(x + 7, 10, 14);
|
||||
let last = performance.now();
|
||||
|
||||
function frame(now) {
|
||||
requestAnimationFrame(frame);const dt=Math.min((now-last)/1000,.1);last=now;elapsed+=dt;
|
||||
requestAnimationFrame(frame);
|
||||
const dt = Math.min((now - last) / 1000, .1);
|
||||
last = now;
|
||||
elapsed += dt;
|
||||
const direction = movementDirection(pressed, held);
|
||||
const next=nextChapter(story,x);if(next&&!chapterId&&!shop.open&&!ending.open&&!notebook.open&&!state.busy&&!state.finished)showChapter(next);
|
||||
const next = nextChapter(story, x);
|
||||
if (next && !chapterId && !shop.open && !ending.open && !notebook.open && !state.busy && !state.finished) showChapter(next);
|
||||
if (chapterId && !shop.open && !notebook.open && !ending.open && !document.hidden) {
|
||||
const chapter = chapters[chapterId], lastLine = chapterLine === chapter.lines.length - 1;
|
||||
if (!(lastLine && chapter.action)) {
|
||||
chapterTimer -= dt;
|
||||
if(chapterTimer<=0){if(!lastLine){chapterLine++;renderChapter();}else finishChapter();}
|
||||
if (chapterTimer <= 0) {
|
||||
if (!lastLine) {
|
||||
chapterLine++;
|
||||
renderChapter();
|
||||
} else finishChapter();
|
||||
}
|
||||
}
|
||||
}
|
||||
encounterActive = chapterId === 'rabbit';
|
||||
moving=canWalk({shopOpen:shop.open,notebookOpen:notebook.open,endingOpen:ending.open,busy:state.busy,finished:state.finished})?direction:0;
|
||||
moving = canWalk({
|
||||
shopOpen: shop.open,
|
||||
notebookOpen: notebook.open,
|
||||
endingOpen: ending.open,
|
||||
busy: state.busy,
|
||||
finished: state.finished
|
||||
}) ? direction : 0;
|
||||
x = THREE.MathUtils.clamp(x + moving * 2.2 * dt, -9, 22);
|
||||
family.position.set(x,pathHeight(x),pathZ(x));family.rotation.y=-Math.atan(.255*Math.cos(x*.17));
|
||||
family.position.set(x, pathHeight(x), pathZ(x));
|
||||
family.rotation.y = -Math.atan(.255 * Math.cos(x * .17));
|
||||
const shopping = shop.open || state.busy;
|
||||
if(shopping){family.position.x=THREE.MathUtils.lerp(x,stationX,1-Math.exp(-dt*8));x=family.position.x;}
|
||||
if (shopping) {
|
||||
family.position.x = THREE.MathUtils.lerp(x, stationX, 1 - Math.exp(-dt * 8));
|
||||
x = family.position.x;
|
||||
}
|
||||
const desiredFacing = shopping ? Math.PI : moving < 0 ? -Math.PI / 2 : moving > 0 ? Math.PI / 2 : facing;
|
||||
facing += Math.atan2(Math.sin(desiredFacing - facing), Math.cos(desiredFacing - facing)) * (1 - Math.exp(-dt * 7));
|
||||
if (moving) gait += dt * 6.2;
|
||||
characters.update({dt,time:elapsed,gait,moving:!!moving,facing,shopping,delivery:state.busy,finished:state.finished,watchRabbit:encounterActive});
|
||||
characters.update({
|
||||
dt,
|
||||
time: elapsed,
|
||||
gait,
|
||||
moving: !!moving,
|
||||
facing,
|
||||
shopping,
|
||||
delivery: state.busy,
|
||||
finished: state.finished,
|
||||
watchRabbit: encounterActive
|
||||
});
|
||||
if (state.finished) family.position.x = 22.7;
|
||||
rabbit.position.set(-2.5 + (rabbitSeen ? Math.min(4, encounterTime) * .8 : 0), rabbitSeen ? Math.abs(Math.sin(elapsed * 8)) * .3 : 0, pathZ(-2.5) + 2.5 + (rabbitSeen ? Math.min(4, encounterTime) : 0));
|
||||
if (rabbitSeen) encounterTime = Math.min(4, encounterTime + dt);
|
||||
camTarget.set(x + 7, state.finished ? 8 : innerWidth < 650 ? 12 : 9, innerWidth < 650 ? 19 : 13);
|
||||
lookTarget.set(x + 1, .8 + pathHeight(x), pathZ(x));
|
||||
if(shopping){camTarget.set(stationX+4.2,3.5,stationZ+(innerWidth<650?8:6));lookTarget.set(stationX,.95,stationZ+.9);}
|
||||
if(encounterActive){camTarget.set(x+5,5.5,pathZ(x)+9);lookTarget.set(-2.9,.65,pathZ(-2.5)+1);}
|
||||
if (shopping) {
|
||||
camTarget.set(stationX + 4.2, 3.5, stationZ + (innerWidth < 650 ? 8 : 6));
|
||||
lookTarget.set(stationX, .95, stationZ + .9);
|
||||
}
|
||||
if (encounterActive) {
|
||||
camTarget.set(x + 5, 5.5, pathZ(x) + 9);
|
||||
lookTarget.set(-2.9, .65, pathZ(-2.5) + 1);
|
||||
}
|
||||
if (!shop.open && !notebook.open && !ending.open) {
|
||||
const zoomInput = Number(pressed.has('arrowdown') || held.has('out')) - Number(pressed.has('arrowup') || held.has('in'));
|
||||
zoomDistance = THREE.MathUtils.clamp(zoomDistance + zoomInput * dt * .45, .6, 1.6);
|
||||
}
|
||||
camTarget.sub(lookTarget).multiplyScalar(zoomDistance).add(lookTarget);
|
||||
if (['approaching', 'ready', 'approved'].includes(payment.phase)) {
|
||||
const reader=new THREE.Vector3(.67,1.7,.68);machine.localToWorld(reader);
|
||||
lookTarget.copy(reader);camTarget.copy(reader).add(new THREE.Vector3(.8,.35,1.8));
|
||||
const reader = new THREE.Vector3(.67, 1.7, .68);
|
||||
machine.localToWorld(reader);
|
||||
lookTarget.copy(reader);
|
||||
camTarget.copy(reader).add(new THREE.Vector3(.8, .35, 1.8));
|
||||
}
|
||||
camera.position.lerp(camTarget,1-Math.exp(-dt*2.8));look.lerp(lookTarget,1-Math.exp(-dt*3));camera.lookAt(look);
|
||||
camera.position.lerp(camTarget, 1 - Math.exp(-dt * 2.8));
|
||||
look.lerp(lookTarget, 1 - Math.exp(-dt * 3));
|
||||
camera.lookAt(look);
|
||||
document.body.classList.toggle('cinematic', shopping || encounterActive);
|
||||
const near = Math.abs(x - stationX) < 2.5;
|
||||
// Project the prompt from the reader into screen space; it follows the machine.
|
||||
camera.updateMatrixWorld();machine.updateMatrixWorld(true);
|
||||
camera.updateMatrixWorld();
|
||||
machine.updateMatrixWorld(true);
|
||||
const promptPoint = machine.localToWorld(new THREE.Vector3(.55, 2.15, .9)).project(camera);
|
||||
const promptVisible = near && !shop.open && !notebook.open && !ending.open && !state.busy && !state.finished && promptPoint.z > -1 && promptPoint.z < 1;
|
||||
$('#interact').hidden = !promptVisible;
|
||||
if(promptVisible){$('#interact').style.left=`${THREE.MathUtils.clamp((promptPoint.x*.5+.5)*innerWidth,85,innerWidth-85)}px`;$('#interact').style.top=`${THREE.MathUtils.clamp((-.5*promptPoint.y+.5)*innerHeight,85,innerHeight-100)}px`;}
|
||||
if (promptVisible) {
|
||||
$('#interact').style.left = `${THREE.MathUtils.clamp((promptPoint.x * .5 + .5) * innerWidth, 85, innerWidth - 85)}px`;
|
||||
$('#interact').style.top = `${THREE.MathUtils.clamp((-.5 * promptPoint.y + .5) * innerHeight, 85, innerHeight - 100)}px`;
|
||||
}
|
||||
$('#machine-note').hidden = !promptVisible;
|
||||
document.body.classList.toggle('near-machine', promptVisible);
|
||||
celebrate.hidden = !state.finished;
|
||||
@@ -364,29 +789,61 @@ function frame(now){
|
||||
if (x > 21 && storyReady(story) && readyForPicnic(state) && !state.busy && !state.finished) finish();
|
||||
if (payment.phase !== 'idle') {
|
||||
paymentTime += dt;
|
||||
if(payment.phase==='approaching'&&paymentTime>1.2){payment.phase='ready';$('#swipe-card').disabled=false;$('#cancel-payment').hidden=false;$('#payment-result').textContent='دکمهٔ «کشیدن کارت» یا کلید C را بزن.';}
|
||||
if (payment.phase === 'approaching' && paymentTime > 1.2) {
|
||||
payment.phase = 'ready';
|
||||
$('#swipe-card').disabled = false;
|
||||
$('#cancel-payment').hidden = false;
|
||||
$('#payment-result').textContent = 'دکمهٔ «کشیدن کارت» یا کلید C را بزن.';
|
||||
}
|
||||
if (payment.phase === 'approved') {
|
||||
bankCard.position.set(.92 - Math.min(paymentTime, 1) * .35, 1.86 - Math.min(paymentTime, 1) * .32, .78);
|
||||
if(paymentTime>1.35){const product=payment.product;payment=createPayment();$('#payment-panel').hidden=true;bankCard.visible=false;deliver(product);}
|
||||
if (paymentTime > 1.35) {
|
||||
const product = payment.product;
|
||||
payment = createPayment();
|
||||
$('#payment-panel').hidden = true;
|
||||
bankCard.visible = false;
|
||||
deliver(product);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (deliveryTime > 0) {
|
||||
deliveryTime=Math.max(0,deliveryTime-dt);const t=3.4-deliveryTime;
|
||||
deliveryTime = Math.max(0, deliveryTime - dt);
|
||||
const t = 3.4 - deliveryTime;
|
||||
const slotIndex = (activeProduct?.channel ?? 22) - 1;
|
||||
const start=new THREE.Vector3(-.65+(slotIndex%10)*.115,.965+(5-Math.floor(slotIndex/10))*.275,.76);machine.localToWorld(start);
|
||||
const slot=new THREE.Vector3(-.13,.35,.86);machine.localToWorld(slot);
|
||||
const hand=new THREE.Vector3(.5,.65,-.15);family.localToWorld(hand);
|
||||
const start = new THREE.Vector3(-.65 + (slotIndex % 10) * .115, .965 + (5 - Math.floor(slotIndex / 10)) * .275, .76);
|
||||
machine.localToWorld(start);
|
||||
const slot = new THREE.Vector3(-.13, .35, .86);
|
||||
machine.localToWorld(slot);
|
||||
const hand = new THREE.Vector3(.5, .65, -.15);
|
||||
family.localToWorld(hand);
|
||||
if (t < 1.1) delivery.position.lerpVectors(start, slot, t / 1.1);
|
||||
else if(t<1.8){delivery.position.copy(slot);}
|
||||
else delivery.position.lerpVectors(slot,hand,Math.min(1,(t-1.8)/1.3));
|
||||
else if (t < 1.8) {
|
||||
delivery.position.copy(slot);
|
||||
} else delivery.position.lerpVectors(slot, hand, Math.min(1, (t - 1.8) / 1.3));
|
||||
flap.rotation.x = t > 1 && t < 2.6 ? -1 : 0;
|
||||
if(!deliveryTime){state.busy=false;delivery.visible=false;flap.rotation.x=0;tone(980,.18);syncHUD();}
|
||||
if (!deliveryTime) {
|
||||
state.busy = false;
|
||||
delivery.visible = false;
|
||||
flap.rotation.x = 0;
|
||||
tone(980, .18);
|
||||
syncHUD();
|
||||
}
|
||||
lostPaper.visible=!story.done.includes('paper');lostPaper.rotation.y=Math.sin(elapsed)*.15;
|
||||
}
|
||||
lostPaper.visible = !story.done.includes('paper');
|
||||
lostPaper.rotation.y = Math.sin(elapsed) * .15;
|
||||
fishes.forEach((fish, i) => fish.position.z = Math.sin(elapsed * .8 + i) * .8);
|
||||
trees.forEach((tree,i)=>{tree.rotation.z=Math.sin(elapsed*.65+i)*.012;});
|
||||
butterfly.position.set(x+1.6+Math.sin(elapsed)*.5,2+Math.sin(elapsed*1.4)*.4,pathZ(x)+1.8);wings.forEach((wing,i)=>wing.rotation.z=Math.sin(elapsed*15)*(i?1:-1)*.7);
|
||||
if(soundEnabled&&elapsed>nextBird){tone(1500+Math.random()*900,.2,.018);nextBird=elapsed+3+Math.random()*4;}
|
||||
trees.forEach((tree, i) => {
|
||||
tree.rotation.z = Math.sin(elapsed * .65 + i) * .012;
|
||||
});
|
||||
butterfly.position.set(x + 1.6 + Math.sin(elapsed) * .5, 2 + Math.sin(elapsed * 1.4) * .4, pathZ(x) + 1.8);
|
||||
wings.forEach((wing, i) => wing.rotation.z = Math.sin(elapsed * 15) * (i ? 1 : -1) * .7);
|
||||
if (soundEnabled && elapsed > nextBird) {
|
||||
tone(1500 + Math.random() * 900, .2, .018);
|
||||
nextBird = elapsed + 3 + Math.random() * 4;
|
||||
}
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
$('#loading').remove();syncHUD();requestAnimationFrame(frame);
|
||||
|
||||
$('#loading').remove();
|
||||
syncHUD();
|
||||
requestAnimationFrame(frame);
|
||||
|
||||
+27
-7
@@ -1,16 +1,36 @@
|
||||
import {purchase} from './game-state.js';
|
||||
export function createPayment(){return {phase:'idle',product:null};}
|
||||
export function beginPayment(state,payment,product){
|
||||
if(payment.phase!=='idle'||!product||!purchase({...state,basket:[...state.basket]},product.id))return false;
|
||||
payment.product=product;payment.phase='approaching';state.busy=true;return true;
|
||||
|
||||
export function createPayment() {
|
||||
return {phase: 'idle', product: null};
|
||||
}
|
||||
|
||||
export function beginPayment(state, payment, product) {
|
||||
if (payment.phase !== 'idle' || !product || !purchase({
|
||||
...state,
|
||||
basket: [...state.basket]
|
||||
}, product.id)) return false;
|
||||
payment.product = product;
|
||||
payment.phase = 'approaching';
|
||||
state.busy = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function swipePayment(state, payment) {
|
||||
if (payment.phase !== 'ready') return false;
|
||||
state.busy = false;
|
||||
if(!purchase(state,payment.product.id)){payment.phase='idle';payment.product=null;return false;}
|
||||
payment.phase='approved';return true;
|
||||
if (!purchase(state, payment.product.id)) {
|
||||
payment.phase = 'idle';
|
||||
payment.product = null;
|
||||
return false;
|
||||
}
|
||||
payment.phase = 'approved';
|
||||
return true;
|
||||
}
|
||||
|
||||
export function cancelPayment(state, payment) {
|
||||
if (!['approaching', 'ready'].includes(payment.phase)) return false;
|
||||
payment.phase='idle';payment.product=null;state.busy=false;return true;
|
||||
payment.phase = 'idle';
|
||||
payment.product = null;
|
||||
state.busy = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
+47
-8
@@ -1,17 +1,37 @@
|
||||
export const chapters = {
|
||||
morning: {title: 'تپهٔ آرزوها', lines: ['پدر: بریم تپه؟ امروز راه رو با هم کشف میکنیم.']},
|
||||
rabbit:{title:'یک دوست کوچک',lines:['بچه: یه خرگوش! آروم نگاهش کنیم؟'],action:{value:'watch',label:'تماشای خرگوش'}},
|
||||
rabbit: {
|
||||
title: 'یک دوست کوچک',
|
||||
lines: ['بچه: یه خرگوش! آروم نگاهش کنیم؟'],
|
||||
action: {value: 'watch', label: 'تماشای خرگوش'}
|
||||
},
|
||||
bridge: {title: 'روی پل', lines: ['پدر: دستم رو بگیر؛ با هم رد میشیم.']},
|
||||
fish: {title: 'زیر پل', lines: ['بچه: ماهیها رو ببین!']},
|
||||
courage: {title: 'اولین خرید من', lines: ['پدر: حالا برای پیکنیک خرید کنیم.']},
|
||||
wind: {title: 'باد و نقاشی', lines: ['بچه: نقاشیم رفت کنار بوته!']},
|
||||
paper:{title:'پیداش کردیم',lines:['بچه: سالمه! بالای تپه کاملش میکنم.'],action:{value:'recover',label:'برداشتن نقاشی'}},
|
||||
fork:{title:'مسیر تپه',lines:['پدر: از راه آفتابی به تپه میرسیم.'],action:{value:'sunny',label:'ادامهٔ مسیر'}},
|
||||
paper: {
|
||||
title: 'پیداش کردیم',
|
||||
lines: ['بچه: سالمه! بالای تپه کاملش میکنم.'],
|
||||
action: {value: 'recover', label: 'برداشتن نقاشی'}
|
||||
},
|
||||
fork: {
|
||||
title: 'مسیر تپه',
|
||||
lines: ['پدر: از راه آفتابی به تپه میرسیم.'],
|
||||
action: {value: 'sunny', label: 'ادامهٔ مسیر'}
|
||||
},
|
||||
sunny: {title: 'رنگهای جنگل', lines: ['بچه: این رنگها رو توی نقاشیم میکشم!']},
|
||||
shade: {title: 'درخت خاطرهها', lines: ['پدر: بچه که بودم هم زیر همین درخت مینشستم.']},
|
||||
summit:{title:'کنار هم',lines:['بچه: دفعهٔ بعد، من راه رو نشونت میدم!'],action:{value:'gold',label:'کامل کردن نقاشی'}}
|
||||
summit: {
|
||||
title: 'کنار هم',
|
||||
lines: ['بچه: دفعهٔ بعد، من راه رو نشونت میدم!'],
|
||||
action: {value: 'gold', label: 'کامل کردن نقاشی'}
|
||||
}
|
||||
};
|
||||
export function createStory(){return {done:[],rabbitChoice:null,route:null,sky:null};}
|
||||
|
||||
export function createStory() {
|
||||
return {done: [], rabbitChoice: null, route: null, sky: null};
|
||||
}
|
||||
|
||||
export function completeChapter(story, id, choice) {
|
||||
if (!chapters[id] || story.done.includes(id)) return false;
|
||||
if (chapters[id].action && chapters[id].action.value !== choice) return false;
|
||||
@@ -21,6 +41,7 @@ export function completeChapter(story,id,choice){
|
||||
if (id === 'summit') story.sky = choice;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function nextChapter(story, x) {
|
||||
const done = id => story.done.includes(id);
|
||||
if (!done('morning')) return 'morning';
|
||||
@@ -30,6 +51,24 @@ export function nextChapter(story,x){
|
||||
if (!done(story.route)) return x >= 19 ? story.route : null;
|
||||
return null;
|
||||
}
|
||||
export function storyReady(story){return ['morning','rabbit','bridge','fish','courage','wind','paper','fork',story.route].every(id=>story.done.includes(id));}
|
||||
export function routeOffset(story,x){if(x>12&&x<15&&story.done.includes('wind'))return Math.sin((x-12)/3*Math.PI)*1.5;if(x<=16||x>=21||!story.route)return 0;return Math.sin((x-16)/5*Math.PI)*(story.route==='sunny'?2.2:-2.2);}
|
||||
export function discoveries(story){const entries=[];if(story.done.includes('rabbit'))entries.push(story.rabbitChoice==='sketch'?'🐇 نقاشی خرگوش':'🐇 تماشای آرام خرگوش');if(story.done.includes('fish'))entries.push('🐟 ماهیهای زیر پل');if(story.done.includes('courage'))entries.push('🌉 پل شجاعت');if(story.done.includes('paper'))entries.push('📄 نقاشی بازیابیشده');if(story.done.includes('sunny'))entries.push('🦋 رنگهای پروانهها');if(story.done.includes('shade'))entries.push('🌳 خاطرهٔ درخت قدیمی');return entries;}
|
||||
|
||||
export function storyReady(story) {
|
||||
return ['morning', 'rabbit', 'bridge', 'fish', 'courage', 'wind', 'paper', 'fork', story.route].every(id => story.done.includes(id));
|
||||
}
|
||||
|
||||
export function routeOffset(story, x) {
|
||||
if (x > 12 && x < 15 && story.done.includes('wind')) return Math.sin((x - 12) / 3 * Math.PI) * 1.5;
|
||||
if (x <= 16 || x >= 21 || !story.route) return 0;
|
||||
return Math.sin((x - 16) / 5 * Math.PI) * (story.route === 'sunny' ? 2.2 : -2.2);
|
||||
}
|
||||
|
||||
export function discoveries(story) {
|
||||
const entries = [];
|
||||
if (story.done.includes('rabbit')) entries.push(story.rabbitChoice === 'sketch' ? '🐇 نقاشی خرگوش' : '🐇 تماشای آرام خرگوش');
|
||||
if (story.done.includes('fish')) entries.push('🐟 ماهیهای زیر پل');
|
||||
if (story.done.includes('courage')) entries.push('🌉 پل شجاعت');
|
||||
if (story.done.includes('paper')) entries.push('📄 نقاشی بازیابیشده');
|
||||
if (story.done.includes('sunny')) entries.push('🦋 رنگهای پروانهها');
|
||||
if (story.done.includes('shade')) entries.push('🌳 خاطرهٔ درخت قدیمی');
|
||||
return entries;
|
||||
}
|
||||
|
||||
+990
-14
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user