| 1 | // PetSim.kt — the Codewhale pet core, Kotlin/Android port. |
| 2 | // |
| 3 | // A faithful port of PetSim.ts. Same 980-point body from whale-points.tsv, |
| 4 | // same mulberry32(0xC0FFEE) jitter, same gait field and spring integration, |
| 5 | // same colour/hollow/brightness encoding. Pure Kotlin + java.lang.Math — |
| 6 | // no Android APIs, so it runs on the JVM for tests and in the app for rendering. |
| 7 | // |
| 8 | // Compile-verified offline with the cached Kotlin 2.3.0 JVM compiler. |
| 9 | // android/verify.sh compares all four tapes/modes against TypeScript. |
| 10 | |
| 11 | package codewhale.pet |
| 12 | |
| 13 | import kotlin.math.* |
| 14 | |
| 15 | data class PetState( |
| 16 | var activity: Double = 0.35, |
| 17 | var coherence: Double = 0.8, |
| 18 | var attention: Double = 0.0, |
| 19 | var channel: String = "reasoning", |
| 20 | var observed: Double = 1.0, |
| 21 | var roamX: Double = 0.0, |
| 22 | var roamY: Double = 0.0, |
| 23 | var flip: Double = 1.0, |
| 24 | var lit: Double = 1.0, |
| 25 | ) |
| 26 | |
| 27 | data class Channel(val key: String, val label: String, val r: Int, val g: Int, val b: Int, |
| 28 | val arch: String, val form: String) |
| 29 | |
| 30 | val CHANNELS = listOf( |
| 31 | Channel("reasoning", "Model / reasoning", 0x73, 0xc9, 0xb5, "gyre", "gyre · rolling"), |
| 32 | Channel("tool", "Tool calls", 0x74, 0xaa, 0xdd, "strike", "strike · reaching"), |
| 33 | Channel("memory", "Memory / RAG", 0xb6, 0xa7, 0x7f, "gyre", "gyre · scanning"), |
| 34 | Channel("code", "Code execution", 0x9b, 0x9e, 0xd7, "strike", "strike · along the body"), |
| 35 | Channel("filesystem", "Filesystem", 0x92, 0xb9, 0xc9, "strike", "strike · fanning"), |
| 36 | Channel("network", "Network / API", 0xd3, 0xac, 0x74, "cross", "crossing · one way"), |
| 37 | Channel("browser", "Browser / computer", 0x9e, 0xa9, 0xdf, "cross", "crossing · a sweep"), |
| 38 | Channel("communication", "Agent messages", 0x83, 0xc5, 0xc9, "cross", "crossing · two ways"), |
| 39 | Channel("agent", "Subagent activity", 0xb0, 0x9a, 0xcb, "pod", "pod · peers"), |
| 40 | Channel("orchestration", "Orchestration", 0x6c, 0x87, 0x98, "pod", "pod · hub"), |
| 41 | Channel("error", "Errors / exceptions", 0xe7, 0x91, 0x86, "tear", "torn · irregular"), |
| 42 | Channel("human", "Human interaction", 0xc2, 0xb7, 0x87, "address", "decision · junction"), |
| 43 | Channel("other", "Unclassified", 0x73, 0x84, 0x92, "drift", "drifting · unformed"), |
| 44 | ) |
| 45 | |
| 46 | val CHANNEL_INDEX = CHANNELS.mapIndexed { i, c -> c.key to i }.toMap() |
| 47 | |
| 48 | fun archOf(key: String) = when (key) { |
| 49 | "reasoning", "memory" -> "gyre" |
| 50 | "tool", "code", "filesystem" -> "strike" |
| 51 | "network", "communication", "browser" -> "cross" |
| 52 | "agent", "orchestration" -> "pod" |
| 53 | "error" -> "tear" |
| 54 | "human" -> "address" |
| 55 | else -> "drift" |
| 56 | } |
| 57 | |
| 58 | private val UNKNOWN_RGB = doubleArrayOf(115.0, 132.0, 146.0) |
| 59 | private val REST_RGB = doubleArrayOf(122.0, 214.0, 240.0) |
| 60 | // Keep the native checkpoint boundary aligned with the shared world clock. |
| 61 | private const val PET_MAX_SECONDS = 100.0 * 365.0 * 86_400.0 |
| 62 | |
| 63 | private fun lerp(a: Double, b: Double, t: Double) = a + (b - a) * t |
| 64 | private fun clamp(v: Double, lo: Double = 0.0, hi: Double = 1.0) = min(hi, max(lo, v)) |
| 65 | |
| 66 | /** mulberry32 — the same 32-bit sequence as every other port. */ |
| 67 | class Mulberry32(seed: Int) { |
| 68 | private var a = seed |
| 69 | fun next(): Double { |
| 70 | a += 0x6D2B79F5.toInt() |
| 71 | var t = a |
| 72 | t = (t xor t.ushr(15)) * (t or 1) |
| 73 | t = t xor (t + (t xor t.ushr(7)) * (t or 61)) |
| 74 | return (t xor t.ushr(14)).toUInt().toDouble() / 4294967296.0 |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | class Particle { |
| 79 | var x = 0.0; var y = 0.0; var vx = 0.0; var vy = 0.0 |
| 80 | var s = 0.0; var jx = 0.0; var jy = 0.0 |
| 81 | var pod = 0 |
| 82 | var hx = 0.0; var hy = 0.0; var ang = 0.0; var rad = 0.0; var tail = 0.0 |
| 83 | var tx = 0.0; var ty = 0.0 |
| 84 | } |
| 85 | |
| 86 | data class Frame( |
| 87 | var r: Double = 122.0, var g: Double = 214.0, var b: Double = 240.0, |
| 88 | var alpha: Double = 0.3, var hollow: Boolean = false, |
| 89 | var channel: String = "reasoning", var arch: String = "gyre", var work: Double = 0.0, |
| 90 | ) |
| 91 | |
| 92 | // Version 2; the same field math as TypeScript, Swift and Rust. |
| 93 | private fun fieldTarget(q: Particle, t: Double, act: Double, att: Double, key: String): Pair<Double, Double>? { |
| 94 | val u = q.s * 2 - 1; val lane = q.pod - 2.5; val a = q.s * PI * 2 |
| 95 | val flow = t * (0.35 + act * 0.65) |
| 96 | return when (key) { |
| 97 | "reasoning" -> { |
| 98 | val ring = 0.34 + 0.105 * cos(a * 3 + flow + lane * 0.18) |
| 99 | ring * cos(a * 2 + flow * 0.3) to ring * sin(a * 2 + flow * 0.3) * 0.7 + 0.10 * sin(a * 3 + flow) |
| 100 | } |
| 101 | "memory" -> 0.46 * cos(a + lane * 0.1 + flow * 0.25) to lane * 0.082 + 0.052 * sin(a * 2 + flow) |
| 102 | "code" -> u * 0.57 to lane * 0.066 + 0.12 * sin(u * 7 + flow * 2 + q.pod * PI / 3) |
| 103 | "filesystem" -> { |
| 104 | val branch = max(0.0, (u + 0.3) / 1.3) |
| 105 | u * 0.56 to lane * 0.13 * branch + 0.025 * sin(u * 8 - flow) |
| 106 | } |
| 107 | "tool" -> { |
| 108 | val reach = 0.14 + (u + 1) * 0.20 + 0.04 * sin(flow * 3 - u * 4) |
| 109 | cos(q.pod * PI / 3) * reach to sin(q.pod * PI / 3) * reach * 0.8 + q.hy * 0.06 |
| 110 | } |
| 111 | "browser" -> u * 0.56 to lane * 0.083 + 0.035 * sin(u * 5 - flow * 2) |
| 112 | "network", "communication" -> { |
| 113 | val direction = if (key == "communication" && q.pod % 2 == 1) -1.0 else 1.0 |
| 114 | val phase = a + flow * direction |
| 115 | 0.54 * cos(phase) to sin(phase) * (0.12 + q.pod * 0.035) + lane * 0.024 |
| 116 | } |
| 117 | "human" -> { |
| 118 | val gap = if (u < 0) -0.075 else 0.075 |
| 119 | u * 0.47 + gap to lane * 0.10 * abs(u) + 0.012 * sin(flow + a) * (1 - att) |
| 120 | } |
| 121 | else -> null |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | /** Version 1 is retained for saved recordings. */ |
| 126 | private fun gaitTarget(q: Particle, t: Double, act: Double, coh: Double, |
| 127 | att: Double, key: String, work: Double, expressionVersion: Int = 1, |
| 128 | podSlots: List<Pair<Int, Double>>? = null): Pair<Double, Double> { |
| 129 | val omega = lerp(4.6, 5.2 + act * 2.8, work) |
| 130 | val breath = 1 + sin(t * 1.85) * lerp(0.048, 0.018, work) |
| 131 | val flex = sin(q.ang * 2.05 + t * omega) * lerp(0.042, 0.016 + act * 0.028, work) * (0.18 + 0.82 * q.tail) |
| 132 | var px = cos(q.ang + flex) * q.rad * breath |
| 133 | var py = sin(q.ang + flex) * q.rad * breath |
| 134 | px += sin(t * 0.33) * lerp(0.030, 0.014, work) |
| 135 | py += cos(t * 0.21) * lerp(0.018, 0.010, work) |
| 136 | if (work < 0.02) return px to py |
| 137 | |
| 138 | var gx = px; var gy = py |
| 139 | when (archOf(key)) { |
| 140 | "gyre" -> if (key == "memory") { |
| 141 | val pulse = 1 + sin(t * (2.4 + act * 1.6) - q.rad * 11) * (0.15 + act * 0.10) |
| 142 | gx *= pulse; gy *= pulse |
| 143 | } else { |
| 144 | val roll = sin(t * (1.05 + act * 0.35)) * (0.48 + act * 0.32) |
| 145 | val c = cos(roll); val sn = sin(roll) |
| 146 | gx = px * c - py * sn * 0.88 |
| 147 | gy = px * sn * 0.88 + py * c |
| 148 | } |
| 149 | "strike" -> if (key == "tool") { |
| 150 | val rate = 2.7 + act * 2.1 |
| 151 | val lunge = max(0.0, sin(t * rate)).pow(2) |
| 152 | gx += lunge * 0.11 |
| 153 | if (q.s > 0.60) { |
| 154 | val reach = max(0.0, sin(t * rate + q.pod * 0.92)).pow(4) * (0.30 + act * 0.24) |
| 155 | gx += cos(q.ang) * reach |
| 156 | gy += sin(q.ang) * reach |
| 157 | } |
| 158 | } else if (key == "code") { |
| 159 | val rate = 3.2 + act * 1.8 |
| 160 | val wave = sin(t * rate - q.tail * 7.5) |
| 161 | val bump = 0.11 + act * 0.08 |
| 162 | gx += cos(q.ang) * wave * bump |
| 163 | gy += sin(q.ang) * wave * bump * 1.2 |
| 164 | gx += max(0.0, wave) * 0.07 |
| 165 | } else { |
| 166 | val rate = 2.15 + act * 1.5 |
| 167 | val side = (q.pod % 2) * 2 - 1 |
| 168 | val w = max(0.0, sin(t * rate + q.pod * 0.72)).pow(2) |
| 169 | gx += w * 0.055 |
| 170 | gy += side * w * (0.17 + act * 0.13) |
| 171 | } |
| 172 | "cross" -> if (key == "browser") { |
| 173 | val band = (t * (0.55 + act * 0.35)) % 1 * 1.28 - 0.64 |
| 174 | val inBand = max(0.0, 1 - abs(q.hy - band) / 0.08) |
| 175 | gx += inBand * (0.24 + act * 0.10) |
| 176 | gy += inBand * 0.02 |
| 177 | } else { |
| 178 | val two = key == "communication" |
| 179 | if (q.s < if (two) 0.44 else 0.32) { |
| 180 | val dir = if (two) (if (q.s < 0.22) 1.0 else -1.0) else 1.0 |
| 181 | val u = (t * (0.38 + act * 0.36) + q.s * 5.2) % 1 |
| 182 | val going = if (u < 0.5) u * 2 else 2 - u * 2 |
| 183 | val e = going * going * (3 - 2 * going) |
| 184 | gx = lerp(q.hx, dir * 0.80, e) |
| 185 | gy = q.hy * (1 - e * 0.38) + sin(going * PI) * 0.11 * dir |
| 186 | } |
| 187 | } |
| 188 | "pod" -> { |
| 189 | val n = 6 |
| 190 | val member = podSlots?.takeIf { it.isNotEmpty() }?.let { it[q.pod % it.size] } |
| 191 | val k = member?.first ?: (q.pod % n) |
| 192 | val hub = key == "orchestration" && k == 0 |
| 193 | val spread = 0.30 + act * 0.11 |
| 194 | val orbit = t * (0.55 + act * 0.28) |
| 195 | if (hub) { gx = px * 0.70; gy = py * 0.70 } |
| 196 | else { |
| 197 | val slots = if (key == "orchestration") n - 1 else n |
| 198 | val a = (if (key == "orchestration") k - 1 else k) * (PI * 2 / slots) + orbit + (member?.second ?: 0.0) * 0.04 |
| 199 | val sc = 0.34 |
| 200 | gx = q.hx * sc + cos(a) * spread * 1.28 |
| 201 | gy = q.hy * sc + sin(a) * spread * 0.80 |
| 202 | } |
| 203 | } |
| 204 | "tear" -> { |
| 205 | val side = if (q.hx + q.hy < 0) -1.0 else 1.0 |
| 206 | gx += side * (0.24 + (1 - coh) * 0.16) |
| 207 | gy += side * 0.15 |
| 208 | gx += sin(t * 11.4 + q.s * 40) * (0.045 + act * 0.05) |
| 209 | gy += cos(t * 9.2 + q.s * 31) * (0.040 + act * 0.045) |
| 210 | } |
| 211 | "address" -> { |
| 212 | val face = 0.90 + att * 0.08 |
| 213 | val th = 0.70 |
| 214 | val z = (q.s - 0.5) * 0.42 |
| 215 | var ax = q.hx * cos(th) + z * sin(th) |
| 216 | var ay = q.hy |
| 217 | val disc = 0.48 * face |
| 218 | ax = lerp(ax, cos(q.ang) * min(0.36, q.rad + 0.06) * 0.95, disc) |
| 219 | ay = lerp(ay, sin(q.ang) * min(0.36, q.rad + 0.06) * 1.08, disc) |
| 220 | val grow = 1.20 + sin(t * 1.65) * 0.055 |
| 221 | gx = ax * grow; gy = ay * grow |
| 222 | } |
| 223 | else -> { |
| 224 | val mill = 0.13 + (1 - coh) * 0.10 |
| 225 | gx = q.hx * 0.52 + sin(t * 0.72 + q.jx) * mill |
| 226 | gy = q.hy * 0.52 + cos(t * 0.54 + q.jy) * mill |
| 227 | } |
| 228 | } |
| 229 | if (expressionVersion == 2) fieldTarget(q, t, act, att, key)?.let { gx = it.first; gy = it.second } |
| 230 | return lerp(px, gx, work) to lerp(py, gy, work) |
| 231 | } |
| 232 | |
| 233 | private fun stillT(key: String) = when (key) { |
| 234 | "reasoning" -> 1.15; "memory" -> 0.42; "tool" -> 0.30; "code" -> 0.18 |
| 235 | "filesystem" -> 0.48; "network" -> 0.72; "browser" -> 0.95 |
| 236 | "communication" -> 0.58; "agent" -> 1.25; "orchestration" -> 0.85 |
| 237 | "error" -> 0.35; "human" -> 0.05; "other" -> 0.90; else -> 0.4 |
| 238 | } |
| 239 | |
| 240 | data class PetParticleCheckpoint( |
| 241 | val version: Int, val expressionVersion: Int, val body: List<List<Double>>, |
| 242 | val particles: List<List<Double>>, val phase: Double, val clock: Double, |
| 243 | val tear: Double, val previous: Int, val current: Int, val color: List<Double>, val frame: Frame, |
| 244 | ) |
| 245 | |
| 246 | class PetSim(points: List<Pair<Double, Double>>, seed: Int = 0xC0FFEE.toInt(), expressionVersion: Int = 2) { |
| 247 | var expressionVersion = expressionVersion; private set |
| 248 | val p: List<Particle> |
| 249 | private var phase = 0.0 |
| 250 | private var clock = 0.0 |
| 251 | private var tear = 0.0 |
| 252 | private var prev: Int |
| 253 | private val col = REST_RGB.copyOf() |
| 254 | private var cur: Int |
| 255 | var frame = Frame(); private set |
| 256 | |
| 257 | init { |
| 258 | require(expressionVersion == 1 || expressionVersion == 2) |
| 259 | val rng = Mulberry32(seed) |
| 260 | p = points.mapIndexed { i, (hx, hy) -> |
| 261 | Particle().apply { |
| 262 | this.hx = hx; this.hy = hy |
| 263 | x = hx; y = hy; tx = hx; ty = hy |
| 264 | s = rng.next(); jx = rng.next() * 6.283; jy = rng.next() * 6.283 |
| 265 | pod = i % 6 |
| 266 | ang = atan2(hy, hx) |
| 267 | rad = hypot(hx, hy) |
| 268 | tail = clamp(((-hx - hy) * 0.5 + 0.22) / 0.62) |
| 269 | } |
| 270 | } |
| 271 | cur = CHANNEL_INDEX["reasoning"]!! |
| 272 | prev = cur |
| 273 | } |
| 274 | |
| 275 | /** The shared world validates the complete recording first. This projection |
| 276 | * also checks body identity and bounds before changing any native particle. */ |
| 277 | fun restoreValidated(c: PetParticleCheckpoint) { |
| 278 | fun finite(v: Double, lo: Double, hi: Double) = v.isFinite() && v in lo..hi |
| 279 | require(c.version == 1 && c.expressionVersion in 1..2 && c.body.size == p.size && c.particles.size == p.size) |
| 280 | require(c.body.withIndex().all { (i, b) -> b == listOf(p[i].hx, p[i].hy, p[i].s) }) |
| 281 | require(c.particles.all { row -> row.size == 8 && row.withIndex().all { (i, v) -> |
| 282 | finite(v, if (i == 4 || i == 5) 0.0 else -8.0, if (i == 4 || i == 5) 2 * PET_MAX_SECONDS else 8.0) |
| 283 | } }) |
| 284 | require(finite(c.phase, 0.0, PET_MAX_SECONDS) && finite(c.clock, 0.0, PET_MAX_SECONDS) && finite(c.tear, 0.0, 1.0)) |
| 285 | require(c.previous in CHANNELS.indices && c.current in CHANNELS.indices && c.color.size == 3 && c.color.all { finite(it, 0.0, 255.0) }) |
| 286 | require(listOf(c.frame.r, c.frame.g, c.frame.b).all { finite(it, 0.0, 255.0) } && finite(c.frame.alpha, 0.0, 1.0) && finite(c.frame.work, 0.0, 1.0)) |
| 287 | require(c.frame.channel == CHANNELS[c.current].key && c.frame.arch == CHANNELS[c.current].arch) |
| 288 | expressionVersion = c.expressionVersion |
| 289 | phase = c.phase; clock = c.clock; tear = c.tear; prev = c.previous; cur = c.current |
| 290 | c.color.forEachIndexed { i, v -> col[i] = v }; frame = c.frame.copy() |
| 291 | p.forEachIndexed { i, q -> |
| 292 | val v = c.particles[i] |
| 293 | q.x = v[0]; q.y = v[1]; q.vx = v[2]; q.vy = v[3] |
| 294 | q.jx = v[4]; q.jy = v[5]; q.tx = v[6]; q.ty = v[7] |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | /** Advance the sim by dt seconds under `state`. Identical math to PetSim.ts. */ |
| 299 | fun step(dt: Double, state: PetState, motion: Boolean = true, sensitivity: Double = 1.0, |
| 300 | podSlots: List<Pair<Int, Double>>? = null) { |
| 301 | fun s(v: Double) = lerp(0.5, v, sensitivity) |
| 302 | val act = s(state.activity); val coh = s(state.coherence); val att = s(state.attention) |
| 303 | val seen = s(state.observed) |
| 304 | phase += dt * (0.18 + act * 0.55) * (if (motion) 1.0 else 0.0) |
| 305 | if (motion) clock += dt |
| 306 | |
| 307 | CHANNEL_INDEX[state.channel]?.let { cur = it } |
| 308 | val shown = cur |
| 309 | val ch = CHANNELS[shown] |
| 310 | |
| 311 | val work = clamp((act - 0.16) / 0.18) |
| 312 | val wander = lerp(0.32, 1.0, (1 - coh).pow(1.15)) |
| 313 | |
| 314 | if (shown != prev) { |
| 315 | if (shown == CHANNEL_INDEX["error"]) tear = 1.0 |
| 316 | prev = shown |
| 317 | } |
| 318 | tear = if (motion) max(0.0, tear - dt * 1.6) else 0.0 |
| 319 | |
| 320 | val split = (1 - coh).pow(1.6) * 0.16 + tear * 0.10 |
| 321 | val blur = (1 - coh).pow(1.45) * 0.22 + tear * 0.18 |
| 322 | val pull = if (motion) 2.2 + coh * 5.2 else 18.0 |
| 323 | val tGait = if (motion) clock else stillT(ch.key) |
| 324 | |
| 325 | for (q in p) { |
| 326 | if (motion) { |
| 327 | q.jx += dt * (0.40 + act * 1.1) |
| 328 | q.jy += dt * (0.34 + act * 0.9) |
| 329 | } |
| 330 | val (gx, gy) = gaitTarget(q, tGait, act, coh, att, ch.key, work, expressionVersion, podSlots) |
| 331 | val podAng = q.pod * 1.047 + phase * 0.22 |
| 332 | val tx = gx + sin(q.jx + q.s * 9) * blur * wander + cos(podAng) * split |
| 333 | val ty = gy + cos(q.jy + q.s * 7) * blur * wander + sin(podAng) * split * 0.55 |
| 334 | q.tx = tx; q.ty = ty |
| 335 | if (!motion) { q.x = tx; q.y = ty; q.vx = 0.0; q.vy = 0.0; continue } |
| 336 | q.vx += (tx - q.x) * pull * dt |
| 337 | q.vy += (ty - q.y) * pull * dt |
| 338 | q.vx *= 0.90; q.vy *= 0.90 |
| 339 | val speed = if (motion) 2.6 else 8.0 |
| 340 | q.x += q.vx * dt * speed |
| 341 | q.y += q.vy * dt * speed |
| 342 | } |
| 343 | |
| 344 | val want = if (work > 0.35) |
| 345 | doubleArrayOf(CHANNELS[shown].r.toDouble(), CHANNELS[shown].g.toDouble(), CHANNELS[shown].b.toDouble()) |
| 346 | else REST_RGB |
| 347 | val k = if (motion) min(1.0, dt * 2.6) else 1.0 |
| 348 | for (c in 0..2) col[c] += (lerp(UNKNOWN_RGB[c], want[c], seen) - col[c]) * k |
| 349 | val lit = clamp(state.lit) |
| 350 | val alpha = (0.22 + act * 0.10) * lerp(0.50, 1.0, coh) * lerp(0.55, 1.0, seen) * lerp(0.35, 1.0, lit) |
| 351 | frame = Frame(col[0], col[1], col[2], |
| 352 | alpha = min(0.92, alpha * 1.85), |
| 353 | hollow = seen < 0.92, |
| 354 | channel = ch.key, arch = ch.arch, work = work) |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | fun petDigest(sim: PetSim): String { |
| 359 | val grid = IntArray(64 * 32) |
| 360 | for (p in sim.p) { |
| 361 | val x = floor((p.x + 0.66) / 1.32 * 64).toInt() |
| 362 | val y = floor((p.y + 0.66) / 1.32 * 32).toInt() |
| 363 | if (x in 0..63 && y in 0..31) grid[y * 64 + x] = min(255, grid[y * 64 + x] + 1) |
| 364 | } |
| 365 | var hash = 0xcbf29ce484222325UL.toLong() |
| 366 | fun mix(n: Int) { hash = (hash xor (n and 255).toLong()) * 0x100000001b3L } |
| 367 | for (n in grid) mix(n) |
| 368 | mix(sim.frame.r.roundToInt()); mix(sim.frame.g.roundToInt()); mix(sim.frame.b.roundToInt()) |
| 369 | mix((sim.frame.alpha * 255).roundToInt()); mix(if (sim.frame.hollow) 1 else 0) |
| 370 | return hash.toULong().toString(16).padStart(16, '0') |
| 371 | } |
| 372 | |
| 373 | /** Body-space → renderer-space, same as PetSim.ts layout(). */ |
| 374 | data class PetLayout(val scale: Double, val flipX: Double, val ox: Double, val oy: Double, val dot: Double) |
| 375 | fun petLayout(w: Double, h: Double, state: PetState): PetLayout { |
| 376 | val att = state.attention |
| 377 | return PetLayout( |
| 378 | scale = min(w * 0.52, h * 0.92) * (1 + att * 0.07), |
| 379 | flipX = state.flip, |
| 380 | ox = w / 2 + state.roamX * w * 0.30, |
| 381 | oy = h / 2 + state.roamY * h * 0.30 + h * att * 0.05, |
| 382 | dot = max(1.6, min(w, h) * 0.0092) * (1 + att * 0.18)) |
| 383 | } |
| 384 |