Files
rangerhq-buddy/inc/state.php
T
ranger 48e97862a6 chore: initial commit — Buddy v0.1.0 (Phase A complete)
Buddy is born. First commit of a new standalone WordPress plugin —
the spiritual successor to the tamagotchi that once lived inside
A-WP-Notes v1.1.5 (gracefully retired). Rebuilt from scratch with
all the v3-discipline lessons baked in from day one.

PHASE A — pet exists
- Dashboard widget at WP Admin → Dashboard showing SVG character +
  name + mood label + four stats bars.
- Dedicated admin page at WP Admin → Buddy → My Buddy (bigger view).
- About page with side-by-side intro + plain-prose cards (Logbook
  About-page pattern carried forward).
- Settings page with name-rename form + Updates panel.
- Per-user state in user_meta key buddy_state (each WP admin gets
  their own pet, no shared state).
- Inline SVG sprite renderer with three mood tones (happy/neutral/
  sad) and three sizes (sm/md/lg). CSS keyframe animations: bobbing
  + periodic blinking. Zero image files.
- Self-hosted update checker wired up from commit 1, ported from
  Logbook v3.3.5: /releases/latest with /tags?limit=1 fallback,
  12h success cache / 1h negative cache. UI on Settings page.
- dashicons-pets admin-menu icon — literal paw-print, brand match.

ARCHITECTURE LOCKED FROM COMMIT 1
- Single-word brand name "Buddy" — no WP prefix, no future rebrand.
- Public GPL v2+ Gitea repo (ranger/a-buddy).
- Constants prefix BUDDY_*, function prefix buddy_*, text domain
  buddy. Clean naming throughout — none of Logbook's wp-notes-*
  historical-artifact baggage.
- Single H1 per admin page, no nested toggle boxes, no duplicate
  sections — Tier-1 discipline carried forward from Logbook.
- All assets local (inline SVG, plain CSS), no third-party CDN,
  no Gravatar-style external pings.

NOT IN THIS RELEASE (planned)
- Phase B — Feed/Play/Clean/Sleep interactions + cooldown timers.
- Phase C — WP-cron decay + "Buddy is hungry" dismissible notices
  (port the persistent-dismissal pattern from Logbook).
- Phase D — Multiple species (dog, dragon, sprite), per-species
  personality phrases.
- Phase E — Site-health hook: pet stats react to wp_get_site_health()
  results. The killer feature.
- Phase F — Pro tier (€2.99 lifetime) with custom skins + multi-pet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:23:57 +01:00

114 lines
4.1 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 0100. 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 0100).
* @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 0100.
*/
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 emoji / status label based on overall mood. Pure
* cosmetic, used in the dashboard widget header.
*/
function buddy_mood_label( $mood_score ) {
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' );
}