Replace child character with Robot Market mascot
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# تپهٔ آرزوها — روبات مارکت
|
# تپهٔ آرزوها — روبات مارکت
|
||||||
|
|
||||||
بازی سهبعدی JavaScript با Three.js: پدر و بچه در جنگل راه میروند، از دستگاه خرید میکنند و به پیکنیک میرسند. مدلها با کد ساخته شدهاند.
|
بازی سهبعدی JavaScript با Three.js: پدر و بچه در جنگل راه میروند، از دستگاه خرید میکنند و به پیکنیک میرسند. مدلها با کد ساخته شدهاند. شخصیت کودک با روبات فیروزهای و سفید Robot Market، الهامگرفته از تصویر مرجع، جایگزین شده است؛ پدر همراه او باقی میماند. روبات دارای صورت نورانی، آنتن و انیمیشن راهرفتن، دریافت محصول و نشستن است.
|
||||||
|
|
||||||
🎮 [اجرای آنلاین بازی](https://hamedabdollahzade.github.io/vending-machine-game/)
|
🎮 [اجرای آنلاین بازی](https://hamedabdollahzade.github.io/vending-machine-game/)
|
||||||
|
|
||||||
|
|||||||
+75
-1
@@ -33,6 +33,80 @@ export function createFamily() {
|
|||||||
bone.mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize());
|
bone.mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Procedural Robot Market mascot: the child-sized playable companion.
|
||||||
|
function makeRobot(z) {
|
||||||
|
const scale = .69, root = new THREE.Group(), torso = new THREE.Group();
|
||||||
|
root.name = 'robot-market-mascot';
|
||||||
|
root.position.z = z;
|
||||||
|
group.add(root);
|
||||||
|
root.add(torso);
|
||||||
|
const teal = new THREE.MeshStandardMaterial({color: '#079f98', metalness: .4, roughness: .28});
|
||||||
|
const white = new THREE.MeshStandardMaterial({color: '#edf6f4', metalness: .22, roughness: .32});
|
||||||
|
const joint = new THREE.MeshStandardMaterial({color: '#102b30', metalness: .5, roughness: .35});
|
||||||
|
const glow = new THREE.MeshStandardMaterial({color: '#c6f7ff', emissive: '#23bcff', emissiveIntensity: 2.5, roughness: .2});
|
||||||
|
const visor = new THREE.MeshStandardMaterial({color: '#020d16', metalness: .5, roughness: .16});
|
||||||
|
sphere(torso, .25, teal, [0, .76, 0], [1, .65, .75]);
|
||||||
|
sphere(torso, .31, white, [0, .97, 0], [1, 1.05, .72]);
|
||||||
|
for (const side of [-1, 1]) sphere(torso, .17, teal, [side * .24, 1.03, -.04], [.55, 1, 1]);
|
||||||
|
shape(torso, new THREE.CylinderGeometry(.09, .09, .12, 12), joint, [0, 1.27, 0]);
|
||||||
|
const head = new THREE.Group();
|
||||||
|
head.position.y = 1.61;
|
||||||
|
torso.add(head);
|
||||||
|
sphere(head, .43, teal, [0, 0, 0], [1.12, .91, .88]);
|
||||||
|
sphere(head, .40, white, [0, -.015, .12], [1.12, .86, .77]);
|
||||||
|
sphere(head, .35, visor, [0, -.015, .255], [1.13, .83, .48]);
|
||||||
|
const eyes = [];
|
||||||
|
function arc(parent, radius, thickness, pos, rotation = 0, material = glow) {
|
||||||
|
const line = shape(parent, new THREE.TorusGeometry(radius, thickness, 8, 24, Math.PI), material, pos);
|
||||||
|
line.rotation.z = rotation;
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
for (const side of [-1, 1]) {
|
||||||
|
eyes.push(arc(head, .065, .016, [side * .16, .025, .423]));
|
||||||
|
const ear = shape(head, new THREE.CylinderGeometry(.14, .14, .085, 24), joint, [side * .47, 0, 0]);
|
||||||
|
ear.rotation.z = Math.PI / 2;
|
||||||
|
const ring = shape(head, new THREE.TorusGeometry(.105, .012, 8, 24), glow, [side * .518, 0, 0]);
|
||||||
|
ring.rotation.y = Math.PI / 2;
|
||||||
|
sphere(head, .12, teal, [side * .49, 0, 0], [.4, 1, 1]);
|
||||||
|
}
|
||||||
|
arc(head, .075, .016, [0, -.085, .425], Math.PI);
|
||||||
|
sphere(head, .10, teal, [0, .38, 0], [1, .55, 1]);
|
||||||
|
shape(head, new THREE.CylinderGeometry(.017, .022, .18, 10), teal, [0, .49, 0]);
|
||||||
|
sphere(head, .055, teal, [0, .60, 0]);
|
||||||
|
// Draw the chest mark as a texture so it remains attached to the 3D body.
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 256; canvas.height = 256;
|
||||||
|
const c = canvas.getContext('2d');
|
||||||
|
if (c) {
|
||||||
|
c.strokeStyle = '#102b30'; c.fillStyle = '#102b30';
|
||||||
|
c.lineWidth = 12; c.lineJoin = 'round';
|
||||||
|
c.beginPath(); c.moveTo(65, 40); c.lineTo(82, 40); c.lineTo(99, 110); c.lineTo(170, 110); c.lineTo(186, 58); c.lineTo(89, 58); c.stroke();
|
||||||
|
for (const x of [111, 162]) {c.beginPath(); c.arc(x, 130, 9, 0, Math.PI * 2); c.fill();}
|
||||||
|
c.textAlign = 'center'; c.font = 'bold 34px sans-serif';
|
||||||
|
c.fillText('ROBOT', 128, 181); c.fillText('MARKET', 128, 218);
|
||||||
|
shape(torso, new THREE.PlaneGeometry(.31, .31), new THREE.MeshBasicMaterial({map: new THREE.CanvasTexture(canvas), transparent: true, depthWrite: false}), [0, .99, .224]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const legs = [-1, 1].map(side => {
|
||||||
|
const hip = new THREE.Group(); hip.position.set(side * .15, .65, 0); root.add(hip);
|
||||||
|
sphere(hip, .09, joint, [0, 0, 0]);
|
||||||
|
shape(hip, new THREE.CapsuleGeometry(.095, .12, 4, 12), white, [0, -.12, 0]);
|
||||||
|
const knee = new THREE.Group(); knee.position.y = -.25; hip.add(knee);
|
||||||
|
sphere(knee, .105, teal, [0, 0, .035]);
|
||||||
|
shape(knee, new THREE.CapsuleGeometry(.10, .12, 4, 12), white, [0, -.13, 0]);
|
||||||
|
const foot = sphere(knee, .13, teal, [0, -.29, .07], [1, .65, 1.6]);
|
||||||
|
sphere(knee, .13, joint, [0, -.34, .07], [1.06, .22, 1.65]);
|
||||||
|
return {hip, knee, foot};
|
||||||
|
});
|
||||||
|
const arms = [-1, 1].map(() => ({
|
||||||
|
upper: segment(group, null, null, .09, teal),
|
||||||
|
lower: segment(group, null, null, .085, white),
|
||||||
|
hand: sphere(group, .09, joint, [0, 0, 0], [1, 1.1, 1])
|
||||||
|
}));
|
||||||
|
return {root, torso, head, legs, arms, scale, z, eyes};
|
||||||
|
}
|
||||||
|
|
||||||
function makePerson(isChild, z) {
|
function makePerson(isChild, z) {
|
||||||
const scale = isChild ? .69 : 1.05, root = new THREE.Group();
|
const scale = isChild ? .69 : 1.05, root = new THREE.Group();
|
||||||
root.position.z = z;
|
root.position.z = z;
|
||||||
@@ -95,7 +169,7 @@ export function createFamily() {
|
|||||||
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);
|
const father = makePerson(false, -.45), child = makeRobot(.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}) {
|
||||||
|
|||||||
@@ -30,3 +30,15 @@ test('Sixty channels, exactly 21–60 purchasable, Hetoman conversion',async()=>
|
|||||||
assert.equal(productByCode('۲۲').channel,22);assert.equal(productByCode('٦٠').channel,60);assert.equal(productByCode('61'),null);
|
assert.equal(productByCode('۲۲').channel,22);assert.equal(productByCode('٦٠').channel,60);assert.equal(productByCode('61'),null);
|
||||||
const s=createState();assert.equal(purchase(s,'channel-20'),null);assert.equal(s.balance,500);
|
const s=createState();assert.equal(purchase(s,'channel-20'),null);assert.equal(s.balance,500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Robot Market mascot replaces the child and keeps its articulated rig',()=>{
|
||||||
|
const family=createFamily();
|
||||||
|
const robot=family.group.getObjectByName('robot-market-mascot');
|
||||||
|
assert.ok(robot);
|
||||||
|
assert.equal(family.group.children.filter(node=>node.type==='Group').length,2);
|
||||||
|
for(const mode of ['walk','shop','sit']){
|
||||||
|
family.update({time:2,gait:1,dt:.016,moving:mode==='walk',facing:Math.PI/2,shopping:mode==='shop',delivery:mode==='shop',finished:mode==='sit'});
|
||||||
|
family.group.updateMatrixWorld(true);
|
||||||
|
robot.traverse(node=>assert.ok(node.matrixWorld.elements.every(Number.isFinite)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user