Wildlife Kingdom — original browser zoo-management sim
- Procedural canvas art: 15 species × 4 poses, 15 buildings, modular terrain, UI icons - Habitat enclosure detection, biome suitability, staff/guest AI pathfinding - Economy: tickets, shops, wages, star rating, research tiers, 15 missions - Day/night cycle, particles, WebAudio soundtrack & synth SFX - Mouse + touch controls, responsive UI, localStorage autosave - Verified: 31/31 logic tests (test/smoke.js) · 12/12 render audits (test/qa.html)
This commit is contained in:
+483
@@ -0,0 +1,483 @@
|
||||
/* ============================================================
|
||||
Wildlife Kingdom — js/entities.js
|
||||
Animals · Guests · Keepers · Vets · Particles
|
||||
All coordinates are float tile-space; drawing projects them.
|
||||
============================================================ */
|
||||
(function(){
|
||||
'use strict';
|
||||
const WK = window.WK;
|
||||
const D = WK.Data;
|
||||
const HW = WK.HW, HH = WK.HH;
|
||||
|
||||
WK.ANIMAL_SIZE = {
|
||||
deer:.95, zebra:.95, monkey:.78, flamingo:.78, penguin:.68, crocodile:.9,
|
||||
giraffe:1.12, lion:1, hippo:1.08, tiger:.98, rhino:1.05, elephant:1.22,
|
||||
gorilla:.92, polarbear:1.08, panda:.88,
|
||||
};
|
||||
|
||||
let NEXT_ID = 1;
|
||||
const R = Math.random;
|
||||
|
||||
/* ============================================================
|
||||
ANIMAL
|
||||
============================================================ */
|
||||
class Animal{
|
||||
constructor(speciesId, x, y, regionId){
|
||||
const sp = D.SPECIES[speciesId];
|
||||
this.id = NEXT_ID++;
|
||||
this.kind='animal';
|
||||
this.species = speciesId;
|
||||
this.sp = sp;
|
||||
this.name = D.ANIMAL_NAMES[(R()*D.ANIMAL_NAMES.length)|0];
|
||||
this.x=x; this.y=y;
|
||||
this.face = R()<.5?-1:1;
|
||||
this.state='idle';
|
||||
this.stateT = 1+R()*4;
|
||||
this.phase = R()*10;
|
||||
this.tOff = R()*100;
|
||||
this.hunger = 20+R()*20;
|
||||
this.happiness = 62;
|
||||
this.happyT = 0; // remaining happy-burst time
|
||||
this.eatT = 0;
|
||||
this.sick = false;
|
||||
this.regionId = regionId;
|
||||
this.wp=null;
|
||||
}
|
||||
get size(){ return WK.ANIMAL_SIZE[this.species]||1; }
|
||||
|
||||
startEating(){
|
||||
this.eatT = 3;
|
||||
this.hunger = 4;
|
||||
this.happyT = 3.5;
|
||||
this._taken = false;
|
||||
}
|
||||
makeHappy(){ this.happyT = Math.max(this.happyT, 2.6); }
|
||||
|
||||
update(sdt, game){
|
||||
const sp=this.sp;
|
||||
// needs drift
|
||||
this.hunger = WK.clamp(this.hunger + sdt*0.19, 0, 100);
|
||||
let target = game.habitatScore(this);
|
||||
if(this.hunger>55) target -= (this.hunger-55)*0.85;
|
||||
if(this.sick) target -= 38;
|
||||
target = WK.clamp(target,5,98);
|
||||
this.happiness += (target-this.happiness)*Math.min(1,sdt*0.07);
|
||||
|
||||
if(this.happyT>0) this.happyT-=sdt;
|
||||
if(this.eatT>0){ this.eatT-=sdt; this.state='eat'; return; }
|
||||
if(this.happyT>0 && this.state!=='walk'){ this.state='happy'; this.stateT-=sdt; if(this.stateT<=0)this.state='idle'; return; }
|
||||
|
||||
this.stateT -= sdt;
|
||||
if(this.state==='idle'){
|
||||
if(this.stateT<=0){
|
||||
// choose a stroll
|
||||
const dest = game.world.randomRegionTile(this.regionId, sp, R);
|
||||
if(dest){
|
||||
const path = game.world.findPathAnimal(sp, this.x, this.y, dest[0], dest[1], this.regionId);
|
||||
if(path&&path.length){ this.wp=path; this.state='walk'; }
|
||||
}
|
||||
this.stateT = 3.5+R()*7;
|
||||
}
|
||||
}else if(this.state==='walk'){
|
||||
if(!this.wp||!this.wp.length){ this.state='idle'; this.stateT=2+R()*4; return; }
|
||||
const [tx,ty]=this.wp[0];
|
||||
const dx=tx-this.x, dy=ty-this.y;
|
||||
const d=Math.hypot(dx,dy);
|
||||
const step=sp.speed*sdt;
|
||||
if(d<=step){ this.x=tx; this.y=ty; this.wp.shift(); if(!this.wp.length){this.state='idle'; this.stateT=2.5+R()*6;} }
|
||||
else{ this.x+=dx/d*step; this.y+=dy/d*step; }
|
||||
const screenDx=(tx-this.x)-(ty-this.y);
|
||||
if(Math.abs(screenDx)>0.05) this.face = screenDx>0?1:-1;
|
||||
this.phase += step*5.2;
|
||||
}
|
||||
}
|
||||
|
||||
draw(ctx, cam, time, groundId){
|
||||
const p = cam.project(this.x,this.y);
|
||||
if(!cam.isVisible(p.x,p.y,80)) return;
|
||||
const s=this.size;
|
||||
const g = WK.Data.GROUND[groundId||'grass'];
|
||||
const swimming = g.water && this.sp.swim;
|
||||
ctx.save();
|
||||
ctx.translate(p.x,p.y);
|
||||
// shadow (not for swimmers)
|
||||
if(!swimming){
|
||||
ctx.fillStyle='rgba(50,40,15,.26)';
|
||||
ctx.beginPath(); ctx.ellipse(0,1,15*s,5.5*s,0,0,WK.TAU); ctx.fill();
|
||||
}
|
||||
ctx.scale(this.face*s, s);
|
||||
if(swimming){
|
||||
ctx.save();
|
||||
ctx.beginPath(); ctx.rect(-48,-72,96,64); ctx.clip(); // hide feet under waterline
|
||||
WK.Sprites.drawAnimal(ctx, this.species, {
|
||||
state:this.state==='walk'?'walk':this.state==='eat'?'eat':this.state==='happy'?'happy':'idle',
|
||||
ph:this.phase, t:time+this.tOff,
|
||||
});
|
||||
ctx.restore();
|
||||
// ripples at waterline
|
||||
ctx.strokeStyle='rgba(255,255,255,.55)'; ctx.lineWidth=1.6;
|
||||
const rr=(14+Math.sin(time*3+this.tOff)*2)*s;
|
||||
ctx.beginPath(); ctx.ellipse(0,-6*s,rr,rr*0.36,0,0,WK.TAU); ctx.stroke();
|
||||
ctx.beginPath(); ctx.ellipse(0,-6*s,rr*.6,rr*.2,0,0,WK.TAU); ctx.stroke();
|
||||
}else{
|
||||
WK.Sprites.drawAnimal(ctx, this.species, {
|
||||
state:this.state==='walk'?'walk':this.state==='eat'?'eat':this.state==='happy'?'happy':'idle',
|
||||
ph:this.phase, t:time+this.tOff,
|
||||
});
|
||||
}
|
||||
ctx.restore();
|
||||
// status markers
|
||||
if(this.sick || this.hunger>82){
|
||||
ctx.font='bold 13px sans-serif'; ctx.textAlign='center';
|
||||
ctx.fillText(this.sick?'🤒':'🍖', p.x, p.y-46*s + Math.sin(time*4)*1.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WK.Animal = Animal;
|
||||
|
||||
/* ============================================================
|
||||
GUEST
|
||||
============================================================ */
|
||||
const SKINS=['#f5d5b8','#eec39a','#d9a06b','#a9744f','#8a5a3a'];
|
||||
const SHIRTS=['#e8624e','#ffc93c','#63b34c','#69c3ef','#b78ae8','#ff8aa8','#ff9d47','#5fb8a8'];
|
||||
const PANTS=['#4a6a8a','#6b4a8a','#7a6a55','#43362a','#5a7a4a'];
|
||||
const HAIRS=['#5a3c22','#2e2a26','#a9743f','#d9b97e','#8a3a2a','#e8e2d4'];
|
||||
|
||||
class Guest{
|
||||
constructor(game, ex, ey){
|
||||
this.id=NEXT_ID++;
|
||||
this.kind='guest';
|
||||
this.game=game;
|
||||
this.x=ex; this.y=ey;
|
||||
this.skin=SKINS[(R()*SKINS.length)|0];
|
||||
this.shirt=SHIRTS[(R()*SHIRTS.length)|0];
|
||||
this.pants=PANTS[(R()*PANTS.length)|0];
|
||||
this.hair=HAIRS[(R()*HAIRS.length)|0];
|
||||
this.kid=R()<0.3;
|
||||
this.balloon=R()<0.22?(SHIRTS[(R()*SHIRTS.length)|0]):null;
|
||||
this.joy=58+R()*10;
|
||||
this.speed=(this.kid?1.25:1.6)*(0.9+R()*0.25);
|
||||
this.lane=(R()-0.5)*0.24;
|
||||
this.state='walk';
|
||||
this.phase=R()*10;
|
||||
this.itin=[];
|
||||
const stops=2+(R()*4|0);
|
||||
for(let i=0;i<stops;i++) this.itin.push('poi');
|
||||
this.leaving=false;
|
||||
this.wantsRestroom=false;
|
||||
this.restroomWarned=false;
|
||||
this.dwellT=0;
|
||||
this.poi=null;
|
||||
this.wp=null;
|
||||
this.waitT=0.5+R();
|
||||
}
|
||||
setPathTo(tx,ty){
|
||||
const w=this.game.world;
|
||||
this.wp=w.findPathVisitor(Math.round(this.x),Math.round(this.y),tx,ty);
|
||||
if(this.wp&&this.wp.length===0) this.wp=[[tx,ty]];
|
||||
return !!this.wp;
|
||||
}
|
||||
pickNext(){
|
||||
const pois=this.game.pois;
|
||||
if(!pois||!pois.length){ this.leave(); return; }
|
||||
let pool=[];
|
||||
if(this.wantsRestroom){
|
||||
const rr=pois.filter(p=>p.type==='restroom');
|
||||
if(rr.length) pool=rr.map(p=>({p,w:50}));
|
||||
}
|
||||
if(!pool.length){
|
||||
for(const p of pois){
|
||||
let wgt=0;
|
||||
if(p.type==='view') wgt=6+p.appeal*2.2;
|
||||
else if(p.type==='shop') wgt=3+p.income*0.5+p.joy;
|
||||
else if(p.type==='visit') wgt=1.5+p.joy*0.5;
|
||||
else if(p.type==='bench') wgt=2;
|
||||
else if(p.type==='restroom') wgt=this.wantsRestroom?40:1.2;
|
||||
pool.push({p,w:wgt||0.5});
|
||||
}
|
||||
}
|
||||
const pick=WK.weightedPick(pool,R).p;
|
||||
this.poi=pick;
|
||||
if(this.setPathTo(pick.x,pick.y)){
|
||||
this.state='walk';
|
||||
}else{
|
||||
// unreachable: skip stop
|
||||
if(this.itin.length)this.itin.pop();
|
||||
if(!this.itin.length) this.leave();
|
||||
else this.pickNext();
|
||||
}
|
||||
}
|
||||
leave(){
|
||||
this.leaving=true; this.poi=null;
|
||||
const e=this.game.exitTile;
|
||||
if(e) this.setPathTo(e[0],e[1]);
|
||||
}
|
||||
arriveAtPoi(){
|
||||
const p=this.poi;
|
||||
this.dwellT=2.2+R()*3.5;
|
||||
this.state='dwell';
|
||||
this.act='stand';
|
||||
if(R()<0.3) this.act='photo';
|
||||
if(!p) return;
|
||||
const g=this.game;
|
||||
if(p.type==='view'){
|
||||
this.joy=WK.clamp(this.joy+3.5+p.appeal*0.85*(p.quality||1),0,100);
|
||||
this.act=R()<0.45?'photo':'point';
|
||||
if(R()<0.3){ g.puffHeart(this.x,this.y); }
|
||||
}else if(p.type==='shop'){
|
||||
const def=D.BUILDING[p.btype];
|
||||
this.joy=WK.clamp(this.joy+def.joy*0.65,0,100);
|
||||
const spend=Math.round(def.income*(0.8+R()*0.4));
|
||||
g.earnMoney(spend, 'shop:'+p.btype);
|
||||
g.coinBurst(p.x,p.y);
|
||||
if(['burger','icecream','restaurant'].includes(p.btype)) this.wantsRestroom=true;
|
||||
}else if(p.type==='visit'){
|
||||
const def=D.BUILDING[p.btype];
|
||||
this.joy=WK.clamp(this.joy+def.joy*0.4,0,100);
|
||||
}else if(p.type==='restroom'){
|
||||
this.wantsRestroom=false;
|
||||
this.joy=WK.clamp(this.joy+3,0,100);
|
||||
}else if(p.type==='bench'){
|
||||
this.joy=WK.clamp(this.joy+2.5,0,100);
|
||||
this.act='sit';
|
||||
this.dwellT=3.5+R()*3;
|
||||
}
|
||||
}
|
||||
update(sdt){
|
||||
if(this.waitT>0){ this.waitT-=sdt; return; }
|
||||
// slow boredom / crowd stress
|
||||
this.joy=WK.clamp(this.joy - sdt*0.16 - (this.game.crowdStress||0)*sdt, 0, 100);
|
||||
if(this.state==='walk'){
|
||||
if(!this.wp){
|
||||
if(this.leaving){ this.done=true; return; }
|
||||
this.pickNext(); if(!this.wp) return;
|
||||
}
|
||||
if(!this.wp.length){
|
||||
if(this.leaving){ this.done=true; return; }
|
||||
this.arriveAtPoi();
|
||||
return;
|
||||
}
|
||||
const [tx,ty]=this.wp[0];
|
||||
const dx=tx-this.x, dy=ty-this.y;
|
||||
const d=Math.hypot(dx,dy);
|
||||
const step=this.speed*sdt;
|
||||
if(d<=step){ this.x=tx; this.y=ty; this.wp.shift(); }
|
||||
else{
|
||||
// gentle lane offset for organic crowds
|
||||
const px=-dy/d*this.lane, py=dx/d*this.lane;
|
||||
this.x+=(dx/d)*step+px*sdt*0.6;
|
||||
this.y+=(dy/d)*step+py*sdt*0.6;
|
||||
const sdx=dx-dy;
|
||||
if(Math.abs(sdx)>0.05) this.faceDir=sdx>0?1:-1;
|
||||
}
|
||||
this.phase+=step*5.5;
|
||||
this.act='walk';
|
||||
}else if(this.state==='dwell'){
|
||||
this.dwellT-=sdt;
|
||||
this.phase+=sdt*1.4;
|
||||
if(this.dwellT<=0){
|
||||
this.poi=null;
|
||||
if(this.itin.length){ this.itin.pop(); this.pickNext(); }
|
||||
else this.leave();
|
||||
this.state='walk';
|
||||
}
|
||||
}
|
||||
if(this.leaving && (!this.wp || !this.wp.length) && this.state!=='dwell'){
|
||||
this.done=true;
|
||||
}
|
||||
}
|
||||
draw(ctx,cam,time){
|
||||
const p=cam.project(this.x,this.y);
|
||||
if(!cam.isVisible(p.x,p.y,40)) return;
|
||||
ctx.save();
|
||||
ctx.translate(p.x,p.y);
|
||||
let act=this.act||'stand';
|
||||
if(this.state==='walk') act='walk';
|
||||
if(act==='sit') ctx.translate(0,-6);
|
||||
WK.Sprites.drawPerson(ctx,{
|
||||
phase:this.phase, state:act,
|
||||
shirt:this.shirt, pants:this.pants, skin:this.skin, hair:this.hair,
|
||||
kid:this.kid, balloon:this.balloon, face:this.faceDir||1,
|
||||
smile:this.joy>45,
|
||||
});
|
||||
if(this.joy<25 && R()<0.02){
|
||||
ctx.fillStyle='rgba(90,110,220,.8)';
|
||||
ctx.font='bold 10px sans-serif'; ctx.textAlign='center';
|
||||
ctx.fillText('💧',8,-34);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
WK.Guest = Guest;
|
||||
|
||||
/* ============================================================
|
||||
STAFF (keeper / vet)
|
||||
============================================================ */
|
||||
class Staff{
|
||||
constructor(role,x,y){
|
||||
this.id=NEXT_ID++;
|
||||
this.kind='staff'; this.role=role; // 'keeper'|'vet'
|
||||
this.x=x; this.y=y;
|
||||
this.speed=role==='keeper'?2.1:2.3;
|
||||
this.state='idle';
|
||||
this.job=null;
|
||||
this.wp=null;
|
||||
this.workT=0;
|
||||
this.phase=R()*10;
|
||||
this.shirt=role==='keeper'?'#5fb868':'#f5f5f5';
|
||||
this.pants='#43362a';
|
||||
this.skin=SKINS[(R()*SKINS.length)|0];
|
||||
this.hair=HAIRS[(R()*HAIRS.length)|0];
|
||||
this.face=1;
|
||||
this.idleT=1+R()*2;
|
||||
}
|
||||
update(sdt,game){
|
||||
if(this.workT>0){
|
||||
this.workT-=sdt;
|
||||
if(this.workT<=0){
|
||||
if(this.role==='keeper'&&this.job&&this.job.alive){
|
||||
this.job.startEating();
|
||||
game.puffHeart(this.job.x,this.job.y);
|
||||
this.job.makeHappy();
|
||||
}
|
||||
if(this.role==='vet'&&this.job&&this.job.alive){
|
||||
this.job.sick=false;
|
||||
this.job.makeHappy();
|
||||
game.puffHeart(this.job.x,this.job.y);
|
||||
game.notify(this.job.name+' the '+D.SPECIES[this.job.species].name+' was healed!','good');
|
||||
}
|
||||
this.job=null;
|
||||
this.state='idle';
|
||||
this.idleT=0.5;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!this.job){
|
||||
this.idleT-=sdt;
|
||||
// ask dispatcher
|
||||
const j=game.requestJob(this.role,this);
|
||||
if(j){
|
||||
this.job=j;
|
||||
const w=game.world;
|
||||
this.wp=w.findPathKeeper(Math.round(this.x),Math.round(this.y),Math.round(j.x),Math.round(j.y));
|
||||
if(!this.wp){ game.abandonJob(this.role,j); this.job=null; this.idleT=2; }
|
||||
else { this.state='walk'; }
|
||||
return;
|
||||
}
|
||||
if(this.idleT<=0){
|
||||
// casual wander
|
||||
const w=game.world;
|
||||
const b=w.buildings.find(bb=>bb.type===(this.role==='keeper'?'hut':'vet'))||w.buildings[0];
|
||||
let tx,ty;
|
||||
if(b){ const d=w.doorTile(b); tx=d[0]+((R()*7)|0)-3; ty=d[1]+((R()*7)|0)-3; }
|
||||
else { tx=(R()*w.cols)|0; ty=(R()*w.rows)|0; }
|
||||
tx=WK.clamp(tx,1,w.cols-2); ty=WK.clamp(ty,1,w.rows-2);
|
||||
if(w.path[w.idx(tx,ty)]||D.GROUND[w.gid(tx,ty)].walk){
|
||||
this.wp=w.findPathKeeper(Math.round(this.x),Math.round(this.y),tx,ty);
|
||||
if(this.wp&&this.wp.length) this.state='walk';
|
||||
}
|
||||
this.idleT=4+R()*5;
|
||||
}
|
||||
if(this.state==='idle') return;
|
||||
}
|
||||
if(this.state==='walk'){
|
||||
if(!this.wp||!this.wp.length){
|
||||
if(this.job){ this.state='work'; this.workT=this.role==='keeper'?2.4:3; this.face=this.job.x>=this.x?1:-1; }
|
||||
else { this.state='idle'; }
|
||||
return;
|
||||
}
|
||||
const [tx,ty]=this.wp[0];
|
||||
const dx=tx-this.x, dy=ty-this.y;
|
||||
const d=Math.hypot(dx,dy);
|
||||
const step=this.speed*sdt;
|
||||
if(d<=step){ this.x=tx; this.y=ty; this.wp.shift(); }
|
||||
else{ this.x+=dx/d*step; this.y+=dy/d*step; }
|
||||
const sdx=(tx-this.x)-(ty-this.y);
|
||||
if(Math.abs(sdx)>0.05) this.face=sdx>0?1:-1;
|
||||
this.phase+=step*5.5;
|
||||
}
|
||||
}
|
||||
draw(ctx,cam,time){
|
||||
const p=cam.project(this.x,this.y);
|
||||
if(!cam.isVisible(p.x,p.y,40)) return;
|
||||
ctx.save();
|
||||
ctx.translate(p.x,p.y);
|
||||
const act=this.workT>0?'feed':(this.state==='walk'?'walk':'stand');
|
||||
WK.Sprites.drawPerson(ctx,{
|
||||
phase:this.phase,state:act,shirt:this.shirt,pants:this.pants,
|
||||
skin:this.skin,hair:this.hair,kid:false,balloon:null,face:this.face,smile:true,
|
||||
});
|
||||
if(this.workT>0){
|
||||
// work bubble
|
||||
ctx.font='bold 12px sans-serif'; ctx.textAlign='center';
|
||||
ctx.fillText(this.role==='keeper'?'🍎':'🩺',0,-36+Math.sin(time*6));
|
||||
}
|
||||
// role cap
|
||||
ctx.fillStyle=this.role==='keeper'?'#3e8a33':'#e86a6a';
|
||||
ctx.beginPath(); ctx.ellipse(0,-29,4.6,2,0,0,WK.TAU); ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
WK.Staff = Staff;
|
||||
|
||||
/* ============================================================
|
||||
PARTICLES
|
||||
============================================================ */
|
||||
class Particles{
|
||||
constructor(){ this.list=[]; }
|
||||
add(o){ o.age=0; this.list.push(o); if(this.list.length>240) this.list.splice(0,this.list.length-240); }
|
||||
coins(x,y){ for(let i=0;i<3;i++) this.add({t:'coin',x:x+(R()-.5)*.4,y,vz:0,z:14+R()*8,vx:(R()-.5)*0.8,vy:(R()-.5)*0.4,dur:0.9}); }
|
||||
hearts(x,y){ for(let i=0;i<2;i++) this.add({t:'heart',x:x+(R()-.5)*.5,y:y-0.2,z:20,vx:(R()-.5)*0.3,vy:-0.5-R()*0.4,dur:1.2}); }
|
||||
puff(x,y,c){ this.add({t:'puff',x,y,z:4,vx:0,vy:0,dur:0.5,c:c||'rgba(180,160,130,'}); }
|
||||
splash(x,y){ this.add({t:'splash',x,y,z:2,vx:0,vy:0,dur:0.5}); }
|
||||
note(x,y){ this.add({t:'note',x,y,z:18,vx:0.2,vy:-0.6,dur:1}); }
|
||||
update(sdt){
|
||||
const L=this.list;
|
||||
for(let i=L.length-1;i>=0;i--){
|
||||
const p=L[i];
|
||||
p.age+=sdt;
|
||||
if(p.age>=p.dur){ L.splice(i,1); continue; }
|
||||
p.x+=(p.vx||0)*sdt; p.y+=(p.vy||0)*sdt;
|
||||
}
|
||||
}
|
||||
draw(ctx,cam,time){
|
||||
for(const p of this.list){
|
||||
const k=p.age/p.dur;
|
||||
const pr=cam.project(p.x,p.y);
|
||||
if(!cam.isVisible(pr.x,pr.y,30)) continue;
|
||||
const z=p.z*(1-k)+ (p.z||0)*0;
|
||||
ctx.save();
|
||||
ctx.translate(pr.x,pr.y-(p.z||0)*(0.4+k*1.4)*16);
|
||||
ctx.globalAlpha=1-k*k;
|
||||
if(p.t==='coin'){
|
||||
ctx.fillStyle='#ffc93c';
|
||||
ctx.beginPath(); ctx.ellipse(0,0,4.4*Math.abs(Math.cos(k*9)),4.4,0,0,WK.TAU); ctx.fill();
|
||||
ctx.strokeStyle='#d99a1e'; ctx.lineWidth=1.2; ctx.stroke();
|
||||
}else if(p.t==='heart'){
|
||||
ctx.fillStyle='#ff6b81';
|
||||
const s=3.2+k*2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0,s*0.9); ctx.bezierCurveTo(-s*1.6,-s*0.3,-s*0.9,-s*1.4,0,-s*0.4);
|
||||
ctx.bezierCurveTo(s*0.9,-s*1.4,s*1.6,-s*0.3,0,s*0.9);
|
||||
ctx.fill();
|
||||
}else if(p.t==='puff'){
|
||||
ctx.fillStyle=p.c+(0.5*(1-k))+')';
|
||||
ctx.beginPath(); ctx.arc(0,0,4+k*14,0,WK.TAU); ctx.fill();
|
||||
}else if(p.t==='splash'){
|
||||
ctx.strokeStyle='rgba(200,235,250,'+(0.8*(1-k))+')';
|
||||
ctx.lineWidth=1.6;
|
||||
ctx.beginPath(); ctx.arc(0,0,3+k*13,0,WK.TAU); ctx.stroke();
|
||||
}else if(p.t==='note'){
|
||||
ctx.fillStyle='rgba(90,60,120,'+(1-k)+')';
|
||||
ctx.font='bold 12px sans-serif'; ctx.textAlign='center';
|
||||
ctx.fillText(Math.sin(p.x*7)>0?'♪':'♫',0,0);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.globalAlpha=1;
|
||||
}
|
||||
}
|
||||
WK.Particles = Particles;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user