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>
|
||||
+24
-21
@@ -41,26 +41,29 @@ export function createFamily() {
|
||||
root.add(torso);
|
||||
const cloth = mat(isChild ? '#d98150' : '#e4c695'), pants = mat(isChild ? '#527463' : '#496474');
|
||||
// Tapered jacket, hips and neck replace the single rounded toy-like body.
|
||||
shape(torso,new THREE.CylinderGeometry(.255*scale,.22*scale,.62*scale,16),cloth,[0,1.2*scale,0]).scale.z=.72;
|
||||
sphere(torso,.23,pants,[0,.88*scale,0],[scale,.55*scale,.72*scale]);
|
||||
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;
|
||||
shape(torso, new THREE.CylinderGeometry(.255 * scale, .22 * scale, .62 * scale, 16), cloth, [0, 1.2 * scale, 0]).scale.z = .72;
|
||||
sphere(torso, .23, pants, [0, .88 * scale, 0], [scale, .55 * scale, .72 * scale]);
|
||||
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 head = new THREE.Group();
|
||||
head.position.y = 1.77 * scale;
|
||||
torso.add(head);
|
||||
head.scale.setScalar(isChild?.9:.77);
|
||||
const eyes=[];
|
||||
head.scale.setScalar(isChild ? .9 : .77);
|
||||
const eyes = [];
|
||||
sphere(head, .27 * scale, skin, [0, 0, 0]);
|
||||
sphere(head, .275 * scale, hair, [0, .11 * scale, -.035 * scale], [1, .65, 1]);
|
||||
sphere(head, .058 * scale, skin, [0, -.02 * scale, .25 * scale]);
|
||||
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;
|
||||
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;
|
||||
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]);
|
||||
@@ -89,31 +92,31 @@ export function createFamily() {
|
||||
lower: segment(group, null, null, .061 * scale, skin),
|
||||
hand: sphere(group, .075 * scale, skin, [0, 0, 0])
|
||||
}));
|
||||
return {root, torso, head, legs, arms, scale, z,eyes};
|
||||
return {root, torso, head, legs, arms, scale, z, eyes};
|
||||
}
|
||||
|
||||
const father = makePerson(false, -.45), child = makePerson(true, .45);
|
||||
|
||||
|
||||
function update({time, gait, moving, facing, shopping, delivery, finished, watchRabbit,dt=1/60}) {
|
||||
function update({time, gait, moving, facing, shopping, delivery, finished, watchRabbit, dt = 1 / 60}) {
|
||||
const actors = [father, child];
|
||||
actors.forEach((actor, index) => {
|
||||
const blend=1-Math.exp(-dt*7);
|
||||
actor.root.position.x=THREE.MathUtils.lerp(actor.root.position.x,shopping?(index===0?-.55:.25):0,blend);
|
||||
actor.root.position.y=THREE.MathUtils.lerp(actor.root.position.y,finished?-.32:0,blend);
|
||||
actor.root.position.z=actor.z;
|
||||
const blend = 1 - Math.exp(-dt * 7);
|
||||
actor.root.position.x = THREE.MathUtils.lerp(actor.root.position.x, shopping ? (index === 0 ? -.55 : .25) : 0, blend);
|
||||
actor.root.position.y = THREE.MathUtils.lerp(actor.root.position.y, finished ? -.32 : 0, blend);
|
||||
actor.root.position.z = actor.z;
|
||||
actor.root.rotation.y = facing;
|
||||
const bob = moving ? Math.abs(Math.sin(gait)) * .022 : Math.sin(time * 2) * .006;
|
||||
actor.torso.position.y = bob;
|
||||
actor.torso.rotation.z=moving?Math.sin(gait)*.018:0;
|
||||
const blinking=Math.sin(time*1.3+index)>.993;
|
||||
actor.eyes.forEach(eye=>eye.scale.y=blinking?.12:1);
|
||||
actor.torso.rotation.z = moving ? Math.sin(gait) * .018 : 0;
|
||||
const blinking = Math.sin(time * 1.3 + index) > .993;
|
||||
actor.eyes.forEach(eye => eye.scale.y = blinking ? .12 : 1);
|
||||
actor.head.rotation.y = shopping ? -.65 : watchRabbit ? .55 : Math.sin(time * .7) * .06;
|
||||
actor.torso.rotation.x = delivery && index === 1 ? .22 : 0;
|
||||
actor.legs.forEach((leg, i) => {
|
||||
const phase = gait + i * Math.PI;
|
||||
leg.hip.rotation.x = finished ? -1.3 : moving ? Math.sin(phase) * .34 : 0;
|
||||
leg.foot.rotation.x=moving?Math.sin(phase+.5)*.12:0;
|
||||
leg.foot.rotation.x = moving ? Math.sin(phase + .5) * .12 : 0;
|
||||
leg.knee.rotation.x = finished ? 1.5 : moving ? Math.max(0, -Math.sin(phase)) * .5 : .05;
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+2
-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}) {
|
||||
|
||||
export function canWalk({shopOpen = false, notebookOpen = false, endingOpen = false, busy = false, finished = false}) {
|
||||
return !shopOpen && !notebookOpen && !endingOpen && !busy && !finished;
|
||||
}
|
||||
|
||||
+23
-15
@@ -3,23 +3,31 @@ export const formatHetoman = value => `${value.toLocaleString('fa-IR')} هتوم
|
||||
export const hetomanToRial = value => value * 10000;
|
||||
|
||||
const assortment = [
|
||||
{id: 'juice', code:'11', name: 'آبمیوه', icon: '🧃', cost: 35000, type: 'drink'},
|
||||
{id: 'water', code:'12', name: 'آب معدنی', icon: '💧', cost: 15000, type: 'drink'},
|
||||
{id: 'biscuit', code:'13', name: 'بیسکویت', icon: '🍪', cost: 25000, type: 'snack'},
|
||||
{id:'milk',code:'14',name:'شیر کاکائو',icon:'🥛',cost:40000,type:'drink'},
|
||||
{id:'tea',code:'15',name:'چای سرد',icon:'🧋',cost:45000,type:'drink'},
|
||||
{id:'lemonade',code:'16',name:'لیموناد',icon:'🍋',cost:40000,type:'drink'},
|
||||
{id:'chocolate',code:'17',name:'شکلات',icon:'🍫',cost:30000,type:'snack'},
|
||||
{id:'nuts',code:'18',name:'آجیل',icon:'🥜',cost:65000,type:'snack'},
|
||||
{id:'cake',code:'19',name:'کیک',icon:'🧁',cost:35000,type:'snack'}
|
||||
{id: 'juice', code: '11', name: 'آبمیوه', icon: '🧃', cost: 35000, type: 'drink'},
|
||||
{id: 'water', code: '12', name: 'آب معدنی', icon: '💧', cost: 15000, type: 'drink'},
|
||||
{id: 'biscuit', code: '13', name: 'بیسکویت', icon: '🍪', cost: 25000, type: 'snack'},
|
||||
{id: 'milk', code: '14', name: 'شیر کاکائو', icon: '🥛', cost: 40000, type: 'drink'},
|
||||
{id: 'tea', code: '15', name: 'چای سرد', icon: '🧋', cost: 45000, type: 'drink'},
|
||||
{id: 'lemonade', code: '16', name: 'لیموناد', icon: '🍋', cost: 40000, type: 'drink'},
|
||||
{id: 'chocolate', code: '17', name: 'شکلات', icon: '🍫', cost: 30000, type: 'snack'},
|
||||
{id: 'nuts', code: '18', name: 'آجیل', icon: '🥜', cost: 65000, type: 'snack'},
|
||||
{id: 'cake', code: '19', name: 'کیک', icon: '🧁', cost: 35000, type: 'snack'}
|
||||
];
|
||||
|
||||
// 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};
|
||||
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
|
||||
};
|
||||
});
|
||||
export const products=channels.filter(channel=>channel.available);
|
||||
export const products = channels.filter(channel => channel.available);
|
||||
|
||||
export function createState() {
|
||||
return {balance: INITIAL_BALANCE, basket: [], busy: false, finished: false, recycled: false};
|
||||
@@ -43,6 +51,6 @@ export function purchase(state, id) {
|
||||
}
|
||||
|
||||
export function productByCode(value) {
|
||||
const normalized=String(value).trim().replace(/[۰-۹]/g,c=>String('۰۱۲۳۴۵۶۷۸۹'.indexOf(c))).replace(/[٠-٩]/g,c=>String('٠١٢٣٤٥٦٧٨٩'.indexOf(c)));
|
||||
return products.find(p=>p.code===normalized) ?? null;
|
||||
const normalized = String(value).trim().replace(/[۰-۹]/g, c => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(c))).replace(/[٠-٩]/g, c => String('٠١٢٣٤٥٦٧٨٩'.indexOf(c)));
|
||||
return products.find(p => p.code === normalized) ?? null;
|
||||
}
|
||||
|
||||
+705
-248
File diff suppressed because it is too large
Load Diff
+32
-12
@@ -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 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;
|
||||
|
||||
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 cancelPayment(state,payment){
|
||||
if(!['approaching','ready'].includes(payment.phase))return false;
|
||||
payment.phase='idle';payment.product=null;state.busy=false;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;
|
||||
}
|
||||
|
||||
export function cancelPayment(state, payment) {
|
||||
if (!['approaching', 'ready'].includes(payment.phase)) return false;
|
||||
payment.phase = 'idle';
|
||||
payment.product = null;
|
||||
state.busy = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
+66
-27
@@ -1,35 +1,74 @@
|
||||
export const chapters = {
|
||||
morning:{title:'تپهٔ آرزوها',lines:['پدر: بریم تپه؟ امروز راه رو با هم کشف میکنیم.']},
|
||||
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:'ادامهٔ مسیر'}},
|
||||
sunny:{title:'رنگهای جنگل',lines:['بچه: این رنگها رو توی نقاشیم میکشم!']},
|
||||
shade:{title:'درخت خاطرهها',lines:['پدر: بچه که بودم هم زیر همین درخت مینشستم.']},
|
||||
summit:{title:'کنار هم',lines:['بچه: دفعهٔ بعد، من راه رو نشونت میدم!'],action:{value:'gold',label:'کامل کردن نقاشی'}}
|
||||
morning: {title: 'تپهٔ آرزوها', lines: ['پدر: بریم تپه؟ امروز راه رو با هم کشف میکنیم.']},
|
||||
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: 'ادامهٔ مسیر'}
|
||||
},
|
||||
sunny: {title: 'رنگهای جنگل', lines: ['بچه: این رنگها رو توی نقاشیم میکشم!']},
|
||||
shade: {title: 'درخت خاطرهها', lines: ['پدر: بچه که بودم هم زیر همین درخت مینشستم.']},
|
||||
summit: {
|
||||
title: 'کنار هم',
|
||||
lines: ['بچه: دفعهٔ بعد، من راه رو نشونت میدم!'],
|
||||
action: {value: 'gold', label: 'کامل کردن نقاشی'}
|
||||
}
|
||||
};
|
||||
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;
|
||||
|
||||
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;
|
||||
story.done.push(id);
|
||||
if(id==='rabbit')story.rabbitChoice=choice;
|
||||
if(id==='fork')story.route=choice;
|
||||
if(id==='summit')story.sky=choice;
|
||||
if (id === 'rabbit') story.rabbitChoice = choice;
|
||||
if (id === 'fork') story.route = 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';
|
||||
for(const [id,threshold] of [['rabbit',-3.7],['bridge',-2.2],['fish',0],['courage',2.7],['wind',12],['paper',14],['fork',16]]){
|
||||
if(!done(id))return x>=threshold?id:null;
|
||||
|
||||
export function nextChapter(story, x) {
|
||||
const done = id => story.done.includes(id);
|
||||
if (!done('morning')) return 'morning';
|
||||
for (const [id, threshold] of [['rabbit', -3.7], ['bridge', -2.2], ['fish', 0], ['courage', 2.7], ['wind', 12], ['paper', 14], ['fork', 16]]) {
|
||||
if (!done(id)) return x >= threshold ? id : null;
|
||||
}
|
||||
if(!done(story.route))return x>=19?story.route:null;
|
||||
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