740 lines
16 KiB
C
740 lines
16 KiB
C
/*
|
|
* xpet:
|
|
*
|
|
* a suckless, X (and hopefully wayland) implementation of
|
|
* xneko and other on screen pets
|
|
*
|
|
* > uint 2025
|
|
*/
|
|
|
|
#include <locale.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/select.h>
|
|
#include <time.h>
|
|
|
|
#include <X11/Xlib.h>
|
|
#include <X11/extensions/shape.h>
|
|
#include <X11/xpm.h>
|
|
|
|
#include "xpet.h"
|
|
#include "config.h"
|
|
|
|
void create_window(void);
|
|
void draw_bubble(void);
|
|
void get_mouse_pos(void);
|
|
void grab_keys(void);
|
|
void hide_speech_bubble(void);
|
|
void load_animations(void);
|
|
void move_to(int tx, int ty);
|
|
enum state find_octant(int dx, int dy);
|
|
void on_button_press(XButtonEvent* b);
|
|
void on_button_release(XButtonEvent* b);
|
|
void on_key(KeySym sym);
|
|
void on_motion(XMotionEvent* m);
|
|
void pick_random_destination(void);
|
|
void quit(void);
|
|
void run(void);
|
|
void set_pet_state(enum state ns);
|
|
void setup(void);
|
|
void show_speech_bubble(const char* s);
|
|
void step(enum state d, double* step_x, double* step_y);
|
|
void update_animation(void);
|
|
void wander(void);
|
|
Bool walking(enum state s);
|
|
void xsleep(long ms);
|
|
|
|
Display* dpy;
|
|
Window root;
|
|
int scr;
|
|
int scr_width;
|
|
int scr_height;
|
|
XpmAttributes xpm_attrs;
|
|
XFontSet font_set;
|
|
|
|
struct mouse mouse;
|
|
struct pet pet;
|
|
|
|
void create_window(void)
|
|
{
|
|
pet.x = scr_width / 2;
|
|
pet.y = scr_height / 2;
|
|
pet.subpixel_x = pet.x;
|
|
pet.subpixel_y = pet.y;
|
|
|
|
XSetWindowAttributes attrs;
|
|
attrs.override_redirect = True;
|
|
pet.window = XCreateWindow(
|
|
dpy, root, pet.x, pet.y, 64, 64, 0, CopyFromParent,
|
|
InputOutput, CopyFromParent, CWOverrideRedirect, &attrs
|
|
);
|
|
struct frame* f = &pet.current_animation->frames[0];
|
|
XShapeCombineMask(
|
|
dpy, pet.window, ShapeBounding,
|
|
0, 0, f->mask, ShapeSet
|
|
);
|
|
XSetWindowBackgroundPixmap(dpy, pet.window, f->pix);
|
|
XSelectInput(
|
|
dpy, pet.window,
|
|
ButtonPressMask | ButtonReleaseMask | PointerMotionMask
|
|
);
|
|
XMapWindow(dpy, pet.window);
|
|
XClearWindow(dpy, pet.window);
|
|
XRaiseWindow(dpy, pet.window);
|
|
}
|
|
|
|
void draw_bubble(void)
|
|
{
|
|
if (!pet.speech || !pet.bubble_window) {
|
|
return;
|
|
}
|
|
|
|
XClearWindow(dpy, pet.bubble_window);
|
|
GC graphics_context = DefaultGC(dpy, scr);
|
|
XSetForeground(dpy, graphics_context, BlackPixel(dpy, scr));
|
|
|
|
if (font_set) {
|
|
XRectangle ink;
|
|
XRectangle log;
|
|
|
|
XmbTextExtents(font_set, pet.speech, (int)strlen(pet.speech), &ink, &log);
|
|
XmbDrawString(
|
|
dpy, pet.bubble_window, font_set, graphics_context,
|
|
SPEECH_PAD_X, SPEECH_PAD_Y - log.y, pet.speech, strlen(pet.speech)
|
|
);
|
|
}
|
|
else {
|
|
XFontStruct* f = XQueryFont(dpy, XGContextFromGC(graphics_context));
|
|
int ascent = f ? f->ascent : 12;
|
|
XDrawString(
|
|
dpy, pet.bubble_window, graphics_context,
|
|
SPEECH_PAD_X, SPEECH_PAD_Y + ascent, pet.speech, strlen(pet.speech)
|
|
);
|
|
|
|
if (f) {
|
|
XFreeFontInfo(NULL, f, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void get_mouse_pos(void)
|
|
{
|
|
Window root_ret;
|
|
Window child_ret;
|
|
int x_ret;
|
|
int y_ret;
|
|
int winx_ret ;
|
|
int winy_ret ;
|
|
unsigned int mask_ret;
|
|
|
|
XQueryPointer(
|
|
dpy, root, &root_ret, &child_ret,
|
|
&x_ret, &y_ret, &winx_ret, &winy_ret, &mask_ret
|
|
);
|
|
|
|
mouse.x = x_ret;
|
|
mouse.y = y_ret;
|
|
}
|
|
|
|
void grab_keys(void)
|
|
{
|
|
unsigned int mods[] = {
|
|
0, LockMask, Mod2Mask, LockMask | Mod2Mask
|
|
};
|
|
|
|
for (unsigned i = 0; i < N_KEYBINDS; i++) {
|
|
KeyCode code = XKeysymToKeycode(dpy, bindings[i].sym);
|
|
for (unsigned j = 0; j < sizeof(mods) / sizeof(mods[0]); j++) {
|
|
XGrabKey(
|
|
dpy, code, bindings[i].mask | mods[j], root, True,
|
|
GrabModeAsync, GrabModeAsync
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
void hide_speech_bubble(void)
|
|
{
|
|
if (pet.bubble_window) {
|
|
XUnmapWindow(dpy, pet.bubble_window);
|
|
}
|
|
|
|
pet.speech = NULL;
|
|
pet.speech_time = 0;
|
|
}
|
|
|
|
void load_animations(void)
|
|
{
|
|
memset(&xpm_attrs, 0, sizeof(xpm_attrs));
|
|
for (int i = 0; i < STATE_LAST; i++) {
|
|
int frame_count = animations[i].n_frames;
|
|
if (frame_count == 0) {
|
|
char path[512];
|
|
frame_count = 0;
|
|
|
|
for (;;) {
|
|
snprintf(path, sizeof(path), "%s/%s/%d.xpm",
|
|
PET_ASSET_DIR, animations[i].name, frame_count
|
|
);
|
|
|
|
FILE* f = fopen(path, "r");
|
|
if (!f) {
|
|
break;
|
|
}
|
|
|
|
fclose(f);
|
|
frame_count++;
|
|
}
|
|
|
|
if (frame_count == 0) {
|
|
fprintf(stderr, "xpet: warning: no frames for '%s'\n",
|
|
animations[i].name
|
|
);
|
|
animations[i].frames = NULL;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
animations[i].frames = malloc(sizeof(struct frame) * frame_count);
|
|
if (!animations[i].frames) {
|
|
perror("malloc");
|
|
exit(1);
|
|
}
|
|
|
|
for (int frame_index = 0; frame_index < frame_count; frame_index++) {
|
|
char path[512];
|
|
snprintf(path, sizeof(path), "%s/%s/%d.xpm",
|
|
PET_ASSET_DIR, animations[i].name, frame_index
|
|
);
|
|
|
|
Pixmap pixmap = 0;
|
|
Pixmap mask = 0;
|
|
int xpm_result = XpmReadFileToPixmap(
|
|
dpy, root, path, &pixmap, &mask, &xpm_attrs
|
|
);
|
|
|
|
if (xpm_result != XpmSuccess) {
|
|
fprintf(stderr, "xpet: load failed '%s': %d\n", path, xpm_result);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
animations[i].frames[frame_index].pix = pixmap;
|
|
animations[i].frames[frame_index].mask = mask;
|
|
animations[i].frames[frame_index].duration =
|
|
animations[i].frame_durations ?
|
|
animations[i].frame_durations[frame_index] : FRAME_DURATION;
|
|
}
|
|
animations[i].n_frames = frame_count;
|
|
}
|
|
}
|
|
|
|
void move_to(int target_x, int target_y)
|
|
{
|
|
double delta_x = target_x - pet.subpixel_x;
|
|
double delta_y = target_y - pet.subpixel_y;
|
|
double distance_squared = delta_x * delta_x + delta_y * delta_y;
|
|
|
|
if (distance_squared < 4.0) {
|
|
set_pet_state(IDLE);
|
|
return;
|
|
}
|
|
|
|
enum state direction = find_octant((int)delta_x, (int)delta_y);
|
|
double step_x = 0;
|
|
double step_y = 0;
|
|
step(direction, &step_x, &step_y);
|
|
|
|
if (distance_squared <= (PET_SPEED * PET_SPEED)) {
|
|
pet.subpixel_x = target_x;
|
|
pet.subpixel_y = target_y;
|
|
}
|
|
else {
|
|
pet.subpixel_x += step_x;
|
|
pet.subpixel_y += step_y;
|
|
}
|
|
|
|
pet.x = (int)(pet.subpixel_x + (pet.subpixel_x >= 0 ? 0.5 : -0.5));
|
|
pet.y = (int)(pet.subpixel_y + (pet.subpixel_y >= 0 ? 0.5 : -0.5));
|
|
set_pet_state(direction);
|
|
|
|
XMoveWindow(dpy, pet.window, pet.x, pet.y);
|
|
}
|
|
|
|
enum state find_octant(int delta_x, int delta_y)
|
|
{
|
|
int abs_delta_x = ABS(delta_x);
|
|
int abs_delta_y = ABS(delta_y);
|
|
|
|
/* if horizontal movement stronger than vertical */
|
|
if (abs_delta_x > abs_delta_y * 2) {
|
|
return delta_x > 0 ? E : W;
|
|
}
|
|
|
|
/* if vertical movement stronger than horizontal */
|
|
if (abs_delta_y > abs_delta_x * 2) {
|
|
return delta_y > 0 ? S : N;
|
|
}
|
|
|
|
return delta_x > 0 ?
|
|
(delta_y < 0 ? NE : SE) : (delta_y < 0 ? NW : SW);
|
|
}
|
|
|
|
void on_button_press(XButtonEvent* b)
|
|
{
|
|
if (b->button == Button1) {
|
|
pet.was_chasing = pet.chasing;
|
|
pet.was_frozen = pet.frozen;
|
|
pet.dragging = True;
|
|
pet.drag_offset_x = b->x;
|
|
pet.drag_offset_y = b->y;
|
|
pet.frozen = True;
|
|
pet.chasing = False;
|
|
set_pet_state(DRAGGED);
|
|
}
|
|
else if (b->button == Button3) {
|
|
if (pet.state != HAPPY) {
|
|
pet.previous_state = pet.state;
|
|
set_pet_state(HAPPY);
|
|
pet.happy_time = 0;
|
|
|
|
int n = 0;
|
|
while (pet_phrases[n]) {
|
|
n++;
|
|
}
|
|
|
|
if (n > 0) {
|
|
show_speech_bubble(pet_phrases[rand() % n]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void on_button_release(XButtonEvent* b)
|
|
{
|
|
if (b->button != Button1) {
|
|
return;
|
|
}
|
|
|
|
pet.dragging = False;
|
|
pet.frozen = pet.was_frozen;
|
|
pet.chasing = pet.was_chasing;
|
|
pet.subpixel_x = pet.x;
|
|
pet.subpixel_y = pet.y;
|
|
|
|
if (pet.was_frozen) {
|
|
set_pet_state(IDLE);
|
|
pet.frozen_time = 0;
|
|
}
|
|
else if (!pet.was_chasing) {
|
|
pick_random_destination();
|
|
set_pet_state(IDLE);
|
|
}
|
|
else {
|
|
set_pet_state(E);
|
|
}
|
|
}
|
|
|
|
void on_key(KeySym sym)
|
|
{
|
|
if (sym == bindings[0].sym) { /* chasing */
|
|
pet.chasing = !pet.chasing;
|
|
if (pet.chasing) {
|
|
pet.frozen = False;
|
|
set_pet_state(E);
|
|
}
|
|
else {
|
|
pick_random_destination();
|
|
}
|
|
}
|
|
else if (sym == bindings[1].sym) { /* freeze */
|
|
pet.frozen = !pet.frozen;
|
|
if (pet.frozen) {
|
|
set_pet_state(IDLE);
|
|
pet.frozen_time = 0;
|
|
}
|
|
}
|
|
else if (sym == bindings[2].sym) { /* quit */
|
|
quit();
|
|
}
|
|
}
|
|
|
|
void on_motion(XMotionEvent* m)
|
|
{
|
|
(void)m;
|
|
if (!pet.dragging) {
|
|
return;
|
|
}
|
|
|
|
get_mouse_pos();
|
|
pet.x = mouse.x - pet.drag_offset_x;
|
|
pet.y = mouse.y - pet.drag_offset_y;
|
|
|
|
XMoveWindow(dpy, pet.window, pet.x, pet.y);
|
|
if (pet.speech) {
|
|
show_speech_bubble(pet.speech);
|
|
}
|
|
}
|
|
|
|
void pick_random_destination(void)
|
|
{
|
|
int min_x = WANDER_MARGIN;
|
|
int min_y = WANDER_MARGIN;
|
|
int max_x = scr_width - WANDER_MARGIN - 64;
|
|
int max_y = scr_height - WANDER_MARGIN - 64;
|
|
|
|
/* clamping */
|
|
if (max_x < min_x) {
|
|
max_x = min_x;
|
|
}
|
|
|
|
if (max_y < min_y) {
|
|
max_y = min_y;
|
|
}
|
|
|
|
pet.target_x = min_x + (rand() % (max_x - min_x + 1));
|
|
pet.target_y = min_y + (rand() % (max_y - min_y + 1));
|
|
pet.wander_wait = 0;
|
|
}
|
|
|
|
void quit(void)
|
|
{
|
|
if (pet.bubble_window) {
|
|
XDestroyWindow(dpy, pet.bubble_window);
|
|
}
|
|
if (font_set) {
|
|
XFreeFontSet(dpy, font_set);
|
|
}
|
|
for (int i = 0; i < STATE_LAST; i++) {
|
|
if (animations[i].frames) {
|
|
for (int j = 0; j < animations[i].n_frames; j++) {
|
|
XFreePixmap(dpy, animations[i].frames[j].pix);
|
|
XFreePixmap(dpy, animations[i].frames[j].mask);
|
|
}
|
|
free(animations[i].frames);
|
|
}
|
|
}
|
|
XCloseDisplay(dpy);
|
|
exit(EXIT_SUCCESS);
|
|
}
|
|
|
|
void run(void)
|
|
{
|
|
for (;;) {
|
|
while (XPending(dpy)) {
|
|
XEvent ev;
|
|
XNextEvent(dpy, &ev);
|
|
if (ev.type == KeyPress) {
|
|
on_key(XLookupKeysym(&ev.xkey, 0));
|
|
}
|
|
else if (ev.type == ButtonPress && ev.xbutton.window == pet.window) {
|
|
on_button_press(&ev.xbutton);
|
|
}
|
|
else if (ev.type == ButtonRelease && ev.xbutton.window == pet.window) {
|
|
on_button_release(&ev.xbutton);
|
|
}
|
|
else if (ev.type == MotionNotify && ev.xmotion.window == pet.window) {
|
|
on_motion(&ev.xmotion);
|
|
}
|
|
else if (ev.type == Expose && ev.xexpose.window == pet.bubble_window) {
|
|
draw_bubble();
|
|
}
|
|
}
|
|
if (pet.speech) {
|
|
pet.speech_time += PET_REFRESH;
|
|
if (pet.speech_time >= SPEECH_DURATION) {
|
|
hide_speech_bubble();
|
|
}
|
|
}
|
|
if (pet.state == HAPPY) {
|
|
pet.happy_time += PET_REFRESH;
|
|
if (pet.happy_time >= HAPPY_DURATION) {
|
|
set_pet_state(pet.previous_state);
|
|
}
|
|
}
|
|
else if (pet.dragging) { /* animate only */
|
|
}
|
|
else if (!pet.frozen) {
|
|
if (pet.chasing) {
|
|
get_mouse_pos();
|
|
move_to(mouse.x, mouse.y);
|
|
}
|
|
else {
|
|
wander();
|
|
}
|
|
}
|
|
else {
|
|
pet.frozen_time += PET_REFRESH;
|
|
if (pet.frozen_time >= SLEEP_DELAY && pet.state != SLEEPING) {
|
|
set_pet_state(SLEEPING);
|
|
}
|
|
}
|
|
|
|
update_animation();
|
|
|
|
/* keep windows on top of others */
|
|
XRaiseWindow(dpy, pet.window);
|
|
if (pet.bubble_window) {
|
|
XRaiseWindow(dpy, pet.bubble_window);
|
|
}
|
|
|
|
xsleep(PET_REFRESH);
|
|
}
|
|
}
|
|
|
|
void set_pet_state(enum state new_state)
|
|
{
|
|
if (pet.state != new_state) {
|
|
if (walking(pet.state) && walking(new_state)) {
|
|
int current_frame = pet.current_frame;
|
|
long frame_time = pet.frame_time;
|
|
pet.state = new_state;
|
|
pet.current_animation = &animations[new_state];
|
|
|
|
if (pet.current_animation->n_frames > 0) {
|
|
pet.current_frame = current_frame % pet.current_animation->n_frames;
|
|
int dur = pet.current_animation->frames[pet.current_frame].duration;
|
|
pet.frame_time = (frame_time < dur) ? frame_time : 0;
|
|
}
|
|
else {
|
|
pet.current_frame = pet.frame_time = 0;
|
|
}
|
|
}
|
|
else {
|
|
pet.state = new_state;
|
|
pet.current_animation = &animations[new_state];
|
|
pet.current_frame = 0;
|
|
pet.frame_time = 0;
|
|
}
|
|
}
|
|
else if (pet.current_animation != &animations[new_state]) {
|
|
pet.current_animation = &animations[new_state];
|
|
if (!walking(new_state)) {
|
|
pet.current_frame = pet.frame_time = 0;
|
|
}
|
|
else if (pet.current_animation->n_frames > 0) {
|
|
pet.current_frame %= pet.current_animation->n_frames;
|
|
int duration = pet.current_animation->frames[pet.current_frame].duration;
|
|
|
|
if (pet.frame_time >= duration) {
|
|
pet.frame_time = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void setup(void)
|
|
{
|
|
/* for utf8 support */
|
|
setlocale(LC_ALL, "");
|
|
|
|
dpy = XOpenDisplay(NULL);
|
|
if (!dpy) {
|
|
fputs("xpet: no display\n", stderr);
|
|
exit(1);
|
|
}
|
|
|
|
scr = DefaultScreen(dpy);
|
|
scr_width = DisplayWidth(dpy, scr);
|
|
scr_height = DisplayHeight(dpy, scr);
|
|
root = RootWindow(dpy, scr);
|
|
|
|
char** missing; /* missing character sets */
|
|
int n_missing;
|
|
char* def; /* default string */
|
|
const char* patterns[] = {
|
|
"-*-*-medium-r-*-*-14-*-*-*-*-*-*-*",
|
|
"fixed",
|
|
"*",
|
|
NULL
|
|
};
|
|
|
|
for (int i = 0; !font_set && patterns[i]; i++) {
|
|
font_set = XCreateFontSet(
|
|
dpy, patterns[i], &missing, &n_missing, &def
|
|
);
|
|
|
|
if (font_set && n_missing > 0) {
|
|
XFreeStringList(missing);
|
|
}
|
|
}
|
|
|
|
if (!font_set) {
|
|
fputs("xpet: warn: no fontset; non-ascii will fallback\n", stderr);
|
|
}
|
|
|
|
srand((unsigned)time(NULL));
|
|
load_animations();
|
|
|
|
pet.current_frame = 0;
|
|
pet.frame_time = 0;
|
|
pet.frozen_time = 0;
|
|
pet.happy_time = 0;
|
|
pet.previous_state = IDLE;
|
|
pet.dragging = False;
|
|
pet.was_chasing = False;
|
|
pet.was_frozen = False;
|
|
pet.speech = NULL;
|
|
pet.speech_time = 0;
|
|
pet.bubble_window = 0;
|
|
pet.chasing = False;
|
|
pet.frozen = False;
|
|
|
|
set_pet_state(IDLE);
|
|
pick_random_destination();
|
|
create_window();
|
|
grab_keys();
|
|
XSelectInput(dpy, root, KeyPressMask);
|
|
}
|
|
|
|
void show_speech_bubble(const char* s)
|
|
{
|
|
if (!s) {
|
|
return;
|
|
}
|
|
|
|
if (!font_set) {
|
|
for (const unsigned char* p = (const unsigned char*)s; *p; p++) {
|
|
if (*p & 0x80) {
|
|
/* if no font set, replace it with ":3" */
|
|
s = ":3";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
pet.speech = s;
|
|
pet.speech_time = 0;
|
|
if (!pet.bubble_window) {
|
|
XSetWindowAttributes attr;
|
|
attr.override_redirect = True;
|
|
attr.background_pixel = WhitePixel(dpy, scr);
|
|
attr.border_pixel = BlackPixel(dpy, scr);
|
|
|
|
pet.bubble_window = XCreateWindow(
|
|
dpy, root, 0, 0, 10, 10, 2,
|
|
CopyFromParent, InputOutput, CopyFromParent,
|
|
CWOverrideRedirect | CWBackPixel | CWBorderPixel, &attr
|
|
);
|
|
XSelectInput(dpy, pet.bubble_window, ExposureMask);
|
|
}
|
|
|
|
int text_width = 0;
|
|
int text_height = 0;
|
|
if (font_set) {
|
|
XRectangle ink, log;
|
|
XmbTextExtents(font_set, s, (int)strlen(s), &ink, &log);
|
|
text_width = log.width;
|
|
text_height = log.height;
|
|
}
|
|
else {
|
|
GContext gcontext = XGContextFromGC(DefaultGC(dpy, scr));
|
|
XFontStruct* font = XQueryFont(dpy, gcontext);
|
|
if (font) {
|
|
text_width = XTextWidth(font, s, (int)strlen(s));
|
|
text_height = font->ascent + font->descent;
|
|
XFreeFontInfo(NULL, font, 1);
|
|
}
|
|
}
|
|
|
|
int bw = CLAMP(text_width + 2 * SPEECH_PAD_X, SPEECH_MIN_W, 1 << 15);
|
|
int bh = CLAMP(text_height + 2 * SPEECH_PAD_Y, SPEECH_MIN_H, 1 << 15);
|
|
int bx = CLAMP(pet.x + 32 - bw / 2, 10, scr_width - 10 - bw);
|
|
int by = CLAMP(pet.y - bh - 10, 10, scr_height - 10 - bh);
|
|
|
|
XMoveResizeWindow(dpy, pet.bubble_window, bx, by, bw, bh);
|
|
XMapWindow(dpy, pet.bubble_window);
|
|
draw_bubble();
|
|
XRaiseWindow(dpy, pet.bubble_window);
|
|
}
|
|
|
|
void step(enum state d, double* step_x, double* step_y)
|
|
{
|
|
const double s = PET_SPEED;
|
|
const double ds = PET_SPEED * 0.70710678118654752440; /* sqrt(2)^-1 */
|
|
|
|
switch (d) {
|
|
case E: *step_x = s; *step_y = 0; break;
|
|
case W: *step_x = -s; *step_y = 0; break;
|
|
case N: *step_x = 0; *step_y = -s; break;
|
|
case S: *step_x = 0; *step_y = s; break;
|
|
case NE: *step_x = ds; *step_y = -ds; break;
|
|
case SE: *step_x = ds; *step_y = ds; break;
|
|
case NW: *step_x = -ds; *step_y = -ds; break;
|
|
case SW: *step_x = -ds; *step_y = ds; break;
|
|
default: *step_x = 0; *step_y = 0; break;
|
|
}
|
|
}
|
|
|
|
void update_animation(void)
|
|
{
|
|
if (!pet.current_animation || pet.current_animation->n_frames <= 0) {
|
|
if (pet.state != IDLE) {
|
|
set_pet_state(IDLE);
|
|
}
|
|
return;
|
|
}
|
|
|
|
pet.frame_time += PET_REFRESH;
|
|
struct frame* frame_ptr = &pet.current_animation->frames[pet.current_frame];
|
|
|
|
if (pet.frame_time >= frame_ptr->duration) {
|
|
pet.frame_time = 0;
|
|
pet.current_frame++;
|
|
if (pet.current_frame >= pet.current_animation->n_frames) {
|
|
pet.current_frame = pet.current_animation->loop ?
|
|
0 : pet.current_animation->n_frames - 1;
|
|
}
|
|
frame_ptr = &pet.current_animation->frames[pet.current_frame];
|
|
XShapeCombineMask(dpy, pet.window, ShapeBounding, 0, 0, frame_ptr->mask, ShapeSet);
|
|
XSetWindowBackgroundPixmap(dpy, pet.window, frame_ptr->pix);
|
|
XClearWindow(dpy, pet.window);
|
|
}
|
|
}
|
|
|
|
void wander(void)
|
|
{
|
|
double delta_x = (double)pet.target_x - pet.subpixel_x;
|
|
double delta_y = (double)pet.target_y - pet.subpixel_y;
|
|
double distance_squared = delta_x * delta_x + delta_y * delta_y;
|
|
|
|
if (distance_squared < 4.0) {
|
|
if (pet.wander_wait <= 0) {
|
|
pet.wander_wait =
|
|
WANDER_MIN_WAIT + (rand() % (WANDER_MAX_WAIT - WANDER_MIN_WAIT));
|
|
set_pet_state(IDLE);
|
|
}
|
|
else {
|
|
pet.wander_wait -= PET_REFRESH;
|
|
if (pet.wander_wait <= 0) {
|
|
pick_random_destination();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
move_to(pet.target_x, pet.target_y);
|
|
}
|
|
|
|
Bool walking(enum state s)
|
|
{
|
|
return s == N || s == S || s == E || s == W ||
|
|
s == NW || s == NE || s == SW || s == SE;
|
|
}
|
|
|
|
void xsleep(long ms)
|
|
{
|
|
struct timeval tv = {ms / 1000, (int)((ms % 1000) * 1000)};
|
|
select(0, 0, 0, 0, &tv);
|
|
}
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
(void)argv;
|
|
if (argc > 1) {
|
|
printf("xpets " VERSION "\n> uint 2025\n");
|
|
return 0;
|
|
}
|
|
setup();
|
|
run();
|
|
return 0;
|
|
}
|