f661eabba1
The original 30% per-render rate felt disturbing in practice. Default mood is 80, so most admin/dashboard renders cleared the >=75 gate, and ~30% of those flipped to wink. Stacked across the main admin page + dashboard widget visible on the same screen, the visible wink rate felt closer to "stuck" than "playful" — buddy looked like he had one eye closed all the time instead of occasionally cheeking out. 5% is true Easter-egg territory: rare enough to feel magical when it lands, frequent enough you'll catch it after a few admin sessions. Refresh ~20 times before expecting to see one. CHANGES - inc/state.php: mt_rand probability gate 30 -> 5 - inc/state.php: docstring updated to match new ~5% probability NOT CHANGED - Wink visuals (sprite.php) unchanged — still the same closed-eye arc, asymmetric smirk, rosier cheeks. Just rarer. - Version constant + CHANGELOG entry left at v0.1.1 — this is a tuning hotfix, not new behavior worth a version bump. If we want a v0.1.2 bump for hygiene, that's a separate commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
124 lines
4.6 KiB
PHP
124 lines
4.6 KiB
PHP
<?php
|
||
/**
|
||
* Buddy state — persistence for the pet's stats and identity.
|
||
*
|
||
* Storage: per-user, in `wp_usermeta` under key `buddy_state`. A single
|
||
* JSON-shaped associative array. Picked user_meta over site-wide
|
||
* options so multiple admins on the same site each get their own pet,
|
||
* matching the per-user mental model.
|
||
*
|
||
* Stats range 0–100. Higher = better. Decay happens via WP-cron later
|
||
* (Phase C); for Phase A the stats just persist as last set.
|
||
*/
|
||
|
||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||
|
||
const BUDDY_META_KEY = 'buddy_state';
|
||
|
||
/**
|
||
* Default state for a freshly-adopted pet. Used the first time a user
|
||
* visits a Buddy-rendering page.
|
||
*/
|
||
function buddy_default_state() {
|
||
return array(
|
||
'name' => __( 'Buddy', 'buddy' ),
|
||
'species' => 'default',
|
||
'hunger' => 80,
|
||
'happiness' => 80,
|
||
'health' => 90,
|
||
'energy' => 70,
|
||
'born_at' => time(),
|
||
'last_tick' => time(),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Fetch the current user's Buddy state, falling back to defaults and
|
||
* persisting them on first read so the pet has a "birthday" timestamp.
|
||
*
|
||
* @param int $user_id Optional. Defaults to current user.
|
||
* @return array { name, species, hunger, happiness, health, energy, born_at, last_tick }
|
||
*/
|
||
function buddy_get_state( $user_id = 0 ) {
|
||
$user_id = $user_id ? (int) $user_id : get_current_user_id();
|
||
if ( ! $user_id ) { return buddy_default_state(); }
|
||
|
||
$state = get_user_meta( $user_id, BUDDY_META_KEY, true );
|
||
if ( ! is_array( $state ) || empty( $state ) ) {
|
||
$state = buddy_default_state();
|
||
update_user_meta( $user_id, BUDDY_META_KEY, $state );
|
||
}
|
||
|
||
// Merge against defaults so newly-added keys appear with sensible
|
||
// values for users adopted under earlier versions.
|
||
return array_merge( buddy_default_state(), $state );
|
||
}
|
||
|
||
/**
|
||
* Persist a partial state update for the current user. Unknown keys
|
||
* are silently dropped; values are clamped to valid ranges.
|
||
*
|
||
* @param array $patch Keys: name (str), species (str), hunger/happiness/health/energy (int 0–100).
|
||
* @return array The full updated state.
|
||
*/
|
||
function buddy_update_state( array $patch, $user_id = 0 ) {
|
||
$user_id = $user_id ? (int) $user_id : get_current_user_id();
|
||
if ( ! $user_id ) { return buddy_default_state(); }
|
||
|
||
$state = buddy_get_state( $user_id );
|
||
|
||
$stat_keys = array( 'hunger', 'happiness', 'health', 'energy' );
|
||
foreach ( $stat_keys as $k ) {
|
||
if ( array_key_exists( $k, $patch ) ) {
|
||
$state[ $k ] = max( 0, min( 100, (int) $patch[ $k ] ) );
|
||
}
|
||
}
|
||
if ( array_key_exists( 'name', $patch ) ) {
|
||
$name = sanitize_text_field( (string) $patch['name'] );
|
||
if ( $name !== '' && mb_strlen( $name ) <= 32 ) {
|
||
$state['name'] = $name;
|
||
}
|
||
}
|
||
if ( array_key_exists( 'species', $patch ) ) {
|
||
$allowed_species = array( 'default' ); // Phase D will widen this.
|
||
$sp = sanitize_key( (string) $patch['species'] );
|
||
if ( in_array( $sp, $allowed_species, true ) ) {
|
||
$state['species'] = $sp;
|
||
}
|
||
}
|
||
|
||
update_user_meta( $user_id, BUDDY_META_KEY, $state );
|
||
return $state;
|
||
}
|
||
|
||
/**
|
||
* Average of the four stats — used as a single "mood" indicator for
|
||
* the dashboard widget. Returns 0–100.
|
||
*/
|
||
function buddy_overall_mood( array $state ) {
|
||
$sum = (int) $state['hunger'] + (int) $state['happiness'] + (int) $state['health'] + (int) $state['energy'];
|
||
return (int) round( $sum / 4 );
|
||
}
|
||
|
||
/**
|
||
* Pick a one-word status label + sprite tone based on overall mood.
|
||
* Pure cosmetic, used in the dashboard widget header and the main
|
||
* admin page.
|
||
*
|
||
* Easter-egg: when Buddy is genuinely happy (mood >= 75) there's a
|
||
* ~5% chance per page-render of returning the "Cheeky" wink tone
|
||
* instead of the standard happy face. Rare enough to feel magical
|
||
* rather than disturbing — refresh the page a few dozen times and
|
||
* you'll catch it.
|
||
*/
|
||
function buddy_mood_label( $mood_score ) {
|
||
if ( $mood_score >= 75 && mt_rand( 1, 100 ) <= 5 ) {
|
||
return array( 'label' => __( 'Cheeky 😉', 'buddy' ), 'tone' => 'wink' );
|
||
}
|
||
if ( $mood_score >= 80 ) { return array( 'label' => __( 'Thriving', 'buddy' ), 'tone' => 'happy' ); }
|
||
if ( $mood_score >= 60 ) { return array( 'label' => __( 'Content', 'buddy' ), 'tone' => 'happy' ); }
|
||
if ( $mood_score >= 40 ) { return array( 'label' => __( 'Okay', 'buddy' ), 'tone' => 'neutral' ); }
|
||
if ( $mood_score >= 20 ) { return array( 'label' => __( 'Hungry', 'buddy' ), 'tone' => 'sad' ); }
|
||
return array( 'label' => __( 'Distressed', 'buddy' ), 'tone' => 'sad' );
|
||
}
|