306 lines
11 KiB
JavaScript
306 lines
11 KiB
JavaScript
"use strict";
|
|
function makeName(worldRef = null) {
|
|
const existing = new Set((worldRef?.tarinai || []).filter(t => !t.dead).map(t => t.name));
|
|
const build = () => {
|
|
const len = randi(2, 7);
|
|
const hasStop = len >= 2 && Math.random() < 0.30;
|
|
const middleLen = Math.max(0, len - 1 - (hasStop ? 1 : 0));
|
|
let name = "\u306f";
|
|
for (let i = 0; i < middleLen; i++) name += Math.random() < 0.64 ? "\u3046" : "\u3045";
|
|
if (hasStop) name += "\u3063";
|
|
return name;
|
|
};
|
|
for (let i = 0; i < 80; i++) {
|
|
const candidate = build();
|
|
if (!existing.has(candidate)) return candidate;
|
|
}
|
|
return build();
|
|
}
|
|
|
|
function spawnableSprites() {
|
|
return SPRITES.filter(s => !s.actionOnly && !s.displayOnly);
|
|
}
|
|
|
|
function displayNormalSprites() {
|
|
return ["smile", "jito", "normal_smirk"].filter(id => SPRITES.some(s => s.id === id));
|
|
}
|
|
|
|
function lowStressSprites() {
|
|
return ["normal_tongue", "normal_happy"].filter(id => SPRITES.some(s => s.id === id));
|
|
}
|
|
|
|
function stableChoice(seed, salt, options) {
|
|
if (!options?.length) return null;
|
|
const i = Math.min(options.length - 1, Math.floor(stableUnit(seed, salt) * options.length));
|
|
return options[i];
|
|
}
|
|
|
|
function baseSpriteId() {
|
|
return pick(spawnableSprites()).id;
|
|
}
|
|
|
|
function makeTrait(type) {
|
|
const base = {
|
|
smile: { hunger: 0.92, social: 1.14, sleep: 1.00, stress: 0.82, speed: 1.02 },
|
|
angry: { hunger: 1.05, social: 0.84, sleep: 0.94, stress: 1.24, speed: 1.15 },
|
|
teary: { hunger: 0.96, social: 1.22, sleep: 1.04, stress: 1.12, speed: 0.96 },
|
|
jito: { hunger: 0.88, social: 0.80, sleep: 0.96, stress: 0.80, speed: 0.98 },
|
|
drool: { hunger: 1.30, social: 1.02, sleep: 1.04, stress: 0.98, speed: 1.00 },
|
|
cry: { hunger: 1.00, social: 1.30, sleep: 1.12, stress: 1.36, speed: 0.90 },
|
|
sleep: { hunger: 0.78, social: 0.88, sleep: 1.44, stress: 0.72, speed: 0.70 },
|
|
pokan: { hunger: 1.02, social: 1.02, sleep: 1.02, stress: 1.06, speed: 0.88 },
|
|
stretch: { hunger: 1.08, social: 0.90, sleep: 0.92, stress: 1.08, speed: 1.26 },
|
|
}[type] || { hunger: 1, social: 1, sleep: 1, stress: 1, speed: 1 };
|
|
return { ...base };
|
|
}
|
|
|
|
function stableUnit(seed, salt = "") {
|
|
const str = `${seed}:${salt}`;
|
|
let h = 2166136261;
|
|
for (let i = 0; i < str.length; i++) {
|
|
h ^= str.charCodeAt(i);
|
|
h = Math.imul(h, 16777619);
|
|
}
|
|
return ((h >>> 0) % 100000) / 100000;
|
|
}
|
|
|
|
const PERSONALITIES = {
|
|
timid: { label: "\u81c6\u75c5", desc: "\u9003\u3052\u3084\u3059\u304f\u3001\u55a7\u5629\u3092\u907f\u3051\u308b", fear: 1.42, fight: 0.48, social: 0.96, relation: 1.04, sleep: 1.00, zunda: 1.00 },
|
|
glutton: { label: "\u98df\u3044\u3057\u3093\u574a", desc: "\u305a\u3093\u3060\u9905\u3092\u512a\u5148\u3057\u3084\u3059\u3044", fear: 0.92, fight: 0.82, social: 1.00, relation: 1.00, sleep: 0.95, zunda: 1.42 },
|
|
sleepy: { label: "\u7720\u305f\u304c\u308a", desc: "\u65e9\u3081\u306b\u5bdd\u5e8a\u3078\u5411\u304b\u3046", fear: 0.96, fight: 0.72, social: 0.92, relation: 0.96, sleep: 1.34, zunda: 0.92 },
|
|
lonely: { label: "\u5bc2\u3057\u304c\u308a", desc: "\u4ed6\u306e\u305f\u308a\u306a\u3044\u3078\u5bc4\u308a\u3084\u3059\u3044", fear: 1.02, fight: 0.70, social: 1.34, relation: 1.38, sleep: 0.96, zunda: 1.00 },
|
|
irritable: { label: "\u6012\u308a\u3063\u307d\u3044", desc: "\u55a7\u5629\u304c\u8d77\u304d\u3084\u3059\u3044", fear: 0.82, fight: 1.82, social: 0.78, relation: 0.78, sleep: 0.90, zunda: 0.94 },
|
|
calm: { label: "\u30de\u30a4\u30da\u30fc\u30b9", desc: "\u53cd\u5fdc\u304c\u7a4f\u3084\u304b\u3067\u5b89\u5b9a\u3057\u3084\u3059\u3044", fear: 0.82, fight: 0.58, social: 1.02, relation: 1.08, sleep: 1.02, zunda: 0.94 },
|
|
};
|
|
const PERSONALITY_IDS = Object.keys(PERSONALITIES);
|
|
|
|
function personalityForSeed(seed) {
|
|
return stableChoice(seed, "personality", PERSONALITY_IDS) || "calm";
|
|
}
|
|
|
|
function relationDefaults() {
|
|
return { affinity: 0, fear: 0, fightsWon: 0, fightsLost: 0, lastEvent: "", lastTime: 0 };
|
|
}
|
|
|
|
function isParentChild(a, b) {
|
|
if (!a || !b) return false;
|
|
return Boolean(a.parents?.includes(b.id) || b.parents?.includes(a.id));
|
|
}
|
|
|
|
function relationDisplayName(worldRef, id) {
|
|
if (!id) return "\u306a\u3057";
|
|
const live = worldRef?.tarinai?.find(t => t.id === id);
|
|
if (live) return live.name;
|
|
const fam = worldRef?.family?.[id];
|
|
return fam?.name || "\u4e0d\u660e";
|
|
}
|
|
|
|
class Effect {
|
|
constructor(type, x, y, options = {}) {
|
|
this.type = type;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.vx = options.vx ?? rand(-10, 10);
|
|
this.vy = options.vy ?? rand(-10, 10);
|
|
this.size = options.size ?? rand(3, 8);
|
|
this.life = options.life ?? 0.7;
|
|
this.maxLife = this.life;
|
|
this.color = options.color || "#fff4a8";
|
|
this.seed = Math.random() * 1000;
|
|
this.text = options.text || "";
|
|
}
|
|
|
|
update(dt) {
|
|
this.life -= dt;
|
|
this.x += this.vx * dt;
|
|
this.y += this.vy * dt;
|
|
this.vx *= Math.pow(0.92, dt * 60);
|
|
this.vy *= Math.pow(0.92, dt * 60);
|
|
}
|
|
get dead() { return this.life <= 0; }
|
|
|
|
draw(ctx) {
|
|
const alpha = clamp(this.life / this.maxLife, 0, 1);
|
|
ctx.save();
|
|
ctx.globalAlpha = alpha;
|
|
ctx.translate(this.x, this.y);
|
|
if (this.type === "eat") {
|
|
ctx.fillStyle = this.color;
|
|
ctx.strokeStyle = "rgba(143, 111, 43, 0.7)";
|
|
ctx.lineWidth = 1.2;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
} else if (this.type === "bleed") {
|
|
ctx.fillStyle = this.color;
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 0, this.size * 1.25, this.size * 0.72, this.seed, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
} else if (this.type === "ring") {
|
|
ctx.strokeStyle = this.color;
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, this.size + (1 - alpha) * 18, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
} else if (this.type === "fight") {
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
const puffColor = this.color || "rgba(105, 62, 34, 0.86)";
|
|
ctx.fillStyle = puffColor;
|
|
ctx.strokeStyle = puffColor;
|
|
for (let i = 0; i < 4; i++) {
|
|
const a = this.seed + i * 1.55;
|
|
const px = Math.cos(a) * this.size * randSeed(this.seed + i, 0.10, 0.72);
|
|
const py = Math.sin(a) * this.size * randSeed(this.seed + i + 8, 0.08, 0.48);
|
|
ctx.globalAlpha = alpha * (0.26 + i * 0.08);
|
|
ctx.beginPath();
|
|
ctx.ellipse(px, py, this.size * randSeed(this.seed + i + 20, 0.18, 0.34), this.size * randSeed(this.seed + i + 40, 0.10, 0.22), a, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.globalAlpha = alpha * 0.78;
|
|
ctx.lineWidth = 2.0;
|
|
for (let i = 0; i < 2; i++) {
|
|
const dir = i === 0 ? -1 : 1;
|
|
const y = dir * this.size * 0.14;
|
|
ctx.beginPath();
|
|
ctx.moveTo(-this.size * 0.62, y);
|
|
ctx.quadraticCurveTo(-this.size * 0.12, y - dir * this.size * 0.42, this.size * 0.54, y + dir * this.size * 0.06);
|
|
ctx.stroke();
|
|
}
|
|
} else if (this.type === "fall") {
|
|
ctx.fillStyle = this.color || "rgba(154, 124, 80, 0.62)";
|
|
for (let i = 0; i < 5; i++) {
|
|
const a = this.seed + i * 1.37;
|
|
const rr = this.size * randSeed(this.seed + i, 0.18, 0.82);
|
|
const px = Math.cos(a) * rr;
|
|
const py = Math.sin(a) * rr * 0.42;
|
|
ctx.globalAlpha = alpha * randSeed(this.seed + i + 9, 0.18, 0.42);
|
|
ctx.beginPath();
|
|
ctx.ellipse(px, py, this.size * randSeed(this.seed + i + 20, 0.16, 0.34), this.size * randSeed(this.seed + i + 30, 0.08, 0.18), a, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
} else if (this.type === "explosion") {
|
|
const burst = 1 - alpha;
|
|
ctx.globalAlpha = alpha * 0.95;
|
|
for (let i = 0; i < 3; i++) {
|
|
ctx.strokeStyle = i === 0 ? "rgba(255,238,145,0.92)" : (i === 1 ? "rgba(255,128,66,0.68)" : "rgba(96,62,42,0.42)");
|
|
ctx.lineWidth = Math.max(2, this.size * (0.09 - i * 0.018));
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, this.size * (0.28 + burst * (0.78 + i * 0.22)), 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
ctx.fillStyle = "rgba(255,214,86,0.88)";
|
|
for (let i = 0; i < 14; i++) {
|
|
const a = this.seed + i * 0.73;
|
|
const r = this.size * burst * randSeed(this.seed + i, 0.28, 1.00);
|
|
ctx.globalAlpha = alpha * randSeed(this.seed + i + 4, 0.36, 0.88);
|
|
ctx.beginPath();
|
|
ctx.arc(Math.cos(a) * r, Math.sin(a) * r, randSeed(this.seed + i + 12, 2.2, 5.2), 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
} else if (this.type === "bubble") {
|
|
const w = Math.max(30, 14 + this.text.length * 7);
|
|
const h = 20;
|
|
ctx.globalAlpha = Math.min(1, alpha * 1.25);
|
|
ctx.fillStyle = "rgba(255, 252, 240, 0.92)";
|
|
ctx.strokeStyle = "rgba(78, 64, 48, 0.22)";
|
|
ctx.lineWidth = 1.2;
|
|
roundedRect(ctx, -w / 2, -h, w, h, 9);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = "rgba(255, 252, 240, 0.92)";
|
|
ctx.beginPath();
|
|
ctx.moveTo(-4, -1);
|
|
ctx.lineTo(2, 5);
|
|
ctx.lineTo(6, -2);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = this.color || "rgba(42,36,29,0.78)";
|
|
ctx.font = "bold 12px Yomogi, ui-rounded, sans-serif";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(this.text, 0, -10);
|
|
}
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
class SpatialGrid {
|
|
constructor(cellSize = 96) {
|
|
this.cellSize = cellSize;
|
|
this.itemCells = new Map();
|
|
this.tarinaiCells = new Map();
|
|
}
|
|
|
|
clear() {
|
|
this.itemCells.clear();
|
|
this.tarinaiCells.clear();
|
|
}
|
|
|
|
keyFor(x, y) {
|
|
const cx = Math.floor(x / this.cellSize);
|
|
const cy = Math.floor(y / this.cellSize);
|
|
return cx + cy * 100000;
|
|
}
|
|
|
|
add(map, entity) {
|
|
const key = this.keyFor(entity.x, entity.y);
|
|
let bucket = map.get(key);
|
|
if (!bucket) {
|
|
bucket = [];
|
|
map.set(key, bucket);
|
|
}
|
|
bucket.push(entity);
|
|
}
|
|
|
|
rebuild(items, tarinai) {
|
|
this.clear();
|
|
for (const it of items) {
|
|
if (!it.dead) this.add(this.itemCells, it);
|
|
}
|
|
for (const t of tarinai) {
|
|
if (!t.dead) this.add(this.tarinaiCells, t);
|
|
}
|
|
}
|
|
|
|
nearby(map, x, y, radius, out = []) {
|
|
out.length = 0;
|
|
const r = Number.isFinite(radius) ? radius : Math.max(1200, this.cellSize * 12);
|
|
const minX = Math.floor((x - r) / this.cellSize);
|
|
const maxX = Math.floor((x + r) / this.cellSize);
|
|
const minY = Math.floor((y - r) / this.cellSize);
|
|
const maxY = Math.floor((y + r) / this.cellSize);
|
|
for (let cy = minY; cy <= maxY; cy++) {
|
|
for (let cx = minX; cx <= maxX; cx++) {
|
|
const bucket = map.get(cx + cy * 100000);
|
|
if (!bucket) continue;
|
|
for (const entity of bucket) out.push(entity);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
}
|
|
|
|
function drawHeartShape(ctx, x, y, size, opts = {}) {
|
|
const fill = opts.fill || "rgba(240,91,135,0.90)";
|
|
const stroke = opts.stroke || "rgba(255,252,246,0.92)";
|
|
const alpha = opts.alpha == null ? 1 : opts.alpha;
|
|
const rotation = opts.rotation || 0;
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
if (rotation) ctx.rotate(rotation);
|
|
ctx.globalAlpha *= alpha;
|
|
ctx.fillStyle = fill;
|
|
ctx.strokeStyle = stroke;
|
|
ctx.lineWidth = Math.max(1.2, size * 0.14);
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, size * 0.72);
|
|
ctx.bezierCurveTo(-size * 1.55, -size * 0.22, -size * 1.16, -size * 1.36, 0, -size * 0.52);
|
|
ctx.bezierCurveTo(size * 1.16, -size * 1.36, size * 1.55, -size * 0.22, 0, size * 0.72);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
|