* change handleClick everywhere to return an int
[openc2e.git] / CompoundPart.cpp
blobfbf0df0a6a616265a71d33de001140ca0491bb46
1 /*
2 * CompoundPart.cpp
3 * openc2e
5 * Created by Alyssa Milburn on Tue May 25 2004.
6 * Copyright (c) 2004-2008 Alyssa Milburn. All rights reserved.
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Lesser General Public
10 * License as published by the Free Software Foundation; either
11 * version 2 of the License, or (at your option) any later version.
13 * This library is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Lesser General Public License for more details.
20 #include "CompoundPart.h"
21 #include "CameraPart.h"
22 #include "Camera.h"
23 #include "World.h"
24 #include "Engine.h"
25 #include "creaturesImage.h"
26 #include "Backend.h"
27 #include "Agent.h"
29 bool partzorder::operator ()(const CompoundPart *s1, const CompoundPart *s2) const {
30 // TODO: unsure about all of this, needs a check (but seems to work)
31 if (s1->getParent()->getZOrder() == s2->getParent()->getZOrder()) {
32 // part 0 is often at the same plane as other parts..
33 // TODO: is this correct fix? I suspect not, we should be sorting by reaction order, not id.
34 if (s1->getParent() == s2->getParent()) {
35 return s1->id > s2->id;
36 } else
37 return s1->getZOrder() > s2->getZOrder();
39 return s1->getParent()->getZOrder() > s2->getParent()->getZOrder();
42 shared_ptr<creaturesImage> TextEntryPart::caretsprite;
44 void CompoundPart::render(Surface *renderer, int xoffset, int yoffset) {
45 if (parent->visible) {
46 partRender(renderer, xoffset + (int)parent->x, yoffset + (int)parent->y);
47 if (parent->displaycore /*&& (id == 0)*/) {
48 // TODO: tsk, this should be drawn along with the other craziness on the line plane, i expect
49 int xoff = xoffset + (int)parent->x + x;
50 int yoff = yoffset + (int)parent->y + y;
51 renderer->renderLine(xoff + (getWidth() / 2), yoff, xoff + getWidth(), yoff + (getHeight() / 2), 0xFF0000CC);
52 renderer->renderLine(xoff + getWidth(), yoff + (getHeight() / 2), xoff + (getWidth() / 2), yoff + getHeight(), 0xFF0000CC);
53 renderer->renderLine(xoff + (getWidth() / 2), yoff + getHeight(), xoff, yoff + (getHeight() / 2), 0xFF0000CC);
54 renderer->renderLine(xoff, yoff + (getHeight() / 2), xoff + (getWidth() / 2), yoff, 0xFF0000CC);
59 bool CompoundPart::showOnRemoteCameras() {
60 return !parent->camerashy();
63 void SpritePart::partRender(Surface *renderer, int xoffset, int yoffset) {
64 // TODO: we need a nicer way to handle such errors
65 if (getCurrentSprite() >= getSprite()->numframes()) {
66 std::string err = boost::str(boost::format("pose to be rendered %d (firstimg %d, base %d) was past end of sprite file '%s' (%d sprites)") %
67 pose % firstimg % base % getSprite()->getName() % getSprite()->numframes());
68 parent->unhandledException(err, false);
69 return;
71 assert(getCurrentSprite() < getSprite()->numframes());
72 renderer->render(getSprite(), getCurrentSprite(), xoffset + x, yoffset + y, has_alpha, alpha, draw_mirrored);
75 void SpritePart::setFrameNo(unsigned int f) {
76 assert(f < animation.size());
77 if (firstimg + base + animation[f] >= getSprite()->numframes()) {
78 std::string err = boost::str(boost::format("animation frame %d (firstimg %d, base %d, value %d) was past end of sprite file '%s' (%d sprites)") %
79 f % firstimg % base % (int)animation[f] % getSprite()->getName() % getSprite()->numframes());
80 parent->unhandledException(err, false);
81 animation.clear();
82 return;
85 frameno = f;
86 pose = animation[f];
87 spriteno = firstimg + base + pose;
90 void SpritePart::setPose(unsigned int p) {
91 if (firstimg + base + p >= getSprite()->numframes()) {
92 std::string err = boost::str(boost::format("new pose %d (firstimg %d, base %d) was past end of sprite file '%s' (%d sprites)") %
93 p % firstimg % base % getSprite()->getName() % getSprite()->numframes());
94 parent->unhandledException(err, false);
95 return;
98 animation.clear();
99 pose = p;
100 spriteno = firstimg + base + pose;
103 void SpritePart::setBase(unsigned int b) {
104 base = b;
107 bool SpritePart::transparentAt(unsigned int x, unsigned int y) {
108 return getSprite()->transparentAt(getCurrentSprite(), x, y);
111 int CompoundPart::handleClick(float clickx, float clicky) {
112 return parent->handleClick(clickx + x + parent->x, clicky + y + parent->y);
115 CompoundPart::CompoundPart(Agent *p, unsigned int _id, int _x, int _y, int _z) : parent(p), zorder(_z), id(_id) {
116 addZOrder();
117 x = _x;
118 y = _y;
120 has_alpha = false;
123 CompoundPart::~CompoundPart() {
124 world.zorder.erase(zorder_iter);
127 SpritePart::SpritePart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg,
128 int _x, int _y, unsigned int _z) : AnimatablePart(p, _id, _x, _y, _z) {
129 origsprite = sprite = world.gallery.getImage(spritefile);
130 firstimg = fimg;
131 caos_assert(sprite);
133 pose = 0;
134 base = 0;
135 spriteno = firstimg;
136 is_transparent = (engine.version > 2);
137 framerate = 1;
138 framedelay = 0;
139 draw_mirrored = false;
141 if (sprite->numframes() <= firstimg) {
142 if (world.gametype == "cv") {
143 // Creatures Village allows you to create sprites with crazy invalid data, do the same as it does
144 // (obviously the firstimg data is invalid so all attempts to change the pose/etc will fail, same as in CV)
145 spriteno = 0;
146 } else if (engine.bmprenderer) {
147 // BLCK hasn't been called yet, so we can't check validity yet
148 } else {
149 throw caosException(boost::str(boost::format("Failed to create sprite part: first sprite %d is beyond %d sprite(s) in file") % firstimg % sprite->numframes()));
154 SpritePart::~SpritePart() {
157 void SpritePart::changeSprite(std::string spritefile, unsigned int fimg) {
158 shared_ptr<creaturesImage> spr = world.gallery.getImage(spritefile);
159 caos_assert(spr);
160 caos_assert(spr->numframes() > fimg);
161 // TODO: should we preserve base/pose here, instead?
162 pose = 0;
163 base = 0;
164 firstimg = fimg;
165 spriteno = fimg;
166 // TODO: should we preserve tint?
167 origsprite = sprite = spr;
170 unsigned int SpritePart::getWidth() {
171 return sprite->width(getCurrentSprite());
174 unsigned int SpritePart::getHeight() {
175 return sprite->height(getCurrentSprite());
178 unsigned int CompoundPart::getZOrder() const {
179 return parent->getZOrder() + zorder;
182 void CompoundPart::zapZOrder() {
183 renderable::zapZOrder();
184 world.zorder.erase(zorder_iter);
187 void CompoundPart::addZOrder() {
188 renderable::addZOrder();
189 zorder_iter = world.zorder.insert(this);
192 void SpritePart::tint(unsigned char r, unsigned char g, unsigned char b, unsigned char rotation, unsigned char swap) {
193 sprite = origsprite->mutableCopy();
194 sprite->tint(r, g, b, rotation, swap);
197 DullPart::DullPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y,
198 unsigned int _z) : SpritePart(p, _id, spritefile, fimg, _x, _y, _z) {
201 ButtonPart::ButtonPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y,
202 unsigned int _z, const bytestring_t &animhover, int msgid, int option) : SpritePart(p, _id, spritefile, fimg, _x, _y, _z) {
203 messageid = msgid;
204 hitopaquepixelsonly = (option == 1);
205 hoveranimation = animhover;
208 unsigned int calculateScriptId(unsigned int message_id); // from caosVM_agent.cpp, TODO: move into shared file
210 int ButtonPart::handleClick(float x, float y) {
211 return calculateScriptId(messageid);
214 TextPart::TextPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y, unsigned int _z, std::string fontsprite)
215 : SpritePart(p, _id, spritefile, fimg, _x, _y, _z) {
216 textsprite = world.gallery.getImage(fontsprite);
217 caos_assert(textsprite);
218 caos_assert(textsprite->numframes() == 224);
219 leftmargin = 8; topmargin = 8; rightmargin = 8; bottommargin = 8;
220 linespacing = 0; charspacing = 0;
221 horz_align = leftalign; vert_align = top;
222 currpage = 0;
223 recalculateData(); // ie, insert a blank first page
226 TextPart::~TextPart() {
229 void TextPart::addTint(std::string tintinfo) {
230 // add a tint, starting at text.size(), using the data in tintinfo
231 // TODO: there's some caching to be done here, but tinting is rather rare, so..
233 unsigned short r = 128, g = 128, b = 128, rot = 128, swap = 128;
234 int where = 0;
235 std::string cur;
236 for (unsigned int i = 0; i <= tintinfo.size(); i++) {
237 if (i == tintinfo.size() || tintinfo[i] == ' ') {
238 unsigned short val = atoi(cur.c_str());
239 if (val <= 256) {
240 switch (where) {
241 case 0: r = val; break;
242 case 1: g = val; break;
243 case 2: b = val; break;
244 case 3: rot = val; break;
245 case 4: swap = val; break;
247 } // TODO: else explode();
248 where++;
249 cur = "";
250 if (where > 4) break;
251 } else cur += tintinfo[i];
254 texttintinfo t;
255 t.offset = text.size();
257 if (!(r == g == b == rot == swap == 128)) {
258 t.sprite = textsprite->mutableCopy();
259 t.sprite->tint(r, g, b, rot, swap);
260 } else t.sprite = textsprite;
262 tints.push_back(t);
265 void TextPart::setText(std::string t) {
266 text.clear();
267 tints.clear();
269 // parse and remove the <tint> tagging
270 for (unsigned int i = 0; i < t.size(); i++) {
271 if ((t[i] == '<') && (t.size() > i+4))
272 if ((t[i + 1] == 't') && (t[i + 2] == 'i') && (t[i + 3] == 'n') && (t[i + 4] == 't')) {
273 i += 5;
274 std::string tintinfo;
275 if (t[i] == ' ') i++; // skip initial space, if any
276 for (; i < t.size(); i++) {
277 if (t[i] == '>')
278 break;
279 tintinfo += t[i];
281 addTint(tintinfo);
282 continue;
284 text += t[i];
287 recalculateData();
290 void TextEntryPart::setText(std::string t) {
291 TextPart::setText(t);
293 // place caret at the end of the text
294 caretpos = text.size();
297 unsigned int calculateScriptId(unsigned int message_id); // from caosVM_agent.cpp, TODO: move into shared file
299 int TextEntryPart::handleClick(float clickx, float clicky) {
300 world.setFocus(this);
302 return -1; // TODO: this shouldn't be passed onto the parent agent?
305 void TextEntryPart::handleKey(char c) {
306 text.insert(caretpos, 1, c);
307 caretpos++;
308 recalculateData();
311 void TextEntryPart::handleSpecialKey(char c) {
312 switch (c) {
313 case 8: // backspace
314 if (caretpos == 0) return;
315 text.erase(text.begin() + (caretpos - 1));
316 caretpos--;
317 break;
319 case 13: // return
320 // TODO: check if we should do this or a newline
321 parent->queueScript(calculateScriptId(messageid), 0); // TODO: is a null FROM correct?
322 return;
324 case 37: // left
325 if (caretpos == 0) return;
326 caretpos--;
327 return;
329 case 39: // right
330 if (caretpos == text.size()) return;
331 caretpos++;
332 return;
334 case 38: // up
335 case 40: // down
336 return;
338 case 46: // delete
339 if ((text.size() == 0) || (caretpos >= text.size()))
340 return;
341 text.erase(text.begin() + caretpos);
342 break;
344 default:
345 return;
348 assert(caretpos <= text.size());
350 recalculateData();
353 void TextPart::setFormat(int left, int top, int right, int bottom, int line, int _char, horizontalalign horza, verticalalign verta, bool lastpagescroll) {
354 leftmargin = left;
355 topmargin = top;
356 rightmargin = right;
357 bottommargin = bottom;
358 linespacing = line;
359 charspacing = _char;
360 horz_align = horza;
361 vert_align = verta;
362 last_page_scroll = lastpagescroll;
363 recalculateData();
366 unsigned int TextPart::calculateWordWidth(std::string word) {
367 unsigned int x = 0;
368 for (unsigned int i = 0; i < word.size(); i++) {
369 if (word[i] < 32) continue; // TODO: replace with space or similar?
370 int spriteid = word[i] - 32;
372 x += textsprite->width(spriteid);
373 if (i != 0) x += charspacing;
375 return x;
379 * Recalculate the data used for rendering the text part.
381 void TextPart::recalculateData() {
382 linedata currentdata;
384 lines.clear();
385 pages.clear();
386 pageheights.clear();
387 pages.push_back(0);
388 if (text.size() == 0) {
389 pageheights.push_back(0);
390 lines.push_back(currentdata); // blank line, so caret is rendered in TextEntryParts
391 return;
394 unsigned int textwidth = getWidth() - leftmargin - rightmargin;
395 unsigned int textheight = getHeight() - topmargin - bottommargin;
397 unsigned int currenty = 0, usedheight = 0;
398 unsigned int i = 0;
399 while (i < text.size()) {
400 bool newline = false;
401 unsigned int startoffset = i;
402 // first, retrieve a word from the text
403 std::string word;
404 for (; i < text.size(); i++) {
405 if ((text[i] == ' ') || (text[i] == '\n')) {
406 if (text[i] == '\n') newline = true;
407 i++;
408 break;
410 word += text[i];
413 // next, work out whether it fits
414 unsigned int wordlen = calculateWordWidth(word);
415 unsigned int spacelen = textsprite->width(0) + charspacing;
416 unsigned int possiblelen = wordlen;
417 if (currentdata.text.size() > 0)
418 possiblelen = wordlen + spacelen;
419 // TODO: set usedheight as appropriate/needed
420 usedheight = textsprite->height(0);
421 if (currentdata.width + possiblelen <= textwidth) {
422 // the rest of the word fits on the current line, so that's okay.
423 // add a space if we're not the first word on this line
424 if (currentdata.text.size() > 0) word = std::string(" ") + word;
425 currentdata.text += word;
426 currentdata.width += possiblelen;
427 } else if (wordlen <= textwidth) {
428 // the rest of the word doesn't fit on the current line, but does on the next line.
429 if (currenty + usedheight > textheight) {
430 pageheights.push_back(currenty);
431 pages.push_back(lines.size());
432 currenty = 0;
433 } else currenty += usedheight + linespacing + 1;
434 currentdata.text += " "; // TODO: HACK THINK ABOUT THIS
435 lines.push_back(currentdata);
436 currentdata.reset(startoffset);
438 currentdata.text += word;
439 currentdata.width += wordlen;
440 } else {
441 // TODO: word is too wide to fit on a single line
442 // we should output as much as possible and then go backwards
445 // we force a newline here if necessary (ie, if the last char is '\n', except not in the last case)
446 if ((i < text.size()) && (newline)) {
447 if (currenty + usedheight > textheight) {
448 pageheights.push_back(currenty);
449 pages.push_back(lines.size());
450 currenty = 0;
451 } else currenty += usedheight + linespacing + 1;
452 lines.push_back(currentdata);
453 currentdata.reset(i);
457 if (currentdata.text.size() > 0) {
458 currenty += usedheight;
459 if (text[text.size() - 1] == ' ') currentdata.text += " "; // TODO: HACK THINK ABOUT THIS
460 lines.push_back(currentdata);
463 pageheights.push_back(currenty);
466 void TextPart::partRender(Surface *renderer, int xoffset, int yoffset, TextEntryPart *caretdata) {
467 SpritePart::partRender(renderer, xoffset, yoffset);
469 unsigned int xoff = xoffset + x + leftmargin;
470 unsigned int yoff = yoffset + y + topmargin;
471 unsigned int textwidth = getWidth() - leftmargin - rightmargin;
472 unsigned int textheight = getHeight() - topmargin - bottommargin;
474 unsigned int currenty = 0, usedheight = 0;
475 if (vert_align == bottom)
476 currenty = textheight - pageheights[currpage];
477 else if (vert_align == middle)
478 currenty = (textheight - pageheights[currpage]) / 2;
479 unsigned int startline = pages[currpage];
480 unsigned int endline = (currpage + 1 < pages.size() ? pages[currpage + 1] : lines.size());
481 shared_ptr<creaturesImage> sprite = textsprite; unsigned int currtint = 0;
482 for (unsigned int i = startline; i < endline; i++) {
483 unsigned int currentx = 0, somex = xoff;
484 if (horz_align == rightalign)
485 somex = somex + (textwidth - lines[i].width);
486 else if (horz_align == centeralign)
487 somex = somex + ((textwidth - lines[i].width) / 2);
489 for (unsigned int x = 0; x < lines[i].text.size(); x++) {
490 if (currtint < tints.size() && tints[currtint].offset == lines[i].offset + x) {
491 sprite = tints[currtint].sprite;
492 currtint++;
495 if (lines[i].text[x] < 32) continue; // TODO: replace with space or similar?
496 int spriteid = lines[i].text[x] - 32;
497 renderer->render(sprite, spriteid, somex + currentx, yoff + currenty, has_alpha, alpha);
498 if ((caretdata) && (caretdata->caretpos == lines[i].offset + x))
499 caretdata->renderCaret(renderer, somex + currentx, yoff + currenty);
500 currentx += textsprite->width(spriteid) + charspacing;
502 if ((caretdata) && (caretdata->caretpos == lines[i].offset + lines[i].text.size()))
503 caretdata->renderCaret(renderer, somex + currentx, yoff + currenty);
504 currenty += textsprite->height(0) + linespacing + 1;
508 FixedTextPart::FixedTextPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y,
509 unsigned int _z, std::string fontsprite) : TextPart(p, _id, spritefile, fimg, _x, _y, _z, fontsprite) {
510 // nothing, hopefully.. :)
513 TextEntryPart::TextEntryPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y,
514 unsigned int _z, unsigned int msgid, std::string fontsprite) : TextPart(p, _id, spritefile, fimg, _x, _y, _z, fontsprite) {
515 // TODO: hm, this never gets freed..
516 if (!caretsprite) { caretsprite = world.gallery.getImage("cursor"); caos_assert(caretsprite); }
518 caretpose = 0;
519 caretpos = 0;
520 focused = false;
521 messageid = msgid;
524 void TextEntryPart::partRender(Surface *renderer, int xoffset, int yoffset) {
525 TextPart::partRender(renderer, xoffset, yoffset, (focused ? this : 0));
528 void TextEntryPart::renderCaret(Surface *renderer, int xoffset, int yoffset) {
529 // TODO: fudge xoffset/yoffset as required
530 renderer->render(caretsprite, caretpose, xoffset, yoffset, has_alpha, alpha);
533 void TextEntryPart::tick() {
534 SpritePart::tick();
536 if (focused) {
537 caretpose++;
538 if (caretpose == caretsprite->numframes())
539 caretpose = 0;
543 void SpritePart::tick() {
544 if (!animation.empty()) {
545 if (framerate > 1) {
546 framedelay++;
547 if (framedelay == (unsigned int)framerate + 1)
548 framedelay = 0;
551 if (framedelay == 0) {
552 unsigned int f = frameno + 1;
553 if (f == animation.size()) return;
554 if (animation[f] == 255) {
555 if (f == (animation.size() - 1)) f = 0;
556 else f = animation[f + 1];
558 // TODO: check f is valid..
559 setFrameNo(f);
564 CameraPart::CameraPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y,
565 unsigned int _z, unsigned int view_width, unsigned int view_height, unsigned int camera_width, unsigned int camera_height)
566 : SpritePart(p, _id, spritefile, fimg, _x, _y, _z) {
567 viewwidth = view_width;
568 viewheight = view_height;
569 camerawidth = camera_width;
570 cameraheight = camera_height;
571 camera = shared_ptr<Camera>(new PartCamera(this));
574 void CameraPart::partRender(class Surface *renderer, int xoffset, int yoffset) {
575 // TODO: hack to stop us rendering cameras inside cameras. better way?
576 if (renderer == engine.backend->getMainSurface()) {
577 // make sure we're onscreen before bothering to do any work..
578 if (xoffset + x + viewwidth > 0 && yoffset + y + viewheight > 0 &&
579 xoffset + x < (int)renderer->getWidth() && yoffset + y < (int)renderer->getHeight()) {
580 Surface *surface = engine.backend->newSurface(viewwidth, viewheight);
581 assert(surface); // TODO: good behaviour?
582 world.drawWorld(camera.get(), surface);
583 renderer->blitSurface(surface, xoffset + x, yoffset + y, camerawidth, cameraheight);
584 engine.backend->freeSurface(surface);
588 SpritePart::partRender(renderer, xoffset, yoffset);
591 void CameraPart::tick() {
592 SpritePart::tick();
594 camera->tick();
597 GraphPart::GraphPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y,
598 unsigned int _z, unsigned int novalues) : SpritePart(p, _id, spritefile, fimg, _x, _y, _z) {
599 // TODO: store novalues
602 /* vim: set noet: */