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>
This commit is contained in:
2026-05-25 10:23:57 +01:00
commit 48e97862a6
12 changed files with 1135 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
<?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' );
}