update
This commit is contained in:
+20
-4
@@ -1,21 +1,37 @@
|
|||||||
export function createJourney() { return {phase: 'home', time: 0, distance: 0, speed: 0, lane: 0}; }
|
export function createJourney() {
|
||||||
|
return {phase: 'home', time: 0, distance: 0, speed: 0, lane: 0};
|
||||||
|
}
|
||||||
|
|
||||||
export function advanceJourney(journey) {
|
export function advanceJourney(journey) {
|
||||||
const next = {home: 'boarding', entrance: 'walking'}[journey.phase];
|
const next = {home: 'boarding', entrance: 'walking'}[journey.phase];
|
||||||
if (!next) return false;
|
if (!next) return false;
|
||||||
journey.phase = next; journey.time = 0; return true;
|
journey.phase = next;
|
||||||
|
journey.time = 0;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tickJourney(journey, dt, paused = false, input = {}) {
|
export function tickJourney(journey, dt, paused = false, input = {}) {
|
||||||
if (paused) return;
|
if (paused) return;
|
||||||
journey.time += dt;
|
journey.time += dt;
|
||||||
if (journey.phase === 'boarding' && journey.time >= 2) { journey.phase = 'driving'; journey.time = 0; return; }
|
if (journey.phase === 'boarding' && journey.time >= 2) {
|
||||||
|
journey.phase = 'driving';
|
||||||
|
journey.time = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (journey.phase === 'driving') {
|
if (journey.phase === 'driving') {
|
||||||
const throttle = Math.max(-1, Math.min(1, input.throttle ?? 0));
|
const throttle = Math.max(-1, Math.min(1, input.throttle ?? 0));
|
||||||
journey.speed = Math.max(0, Math.min(6, journey.speed + (throttle > 0 ? 3.5 : throttle < 0 ? -7 : -2.5) * dt));
|
journey.speed = Math.max(0, Math.min(6, journey.speed + (throttle > 0 ? 3.5 : throttle < 0 ? -7 : -2.5) * dt));
|
||||||
journey.distance = Math.min(21, journey.distance + journey.speed * dt);
|
journey.distance = Math.min(21, journey.distance + journey.speed * dt);
|
||||||
journey.lane = Math.max(-.75, Math.min(.75, journey.lane + (input.steer ?? 0) * dt * journey.speed * .25));
|
journey.lane = Math.max(-.75, Math.min(.75, journey.lane + (input.steer ?? 0) * dt * journey.speed * .25));
|
||||||
if (journey.distance >= 21) { journey.phase = 'entrance'; journey.time = 0; journey.speed = 0; journey.lane = 0; }
|
if (journey.distance >= 21) {
|
||||||
|
journey.phase = 'entrance';
|
||||||
|
journey.time = 0;
|
||||||
|
journey.speed = 0;
|
||||||
|
journey.lane = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const journeyLabels = {
|
export const journeyLabels = {
|
||||||
home: ['از خانه تا کوه', 'پدر: همهچی آمادهست؟ بیا سوار ماشین بشیم.', 'سوار ماشین شو · F'],
|
home: ['از خانه تا کوه', 'پدر: همهچی آمادهست؟ بیا سوار ماشین بشیم.', 'سوار ماشین شو · F'],
|
||||||
boarding: ['آغاز سفر', 'داریم سوار ماشین میشیم…', ''],
|
boarding: ['آغاز سفر', 'داریم سوار ماشین میشیم…', ''],
|
||||||
|
|||||||
+104
-23
@@ -111,13 +111,34 @@ const journeyPanel = document.createElement('section');
|
|||||||
journeyPanel.id = 'journey-panel';
|
journeyPanel.id = 'journey-panel';
|
||||||
journeyPanel.innerHTML = '<small>FOREST ROAD · یک سفر کنار هم</small><h2></h2><p></p><button></button>';
|
journeyPanel.innerHTML = '<small>FOREST ROAD · یک سفر کنار هم</small><h2></h2><p></p><button></button>';
|
||||||
document.body.append(journeyPanel);
|
document.body.append(journeyPanel);
|
||||||
journeyPanel.querySelector('button').onclick = () => { if (advanceJourney(journey)) {firstPerson=false;clearInput();syncHUD();} };
|
journeyPanel.querySelector('button').onclick = () => {
|
||||||
|
if (advanceJourney(journey)) {
|
||||||
|
firstPerson = false;
|
||||||
|
clearInput();
|
||||||
|
syncHUD();
|
||||||
|
}
|
||||||
|
};
|
||||||
const viewButton = document.createElement('button');
|
const viewButton = document.createElement('button');
|
||||||
viewButton.id = 'camera-view';viewButton.textContent = 'تغییر دوربین · V';document.body.append(viewButton);
|
viewButton.id = 'camera-view';
|
||||||
viewButton.onclick = () => {firstPerson = !firstPerson;};
|
viewButton.textContent = 'تغییر دوربین · V';
|
||||||
|
document.body.append(viewButton);
|
||||||
|
viewButton.onclick = () => {
|
||||||
|
firstPerson = !firstPerson;
|
||||||
|
};
|
||||||
let dragging = false, dragX = 0, dragY = 0;
|
let dragging = false, dragX = 0, dragY = 0;
|
||||||
canvas.addEventListener('pointerdown', e => {dragging=true;dragX=e.clientX;dragY=e.clientY;canvas.setPointerCapture(e.pointerId);});
|
canvas.addEventListener('pointerdown', e => {
|
||||||
canvas.addEventListener('pointermove', e => {if(!dragging)return;viewYaw=THREE.MathUtils.clamp(viewYaw+(e.clientX-dragX)*.005,-1.3,1.3);viewPitch=THREE.MathUtils.clamp(viewPitch-(e.clientY-dragY)*.004,-.6,.6);dragX=e.clientX;dragY=e.clientY;});
|
dragging = true;
|
||||||
|
dragX = e.clientX;
|
||||||
|
dragY = e.clientY;
|
||||||
|
canvas.setPointerCapture(e.pointerId);
|
||||||
|
});
|
||||||
|
canvas.addEventListener('pointermove', e => {
|
||||||
|
if (!dragging) return;
|
||||||
|
viewYaw = THREE.MathUtils.clamp(viewYaw + (e.clientX - dragX) * .005, -1.3, 1.3);
|
||||||
|
viewPitch = THREE.MathUtils.clamp(viewPitch - (e.clientY - dragY) * .004, -.6, .6);
|
||||||
|
dragX = e.clientX;
|
||||||
|
dragY = e.clientY;
|
||||||
|
});
|
||||||
canvas.addEventListener('pointerup', () => dragging = false);
|
canvas.addEventListener('pointerup', () => dragging = false);
|
||||||
canvas.addEventListener('pointercancel', () => dragging = false);
|
canvas.addEventListener('pointercancel', () => dragging = false);
|
||||||
const characters = createFamily();
|
const characters = createFamily();
|
||||||
@@ -142,10 +163,20 @@ box(1.72,.07,.04,bezel,machine,[0,2.91,.6]);
|
|||||||
box(1.72, .07, .04, bezel, machine, [0, .12, .6]);
|
box(1.72, .07, .04, bezel, machine, [0, .12, .6]);
|
||||||
box(.3, 1.45, .08, material('#142625'), machine, [.67, 1.53, .6]);
|
box(.3, 1.45, .08, material('#142625'), machine, [.67, 1.53, .6]);
|
||||||
box(1.2, .04, .08, bezel, machine, [-.13, 2.5, .7]);
|
box(1.2, .04, .08, bezel, machine, [-.13, 2.5, .7]);
|
||||||
const displayCanvas=document.createElement('canvas');displayCanvas.width=256;displayCanvas.height=256;
|
const displayCanvas = document.createElement('canvas');
|
||||||
const displayCtx=displayCanvas.getContext('2d');displayCtx.fillStyle='#122d2c';displayCtx.fillRect(0,0,256,256);
|
displayCanvas.width = 256;
|
||||||
displayCtx.textAlign='center';displayCtx.fillStyle='#bcf2d5';displayCtx.font='bold 31px Tahoma';displayCtx.fillText('پرداخت',128,70);
|
displayCanvas.height = 256;
|
||||||
displayCtx.font='24px Tahoma';displayCtx.fillText('کارت بکشید',128,125);displayCtx.fillStyle='#85bbab';displayCtx.fillRect(44,170,168,4);
|
const displayCtx = displayCanvas.getContext('2d');
|
||||||
|
displayCtx.fillStyle = '#122d2c';
|
||||||
|
displayCtx.fillRect(0, 0, 256, 256);
|
||||||
|
displayCtx.textAlign = 'center';
|
||||||
|
displayCtx.fillStyle = '#bcf2d5';
|
||||||
|
displayCtx.font = 'bold 31px Tahoma';
|
||||||
|
displayCtx.fillText('پرداخت', 128, 70);
|
||||||
|
displayCtx.font = '24px Tahoma';
|
||||||
|
displayCtx.fillText('کارت بکشید', 128, 125);
|
||||||
|
displayCtx.fillStyle = '#85bbab';
|
||||||
|
displayCtx.fillRect(44, 170, 168, 4);
|
||||||
mesh(new THREE.PlaneGeometry(.23, .3), new THREE.MeshBasicMaterial({map: new THREE.CanvasTexture(displayCanvas)}), machine, [.67, 1.7, .669]);
|
mesh(new THREE.PlaneGeometry(.23, .3), new THREE.MeshBasicMaterial({map: new THREE.CanvasTexture(displayCanvas)}), machine, [.67, 1.7, .669]);
|
||||||
|
|
||||||
box(1.25, 1.85, .08, material('#a6d5c3', {metalness: .4, roughness: .22}), machine, [-.13, 1.55, .59]);
|
box(1.25, 1.85, .08, material('#a6d5c3', {metalness: .4, roughness: .22}), machine, [-.13, 1.55, .59]);
|
||||||
@@ -345,7 +376,14 @@ function renderChapter() {
|
|||||||
|
|
||||||
function performStoryAction() {
|
function performStoryAction() {
|
||||||
if (shop.open || notebook.open || ending.open || state.busy) return;
|
if (shop.open || notebook.open || ending.open || state.busy) return;
|
||||||
if (journey.phase !== 'walking') { if (advanceJourney(journey)) {firstPerson=false;clearInput();syncHUD();} return; }
|
if (journey.phase !== 'walking') {
|
||||||
|
if (advanceJourney(journey)) {
|
||||||
|
firstPerson = false;
|
||||||
|
clearInput();
|
||||||
|
syncHUD();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!chapterId || shop.open || notebook.open || ending.open || state.busy) return;
|
if (!chapterId || shop.open || notebook.open || ending.open || state.busy) return;
|
||||||
const chapter = chapters[chapterId];
|
const chapter = chapters[chapterId];
|
||||||
if (chapter.action && chapterLine === chapter.lines.length - 1) finishChapter(chapter.action.value);
|
if (chapter.action && chapterLine === chapter.lines.length - 1) finishChapter(chapter.action.value);
|
||||||
@@ -616,7 +654,10 @@ $('#interact').onclick = openShop;
|
|||||||
$('#close').onclick = () => shop.close();
|
$('#close').onclick = () => shop.close();
|
||||||
window.addEventListener('keydown', e => {
|
window.addEventListener('keydown', e => {
|
||||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
||||||
if(e.key.toLowerCase()==='v' && !e.repeat){firstPerson=!firstPerson;return;}
|
if (e.key.toLowerCase() === 'v' && !e.repeat) {
|
||||||
|
firstPerson = !firstPerson;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (e.key.toLowerCase() === 'f') {
|
if (e.key.toLowerCase() === 'f') {
|
||||||
if (!e.repeat) performStoryAction();
|
if (!e.repeat) performStoryAction();
|
||||||
return;
|
return;
|
||||||
@@ -629,7 +670,10 @@ window.addEventListener('keydown', e => {
|
|||||||
if (notebook.open || ending.open) return;
|
if (notebook.open || ending.open) return;
|
||||||
if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', ' '].includes(e.key) && !shop.open && !ending.open) e.preventDefault();
|
if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', ' '].includes(e.key) && !shop.open && !ending.open) e.preventDefault();
|
||||||
pressed.add(e.key.toLowerCase());
|
pressed.add(e.key.toLowerCase());
|
||||||
if (e.key.toLowerCase() === 'e') { e.preventDefault(); openShop(); }
|
if (e.key.toLowerCase() === 'e') {
|
||||||
|
e.preventDefault();
|
||||||
|
openShop();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener('keyup', e => pressed.delete(e.key.toLowerCase()));
|
window.addEventListener('keyup', e => pressed.delete(e.key.toLowerCase()));
|
||||||
|
|
||||||
@@ -652,8 +696,12 @@ for (const [id, dir] of [['back', -1], ['forward', 1], ['zoom-in', 'in'], ['zoom
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
journey = createJourney(); firstPerson=false;viewYaw=0;viewPitch=0;
|
journey = createJourney();
|
||||||
camera.position.set(-39,3.5,-6);look.set(-32,1,0);
|
firstPerson = false;
|
||||||
|
viewYaw = 0;
|
||||||
|
viewPitch = 0;
|
||||||
|
camera.position.set(-39, 3.5, -6);
|
||||||
|
look.set(-32, 1, 0);
|
||||||
payment = createPayment();
|
payment = createPayment();
|
||||||
paymentTime = 0;
|
paymentTime = 0;
|
||||||
$('#payment-panel').hidden = true;
|
$('#payment-panel').hidden = true;
|
||||||
@@ -675,7 +723,9 @@ function reset() {
|
|||||||
encounterActive = false;
|
encounterActive = false;
|
||||||
facing = Math.PI / 2;
|
facing = Math.PI / 2;
|
||||||
zoomDistance = 1;
|
zoomDistance = 1;
|
||||||
picnicFood.traverse(node => { if (node.isMesh) node.geometry.dispose(); });
|
picnicFood.traverse(node => {
|
||||||
|
if (node.isMesh) node.geometry.dispose();
|
||||||
|
});
|
||||||
picnicFood.clear();
|
picnicFood.clear();
|
||||||
clearInput();
|
clearInput();
|
||||||
if (shop.open) shop.close();
|
if (shop.open) shop.close();
|
||||||
@@ -742,7 +792,10 @@ function frame(now) {
|
|||||||
last = now;
|
last = now;
|
||||||
elapsed += dt;
|
elapsed += dt;
|
||||||
const paused = shop.open || notebook.open || ending.open || document.hidden;
|
const paused = shop.open || notebook.open || ending.open || document.hidden;
|
||||||
tickJourney(journey, dt, paused, {throttle:Number(pressed.has('w') || pressed.has('arrowup') || held.has(1))-Number(pressed.has('s') || pressed.has('arrowdown') || held.has(-1)),steer:Number(pressed.has('d') || pressed.has('arrowright') || held.has('out'))-Number(pressed.has('a') || pressed.has('arrowleft') || held.has('in'))});
|
tickJourney(journey, dt, paused, {
|
||||||
|
throttle: Number(pressed.has('w') || pressed.has('arrowup') || held.has(1)) - Number(pressed.has('s') || pressed.has('arrowdown') || held.has(-1)),
|
||||||
|
steer: Number(pressed.has('d') || pressed.has('arrowright') || held.has('out')) - Number(pressed.has('a') || pressed.has('arrowleft') || held.has('in'))
|
||||||
|
});
|
||||||
scenery.update(elapsed, journey.phase, journey.distance / 21, journey.lane);
|
scenery.update(elapsed, journey.phase, journey.distance / 21, journey.lane);
|
||||||
const walking = journey.phase === 'walking';
|
const walking = journey.phase === 'walking';
|
||||||
document.body.classList.toggle('on-journey', !walking);
|
document.body.classList.toggle('on-journey', !walking);
|
||||||
@@ -753,7 +806,15 @@ function frame(now) {
|
|||||||
$('#zoom-out').textContent = journey.phase === 'driving' ? '↷' : '↓';
|
$('#zoom-out').textContent = journey.phase === 'driving' ? '↷' : '↓';
|
||||||
for (const [id, driveLabel, walkLabel] of [['forward', 'گاز', 'حرکت به جلو'], ['back', 'ترمز', 'حرکت به عقب'], ['zoom-in', 'فرمان چپ', 'زوم نزدیک'], ['zoom-out', 'فرمان راست', 'زوم دور']]) $('#' + id).setAttribute('aria-label', journey.phase === 'driving' ? driveLabel : walkLabel);
|
for (const [id, driveLabel, walkLabel] of [['forward', 'گاز', 'حرکت به جلو'], ['back', 'ترمز', 'حرکت به عقب'], ['zoom-in', 'فرمان چپ', 'زوم نزدیک'], ['zoom-out', 'فرمان راست', 'زوم دور']]) $('#' + id).setAttribute('aria-label', journey.phase === 'driving' ? driveLabel : walkLabel);
|
||||||
journeyPanel.hidden = walking;
|
journeyPanel.hidden = walking;
|
||||||
if(!walking){const [title,line,action]=journeyLabels[journey.phase];journeyPanel.querySelector('h2').textContent=title;journeyPanel.querySelector('p').textContent=line;const button=journeyPanel.querySelector('button');button.textContent=action;button.hidden=!action;if(journey.phase==='driving')journeyPanel.querySelector('h2').textContent=`جادهٔ جنگلی · ${Math.round(journey.distance/21*100).toLocaleString('fa-IR')}٪ · ${Math.round(journey.speed*12).toLocaleString('fa-IR')} کیلومتر/ساعت`;}
|
if (!walking) {
|
||||||
|
const [title, line, action] = journeyLabels[journey.phase];
|
||||||
|
journeyPanel.querySelector('h2').textContent = title;
|
||||||
|
journeyPanel.querySelector('p').textContent = line;
|
||||||
|
const button = journeyPanel.querySelector('button');
|
||||||
|
button.textContent = action;
|
||||||
|
button.hidden = !action;
|
||||||
|
if (journey.phase === 'driving') journeyPanel.querySelector('h2').textContent = `جادهٔ جنگلی · ${Math.round(journey.distance / 21 * 100).toLocaleString('fa-IR')}٪ · ${Math.round(journey.speed * 12).toLocaleString('fa-IR')} کیلومتر/ساعت`;
|
||||||
|
}
|
||||||
const direction = movementDirection(pressed, held) || (Number(pressed.has('w')) - Number(pressed.has('s')));
|
const direction = movementDirection(pressed, held) || (Number(pressed.has('w')) - Number(pressed.has('s')));
|
||||||
const next = nextChapter(story, x);
|
const next = nextChapter(story, x);
|
||||||
if (walking && next && !chapterId && !shop.open && !ending.open && !notebook.open && !state.busy && !state.finished) showChapter(next);
|
if (walking && next && !chapterId && !shop.open && !ending.open && !notebook.open && !state.busy && !state.finished) showChapter(next);
|
||||||
@@ -799,7 +860,10 @@ function frame(now) {
|
|||||||
finished: state.finished,
|
finished: state.finished,
|
||||||
watchRabbit: encounterActive
|
watchRabbit: encounterActive
|
||||||
});
|
});
|
||||||
if (state.finished) {family.position.copy(picnic.position);family.rotation.y=0;}
|
if (state.finished) {
|
||||||
|
family.position.copy(picnic.position);
|
||||||
|
family.rotation.y = 0;
|
||||||
|
}
|
||||||
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));
|
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);
|
if (rabbitSeen) encounterTime = Math.min(4, encounterTime + dt);
|
||||||
camTarget.set(x - 4.5, pathHeight(x) + (innerWidth < 650 ? 4 : 3), pathZ(x - 4.5) + 2);
|
camTarget.set(x - 4.5, pathHeight(x) + (innerWidth < 650 ? 4 : 3), pathZ(x - 4.5) + 2);
|
||||||
@@ -825,13 +889,30 @@ function frame(now) {
|
|||||||
if (!walking) {
|
if (!walking) {
|
||||||
const cx = scenery.car.position.x;
|
const cx = scenery.car.position.x;
|
||||||
family.position.set(journey.phase === 'home' ? -35 : journey.phase === 'boarding' ? -35 + Math.min(1, journey.time / 2) * 3 : -10, 0, journey.phase === 'home' ? 4.2 - Math.min(1, journey.time / 2) * 1.2 : 0);
|
family.position.set(journey.phase === 'home' ? -35 : journey.phase === 'boarding' ? -35 + Math.min(1, journey.time / 2) * 3 : -10, 0, journey.phase === 'home' ? 4.2 - Math.min(1, journey.time / 2) * 1.2 : 0);
|
||||||
if(journey.phase==='boarding')characters.update({dt,time:elapsed,gait:elapsed*6,moving:true,facing:Math.PI/2});
|
if (journey.phase === 'boarding') characters.update({
|
||||||
camTarget.set(cx-6,3.2,-7);lookTarget.set(cx,1,0);
|
dt,
|
||||||
if(journey.phase==='driving' || journey.phase==='entrance'){camTarget.set(cx-5,3.2,scenery.car.position.z-5);lookTarget.set(cx+3,1,scenery.car.position.z);}
|
time: elapsed,
|
||||||
|
gait: elapsed * 6,
|
||||||
|
moving: true,
|
||||||
|
facing: Math.PI / 2
|
||||||
|
});
|
||||||
|
camTarget.set(cx - 6, 3.2, -7);
|
||||||
|
lookTarget.set(cx, 1, 0);
|
||||||
|
if (journey.phase === 'driving' || journey.phase === 'entrance') {
|
||||||
|
camTarget.set(cx - 5, 3.2, scenery.car.position.z - 5);
|
||||||
|
lookTarget.set(cx + 3, 1, scenery.car.position.z);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
family.visible = journey.phase !== 'driving' && !(journey.phase === 'boarding' && journey.time > 1.8) && !(walking && firstPerson && !shopping && !state.finished && !encounterActive);
|
family.visible = journey.phase !== 'driving' && !(journey.phase === 'boarding' && journey.time > 1.8) && !(walking && firstPerson && !shopping && !state.finished && !encounterActive);
|
||||||
if(chapterId==='camp' && walking){camTarget.set(1,2.4,-1);lookTarget.set(4,1,4.4);family.visible=true;}
|
if (chapterId === 'camp' && walking) {
|
||||||
if(state.finished){camTarget.set(25.5,5.1,picnic.position.z+6);lookTarget.copy(picnic.position).add(new THREE.Vector3(0,.6,0));}
|
camTarget.set(1, 2.4, -1);
|
||||||
|
lookTarget.set(4, 1, 4.4);
|
||||||
|
family.visible = true;
|
||||||
|
}
|
||||||
|
if (state.finished) {
|
||||||
|
camTarget.set(25.5, 5.1, picnic.position.z + 6);
|
||||||
|
lookTarget.copy(picnic.position).add(new THREE.Vector3(0, .6, 0));
|
||||||
|
}
|
||||||
if (['approaching', 'ready', 'approved'].includes(payment.phase)) {
|
if (['approaching', 'ready', 'approved'].includes(payment.phase)) {
|
||||||
const reader = new THREE.Vector3(.67, 1.7, .68);
|
const reader = new THREE.Vector3(.67, 1.7, .68);
|
||||||
machine.localToWorld(reader);
|
machine.localToWorld(reader);
|
||||||
|
|||||||
+90
-22
@@ -3,43 +3,92 @@ import * as THREE from 'three';
|
|||||||
export function createJourneyScenery(scene, pathZ) {
|
export function createJourneyScenery(scene, pathZ) {
|
||||||
const mat = c => new THREE.MeshStandardMaterial({color: c, roughness: .7});
|
const mat = c => new THREE.MeshStandardMaterial({color: c, roughness: .7});
|
||||||
const wood = mat('#705340'), stone = mat('#a9a393'), black = mat('#172126'), skin = mat('#dca57e');
|
const wood = mat('#705340'), stone = mat('#a9a393'), black = mat('#172126'), skin = mat('#dca57e');
|
||||||
|
|
||||||
function part(parent, geo, material, x, y, z) {
|
function part(parent, geo, material, x, y, z) {
|
||||||
const m=new THREE.Mesh(geo,material);m.position.set(x,y,z);m.castShadow=true;m.receiveShadow=true;parent.add(m);return m;
|
const m = new THREE.Mesh(geo, material);
|
||||||
|
m.position.set(x, y, z);
|
||||||
|
m.castShadow = true;
|
||||||
|
m.receiveShadow = true;
|
||||||
|
parent.add(m);
|
||||||
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
const box = (p, w, h, d, m, x, y, z) => part(p, new THREE.BoxGeometry(w, h, d), m, x, y, z);
|
const box = (p, w, h, d, m, x, y, z) => part(p, new THREE.BoxGeometry(w, h, d), m, x, y, z);
|
||||||
const home=new THREE.Group();home.position.set(-35,0,7);scene.add(home);
|
const home = new THREE.Group();
|
||||||
|
home.position.set(-35, 0, 7);
|
||||||
|
scene.add(home);
|
||||||
box(home, 8, 4, 6, mat('#d2c1a3'), 0, 2, 0);
|
box(home, 8, 4, 6, mat('#d2c1a3'), 0, 2, 0);
|
||||||
const roof=part(home,new THREE.ConeGeometry(6.5,2,4),mat('#6d493d'),0,5,0);roof.rotation.y=Math.PI/4;
|
const roof = part(home, new THREE.ConeGeometry(6.5, 2, 4), mat('#6d493d'), 0, 5, 0);
|
||||||
|
roof.rotation.y = Math.PI / 4;
|
||||||
box(home, 1.3, 2.6, .15, wood, 0, 1.3, -3.08);
|
box(home, 1.3, 2.6, .15, wood, 0, 1.3, -3.08);
|
||||||
for(const x of [-2.5,2.5]){box(home,1.6,1.4,.12,mat('#8eafb3'),x,2.4,-3.08);box(home,.08,1.4,.15,wood,x,2.4,-3.16);}
|
for (const x of [-2.5, 2.5]) {
|
||||||
|
box(home, 1.6, 1.4, .12, mat('#8eafb3'), x, 2.4, -3.08);
|
||||||
|
box(home, .08, 1.4, .15, wood, x, 2.4, -3.16);
|
||||||
|
}
|
||||||
box(scene, 31, .035, 5, mat('#555957'), -22, .02, 0);
|
box(scene, 31, .035, 5, mat('#555957'), -22, .02, 0);
|
||||||
for (let x = -36; x < -8; x += 3) box(scene, 1.5, .045, .12, mat('#ddd0a3'), x, .04, 0);
|
for (let x = -36; x < -8; x += 3) box(scene, 1.5, .045, .12, mat('#ddd0a3'), x, .04, 0);
|
||||||
const car=new THREE.Group();scene.add(car);
|
const car = new THREE.Group();
|
||||||
|
scene.add(car);
|
||||||
const paint = mat('#bd703c'), glass = mat('#72989e');
|
const paint = mat('#bd703c'), glass = mat('#72989e');
|
||||||
box(car,4.4,.65,1.9,paint,0,.8,0);box(car,2.3,.8,1.75,glass,-.15,1.5,0);box(car,2.5,.12,1.9,paint,-.15,1.96,0);
|
box(car, 4.4, .65, 1.9, paint, 0, .8, 0);
|
||||||
|
box(car, 2.3, .8, 1.75, glass, -.15, 1.5, 0);
|
||||||
|
box(car, 2.5, .12, 1.9, paint, -.15, 1.96, 0);
|
||||||
for (const x of [-1.35, 1.35]) for (const z of [-1, 1]) {
|
for (const x of [-1.35, 1.35]) for (const z of [-1, 1]) {
|
||||||
const wheel=part(car,new THREE.CylinderGeometry(.42,.42,.22,16),black,x,.48,z);wheel.rotation.x=Math.PI/2;
|
const wheel = part(car, new THREE.CylinderGeometry(.42, .42, .22, 16), black, x, .48, z);
|
||||||
|
wheel.rotation.x = Math.PI / 2;
|
||||||
box(car, .09, .2, .45, mat(x > 0 ? '#fff1c0' : '#b34232'), x > 0 ? 2.23 : -2.23, .9, z * .65);
|
box(car, .09, .2, .45, mat(x > 0 ? '#fff1c0' : '#b34232'), x > 0 ? 2.23 : -2.23, .9, z * .65);
|
||||||
}
|
}
|
||||||
box(car, .1, .5, 1.7, black, -1.32, 1.45, 0);
|
box(car, .1, .5, 1.7, black, -1.32, 1.45, 0);
|
||||||
box(scene, 5, .04, 3, stone, -11, .025, -4);
|
box(scene, 5, .04, 3, stone, -11, .025, -4);
|
||||||
const gate=new THREE.Group();gate.position.set(-8.5,0,pathZ(-8.5));scene.add(gate);
|
const gate = new THREE.Group();
|
||||||
|
gate.position.set(-8.5, 0, pathZ(-8.5));
|
||||||
|
scene.add(gate);
|
||||||
for (const z of [-2.5, 2.5]) box(gate, .3, 3.7, .3, wood, 0, 1.85, z);
|
for (const z of [-2.5, 2.5]) box(gate, .3, 3.7, .3, wood, 0, 1.85, z);
|
||||||
box(gate, .4, .65, 5.8, wood, 0, 3.4, 0);
|
box(gate, .4, .65, 5.8, wood, 0, 3.4, 0);
|
||||||
const signCanvas=document.createElement('canvas');signCanvas.width=512;signCanvas.height=128;
|
const signCanvas = document.createElement('canvas');
|
||||||
const ctx=signCanvas.getContext('2d');ctx.fillStyle='#24392d';ctx.fillRect(0,0,512,128);ctx.fillStyle='#f2dfae';ctx.textAlign='center';ctx.font='bold 42px Tahoma';ctx.fillText('ورودی جنگل · تپهٔ آرزوها',256,80);
|
signCanvas.width = 512;
|
||||||
const sign=part(gate,new THREE.PlaneGeometry(4,.9),new THREE.MeshBasicMaterial({map:new THREE.CanvasTexture(signCanvas),side:THREE.DoubleSide}),.25,3.4,0);sign.rotation.y=Math.PI/2;
|
signCanvas.height = 128;
|
||||||
const camp=new THREE.Group();camp.position.set(4,0,4.4);scene.add(camp);
|
const ctx = signCanvas.getContext('2d');
|
||||||
for(let i=0;i<12;i++){const a=i*Math.PI/6;part(camp,new THREE.DodecahedronGeometry(.24),stone,Math.cos(a)*.9,.16,Math.sin(a)*.9);}
|
ctx.fillStyle = '#24392d';
|
||||||
for(let i=0;i<4;i++){const log=box(camp,1.4,.16,.17,wood,0,.23,0);log.rotation.y=i*Math.PI/4;}
|
ctx.fillRect(0, 0, 512, 128);
|
||||||
|
ctx.fillStyle = '#f2dfae';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.font = 'bold 42px Tahoma';
|
||||||
|
ctx.fillText('ورودی جنگل · تپهٔ آرزوها', 256, 80);
|
||||||
|
const sign = part(gate, new THREE.PlaneGeometry(4, .9), new THREE.MeshBasicMaterial({
|
||||||
|
map: new THREE.CanvasTexture(signCanvas),
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
}), .25, 3.4, 0);
|
||||||
|
sign.rotation.y = Math.PI / 2;
|
||||||
|
const camp = new THREE.Group();
|
||||||
|
camp.position.set(4, 0, 4.4);
|
||||||
|
scene.add(camp);
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const a = i * Math.PI / 6;
|
||||||
|
part(camp, new THREE.DodecahedronGeometry(.24), stone, Math.cos(a) * .9, .16, Math.sin(a) * .9);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const log = box(camp, 1.4, .16, .17, wood, 0, .23, 0);
|
||||||
|
log.rotation.y = i * Math.PI / 4;
|
||||||
|
}
|
||||||
const flames = [];
|
const flames = [];
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
const flame=part(camp,new THREE.ConeGeometry(.18+(i%2)*.1,.8,7),new THREE.MeshBasicMaterial({color:i%2?'#ffb32f':'#f56528',transparent:true,opacity:.85}),Math.sin(i*2)*.3,.65,Math.cos(i*2)*.3);flames.push(flame);
|
const flame = part(camp, new THREE.ConeGeometry(.18 + (i % 2) * .1, .8, 7), new THREE.MeshBasicMaterial({
|
||||||
|
color: i % 2 ? '#ffb32f' : '#f56528',
|
||||||
|
transparent: true,
|
||||||
|
opacity: .85
|
||||||
|
}), Math.sin(i * 2) * .3, .65, Math.cos(i * 2) * .3);
|
||||||
|
flames.push(flame);
|
||||||
}
|
}
|
||||||
const light=new THREE.PointLight('#ff993d',8,10);light.position.y=1.2;camp.add(light);
|
const light = new THREE.PointLight('#ff993d', 8, 10);
|
||||||
|
light.position.y = 1.2;
|
||||||
|
camp.add(light);
|
||||||
const dancers = [];
|
const dancers = [];
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
const root=new THREE.Group(), a=i*Math.PI/3;root.position.set(Math.cos(a)*2.2,0,Math.sin(a)*2.2);root.rotation.y=-a-Math.PI/2;camp.add(root);
|
const root = new THREE.Group(), a = i * Math.PI / 3;
|
||||||
|
root.position.set(Math.cos(a) * 2.2, 0, Math.sin(a) * 2.2);
|
||||||
|
root.rotation.y = -a - Math.PI / 2;
|
||||||
|
camp.add(root);
|
||||||
const female = i % 2 === 0, cloth = mat(['#ae684a', '#698b84', '#c29d59', '#75829a', '#985b69', '#6f8158'][i]);
|
const female = i % 2 === 0, cloth = mat(['#ae684a', '#698b84', '#c29d59', '#75829a', '#985b69', '#6f8158'][i]);
|
||||||
part(root, new THREE.CylinderGeometry(.23, .20, .6, 10), cloth, 0, 1.15, 0);
|
part(root, new THREE.CylinderGeometry(.23, .20, .6, 10), cloth, 0, 1.15, 0);
|
||||||
part(root, new THREE.SphereGeometry(.22, 12, 10), skin, 0, 1.7, 0);
|
part(root, new THREE.SphereGeometry(.22, 12, 10), skin, 0, 1.7, 0);
|
||||||
@@ -47,13 +96,32 @@ export function createJourneyScenery(scene, pathZ) {
|
|||||||
if (female) part(root, new THREE.CylinderGeometry(.23, .4, .5, 12), cloth, 0, .8, 0);
|
if (female) part(root, new THREE.CylinderGeometry(.23, .4, .5, 12), cloth, 0, .8, 0);
|
||||||
for (const side of [-1, 1]) box(root, .14, .65, .17, black, side * .15, .4, 0);
|
for (const side of [-1, 1]) box(root, .14, .65, .17, black, side * .15, .4, 0);
|
||||||
const arms = [];
|
const arms = [];
|
||||||
for(const side of [-1,1]){const arm=new THREE.Group();arm.position.set(side*.27,1.4,0);root.add(arm);box(arm,.13,.6,.14,cloth,0,-.25,0);arms.push(arm);}
|
for (const side of [-1, 1]) {
|
||||||
|
const arm = new THREE.Group();
|
||||||
|
arm.position.set(side * .27, 1.4, 0);
|
||||||
|
root.add(arm);
|
||||||
|
box(arm, .13, .6, .14, cloth, 0, -.25, 0);
|
||||||
|
arms.push(arm);
|
||||||
|
}
|
||||||
const cup = part(arms[0], new THREE.CylinderGeometry(.07, .06, .2, 10), mat('#e6b853'), 0, -.58, 0);
|
const cup = part(arms[0], new THREE.CylinderGeometry(.07, .06, .2, 10), mat('#e6b853'), 0, -.58, 0);
|
||||||
dancers.push({root, arms, a});
|
dancers.push({root, arms, a});
|
||||||
}
|
}
|
||||||
return {car, update(time, phase, progress=0, lane=0) {
|
return {
|
||||||
|
car, update(time, phase, progress = 0, lane = 0) {
|
||||||
car.position.set(phase === 'home' || phase === 'boarding' ? -32 : phase === 'driving' ? -32 + progress * 21 : -11, .05, ['entrance', 'walking'].includes(phase) ? -4 : -.3 + lane - 3.7 * Math.pow(progress, 6));
|
car.position.set(phase === 'home' || phase === 'boarding' ? -32 : phase === 'driving' ? -32 + progress * 21 : -11, .05, ['entrance', 'walking'].includes(phase) ? -4 : -.3 + lane - 3.7 * Math.pow(progress, 6));
|
||||||
flames.forEach((f,i)=>{f.scale.y=.7+Math.sin(time*11+i)*.25;f.rotation.y=time+i;});light.intensity=7+Math.sin(time*17);
|
flames.forEach((f, i) => {
|
||||||
dancers.forEach(({root,arms,a},i)=>{root.position.y=Math.abs(Math.sin(time*3+i))*.12;root.rotation.z=Math.sin(time*3+i)*.09;arms.forEach((arm,j)=>{arm.rotation.z=(j?1:-1)*(.7+Math.sin(time*3+a+j)*.5);arm.rotation.x=Math.cos(time*2+i)*.3;});});
|
f.scale.y = .7 + Math.sin(time * 11 + i) * .25;
|
||||||
}};
|
f.rotation.y = time + i;
|
||||||
|
});
|
||||||
|
light.intensity = 7 + Math.sin(time * 17);
|
||||||
|
dancers.forEach(({root, arms, a}, i) => {
|
||||||
|
root.position.y = Math.abs(Math.sin(time * 3 + i)) * .12;
|
||||||
|
root.rotation.z = Math.sin(time * 3 + i) * .09;
|
||||||
|
arms.forEach((arm, j) => {
|
||||||
|
arm.rotation.z = (j ? 1 : -1) * (.7 + Math.sin(time * 3 + a + j) * .5);
|
||||||
|
arm.rotation.x = Math.cos(time * 2 + i) * .3;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -7,7 +7,11 @@ export const chapters = {
|
|||||||
},
|
},
|
||||||
bridge: {title: 'روی پل', lines: ['پدر: دستم رو بگیر؛ با هم رد میشیم.']},
|
bridge: {title: 'روی پل', lines: ['پدر: دستم رو بگیر؛ با هم رد میشیم.']},
|
||||||
fish: {title: 'زیر پل', lines: ['بچه: ماهیها رو ببین!']},
|
fish: {title: 'زیر پل', lines: ['بچه: ماهیها رو ببین!']},
|
||||||
camp: {title: 'دور آتش', lines: ['بچه: بابا! اون دخترها و پسرها دور آتیش میرقصن. نوشیدنیهاشون رو از کجا گرفتن؟', 'پدر: بریم بپرسیم. سلام! نوشیدنی رو از کجا خریدین؟', 'یکی از دوستان: سلام! کمی جلوتر یه دستگاه روبات مارکت هست؛ نوشیدنی و خوراکی داره.', 'بچه: پس ما هم از همونجا برای پیکنیک خرید کنیم!'], action: {value: 'ask', label: 'رفتن به دستگاه وندینگ'}},
|
camp: {
|
||||||
|
title: 'دور آتش',
|
||||||
|
lines: ['بچه: بابا! اون دخترها و پسرها دور آتیش میرقصن. نوشیدنیهاشون رو از کجا گرفتن؟', 'پدر: بریم بپرسیم. سلام! نوشیدنی رو از کجا خریدین؟', 'یکی از دوستان: سلام! کمی جلوتر یه دستگاه روبات مارکت هست؛ نوشیدنی و خوراکی داره.', 'بچه: پس ما هم از همونجا برای پیکنیک خرید کنیم!'],
|
||||||
|
action: {value: 'ask', label: 'رفتن به دستگاه وندینگ'}
|
||||||
|
},
|
||||||
courage: {title: 'اولین خرید من', lines: ['پدر: حالا برای پیکنیک خرید کنیم.']},
|
courage: {title: 'اولین خرید من', lines: ['پدر: حالا برای پیکنیک خرید کنیم.']},
|
||||||
wind: {title: 'باد و نقاشی', lines: ['بچه: نقاشیم رفت کنار بوته!']},
|
wind: {title: 'باد و نقاشی', lines: ['بچه: نقاشیم رفت کنار بوته!']},
|
||||||
paper: {
|
paper: {
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import {defineConfig} from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// Relative assets also load under a GitHub Pages repository path.
|
||||||
|
base: './'
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user