creaturesImage work: add mutableCopy and tint methods to the base class (and change...
[openc2e.git] / CompoundPart.cpp
blobce386ddff8a6552443391ac017890dfeddb2c8e6
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 void CompoundPart::handleClick(float clickx, float clicky) {
112 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 {
147 throw caosException(boost::str(boost::format("Failed to create sprite part: first sprite %d is beyond %d sprite(s) in file") % firstimg % sprite->numframes()));
152 SpritePart::~SpritePart() {
155 void SpritePart::changeSprite(std::string spritefile, unsigned int fimg) {
156 shared_ptr<creaturesImage> spr = world.gallery.getImage(spritefile);
157 caos_assert(spr);
158 caos_assert(spr->numframes() > fimg);
159 // TODO: should we preserve base/pose here, instead?
160 pose = 0;
161 base = 0;
162 firstimg = fimg;
163 spriteno = fimg;
164 // TODO: should we preserve tint?
165 origsprite = sprite = spr;
168 unsigned int SpritePart::getWidth() {
169 return sprite->width(getCurrentSprite());
172 unsigned int SpritePart::getHeight() {
173 return sprite->height(getCurrentSprite());
176 unsigned int CompoundPart::getZOrder() const {
177 return parent->getZOrder() + zorder;
180 void CompoundPart::zapZOrder() {
181 renderable::zapZOrder();
182 world.zorder.erase(zorder_iter);
185 void CompoundPart::addZOrder() {
186 renderable::addZOrder();
187 zorder_iter = world.zorder.insert(this);
190 void SpritePart::tint(unsigned char r, unsigned char g, unsigned char b, unsigned char rotation, unsigned char swap) {
191 sprite = origsprite->mutableCopy();
192 sprite->tint(r, g, b, rotation, swap);
195 DullPart::DullPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y,
196 unsigned int _z) : SpritePart(p, _id, spritefile, fimg, _x, _y, _z) {
199 ButtonPart::ButtonPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y,
200 unsigned int _z, const bytestring_t &animhover, int msgid, int option) : SpritePart(p, _id, spritefile, fimg, _x, _y, _z) {
201 messageid = msgid;
202 hitopaquepixelsonly = (option == 1);
203 hoveranimation = animhover;
206 unsigned int calculateScriptId(unsigned int message_id); // from caosVM_agent.cpp, TODO: move into shared file
208 void ButtonPart::handleClick(float x, float y) {
209 caosVar v; v.setInt(id); // _p1_ is id of part, according to Edynn code
210 parent->queueScript(calculateScriptId(messageid), (Agent *)world.hand(), v);
213 TextPart::TextPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y, unsigned int _z, std::string fontsprite)
214 : SpritePart(p, _id, spritefile, fimg, _x, _y, _z) {
215 textsprite = world.gallery.getImage(fontsprite);
216 caos_assert(textsprite);
217 caos_assert(textsprite->numframes() == 224);
218 leftmargin = 8; topmargin = 8; rightmargin = 8; bottommargin = 8;
219 linespacing = 0; charspacing = 0;
220 horz_align = leftalign; vert_align = top;
221 currpage = 0;
222 recalculateData(); // ie, insert a blank first page
225 TextPart::~TextPart() {
228 void TextPart::addTint(std::string tintinfo) {
229 // add a tint, starting at text.size(), using the data in tintinfo
230 // TODO: there's some caching to be done here, but tinting is rather rare, so..
232 unsigned short r = 128, g = 128, b = 128, rot = 128, swap = 128;
233 int where = 0;
234 std::string cur;
235 for (unsigned int i = 0; i <= tintinfo.size(); i++) {
236 if (i == tintinfo.size() || tintinfo[i] == ' ') {
237 unsigned short val = atoi(cur.c_str());
238 if (val <= 256) {
239 switch (where) {
240 case 0: r = val; break;
241 case 1: g = val; break;
242 case 2: b = val; break;
243 case 3: rot = val; break;
244 case 4: swap = val; break;
246 } // TODO: else explode();
247 where++;
248 cur = "";
249 if (where > 4) break;
250 } else cur += tintinfo[i];
253 texttintinfo t;
254 t.offset = text.size();
256 if (!(r == g == b == rot == swap == 128)) {
257 t.sprite = textsprite->mutableCopy();
258 t.sprite->tint(r, g, b, rot, swap);
259 } else t.sprite = textsprite;
261 tints.push_back(t);
264 void TextPart::setText(std::string t) {
265 text.clear();
266 tints.clear();
268 // parse and remove the <tint> tagging
269 for (unsigned int i = 0; i < t.size(); i++) {
270 if ((t[i] == '<') && (t.size() > i+4))
271 if ((t[i + 1] == 't') && (t[i + 2] == 'i') && (t[i + 3] == 'n') && (t[i + 4] == 't')) {
272 i += 5;
273 std::string tintinfo;
274 if (t[i] == ' ') i++; // skip initial space, if any
275 for (; i < t.size(); i++) {
276 if (t[i] == '>')
277 break;
278 tintinfo += t[i];
280 addTint(tintinfo);
281 continue;
283 text += t[i];
286 recalculateData();
289 void TextEntryPart::setText(std::string t) {
290 TextPart::setText(t);
292 // place caret at the end of the text
293 caretpos = text.size();
296 unsigned int calculateScriptId(unsigned int message_id); // from caosVM_agent.cpp, TODO: move into shared file
298 void TextEntryPart::handleClick(float clickx, float clicky) {
299 world.setFocus(this);
302 void TextEntryPart::handleKey(char c) {
303 text.insert(caretpos, 1, c);
304 caretpos++;
305 recalculateData();
308 void TextEntryPart::handleSpecialKey(char c) {
309 switch (c) {
310 case 8: // backspace
311 if (caretpos == 0) return;
312 text.erase(text.begin() + (caretpos - 1));
313 caretpos--;
314 break;
316 case 13: // return
317 // TODO: check if we should do this or a newline
318 parent->queueScript(calculateScriptId(messageid), 0); // TODO: is a null FROM correct?
319 return;
321 case 37: // left
322 if (caretpos == 0) return;
323 caretpos--;
324 return;
326 case 39: // right
327 if (caretpos == text.size()) return;
328 caretpos++;
329 return;
331 case 38: // up
332 case 40: // down
333 return;
335 case 46: // delete
336 if ((text.size() == 0) || (caretpos >= text.size()))
337 return;
338 text.erase(text.begin() + caretpos);
339 break;
341 default:
342 return;
345 assert(caretpos <= text.size());
347 recalculateData();
350 void TextPart::setFormat(int left, int top, int right, int bottom, int line, int _char, horizontalalign horza, verticalalign verta, bool lastpagescroll) {
351 leftmargin = left;
352 topmargin = top;
353 rightmargin = right;
354 bottommargin = bottom;
355 linespacing = line;
356 charspacing = _char;
357 horz_align = horza;
358 vert_align = verta;
359 last_page_scroll = lastpagescroll;
360 recalculateData();
363 unsigned int TextPart::calculateWordWidth(std::string word) {
364 unsigned int x = 0;
365 for (unsigned int i = 0; i < word.size(); i++) {
366 if (word[i] < 32) continue; // TODO: replace with space or similar?
367 int spriteid = word[i] - 32;
369 x += textsprite->width(spriteid);
370 if (i != 0) x += charspacing;
372 return x;
376 * Recalculate the data used for rendering the text part.
378 void TextPart::recalculateData() {
379 linedata currentdata;
381 lines.clear();
382 pages.clear();
383 pageheights.clear();
384 pages.push_back(0);
385 if (text.size() == 0) {
386 pageheights.push_back(0);
387 lines.push_back(currentdata); // blank line, so caret is rendered in TextEntryParts
388 return;
391 unsigned int textwidth = getWidth() - leftmargin - rightmargin;
392 unsigned int textheight = getHeight() - topmargin - bottommargin;
394 unsigned int currenty = 0, usedheight = 0;
395 unsigned int i = 0;
396 while (i < text.size()) {
397 bool newline = false;
398 unsigned int startoffset = i;
399 // first, retrieve a word from the text
400 std::string word;
401 for (; i < text.size(); i++) {
402 if ((text[i] == ' ') || (text[i] == '\n')) {
403 if (text[i] == '\n') newline = true;
404 i++;
405 break;
407 word += text[i];
410 // next, work out whether it fits
411 unsigned int wordlen = calculateWordWidth(word);
412 unsigned int spacelen = textsprite->width(0) + charspacing;
413 unsigned int possiblelen = wordlen;
414 if (currentdata.text.size() > 0)
415 possiblelen = wordlen + spacelen;
416 // TODO: set usedheight as appropriate/needed
417 usedheight = textsprite->height(0);
418 if (currentdata.width + possiblelen <= textwidth) {
419 // the rest of the word fits on the current line, so that's okay.
420 // add a space if we're not the first word on this line
421 if (currentdata.text.size() > 0) word = std::string(" ") + word;
422 currentdata.text += word;
423 currentdata.width += possiblelen;
424 } else if (wordlen <= textwidth) {
425 // the rest of the word doesn't fit on the current line, but does on the next line.
426 if (currenty + usedheight > textheight) {
427 pageheights.push_back(currenty);
428 pages.push_back(lines.size());
429 currenty = 0;
430 } else currenty += usedheight + linespacing + 1;
431 currentdata.text += " "; // TODO: HACK THINK ABOUT THIS
432 lines.push_back(currentdata);
433 currentdata.reset(startoffset);
435 currentdata.text += word;
436 currentdata.width += wordlen;
437 } else {
438 // TODO: word is too wide to fit on a single line
439 // we should output as much as possible and then go backwards
442 // we force a newline here if necessary (ie, if the last char is '\n', except not in the last case)
443 if ((i < text.size()) && (newline)) {
444 if (currenty + usedheight > textheight) {
445 pageheights.push_back(currenty);
446 pages.push_back(lines.size());
447 currenty = 0;
448 } else currenty += usedheight + linespacing + 1;
449 lines.push_back(currentdata);
450 currentdata.reset(i);
454 if (currentdata.text.size() > 0) {
455 currenty += usedheight;
456 if (text[text.size() - 1] == ' ') currentdata.text += " "; // TODO: HACK THINK ABOUT THIS
457 lines.push_back(currentdata);
460 pageheights.push_back(currenty);
463 void TextPart::partRender(Surface *renderer, int xoffset, int yoffset, TextEntryPart *caretdata) {
464 SpritePart::partRender(renderer, xoffset, yoffset);
466 unsigned int xoff = xoffset + x + leftmargin;
467 unsigned int yoff = yoffset + y + topmargin;
468 unsigned int textwidth = getWidth() - leftmargin - rightmargin;
469 unsigned int textheight = getHeight() - topmargin - bottommargin;
471 unsigned int currenty = 0, usedheight = 0;
472 if (vert_align == bottom)
473 currenty = textheight - pageheights[currpage];
474 else if (vert_align == middle)
475 currenty = (textheight - pageheights[currpage]) / 2;
476 unsigned int startline = pages[currpage];
477 unsigned int endline = (currpage + 1 < pages.size() ? pages[currpage + 1] : lines.size());
478 shared_ptr<creaturesImage> sprite = textsprite; unsigned int currtint = 0;
479 for (unsigned int i = startline; i < endline; i++) {
480 unsigned int currentx = 0, somex = xoff;
481 if (horz_align == rightalign)
482 somex = somex + (textwidth - lines[i].width);
483 else if (horz_align == centeralign)
484 somex = somex + ((textwidth - lines[i].width) / 2);
486 for (unsigned int x = 0; x < lines[i].text.size(); x++) {
487 if (currtint < tints.size() && tints[currtint].offset == lines[i].offset + x) {
488 sprite = tints[currtint].sprite;
489 currtint++;
492 if (lines[i].text[x] < 32) continue; // TODO: replace with space or similar?
493 int spriteid = lines[i].text[x] - 32;
494 renderer->render(sprite, spriteid, somex + currentx, yoff + currenty, has_alpha, alpha);
495 if ((caretdata) && (caretdata->caretpos == lines[i].offset + x))
496 caretdata->renderCaret(renderer, somex + currentx, yoff + currenty);
497 currentx += textsprite->width(spriteid) + charspacing;
499 if ((caretdata) && (caretdata->caretpos == lines[i].offset + lines[i].text.size()))
500 caretdata->renderCaret(renderer, somex + currentx, yoff + currenty);
501 currenty += textsprite->height(0) + linespacing + 1;
505 FixedTextPart::FixedTextPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y,
506 unsigned int _z, std::string fontsprite) : TextPart(p, _id, spritefile, fimg, _x, _y, _z, fontsprite) {
507 // nothing, hopefully.. :)
510 TextEntryPart::TextEntryPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y,
511 unsigned int _z, unsigned int msgid, std::string fontsprite) : TextPart(p, _id, spritefile, fimg, _x, _y, _z, fontsprite) {
512 // TODO: hm, this never gets freed..
513 if (!caretsprite) { caretsprite = world.gallery.getImage("cursor"); caos_assert(caretsprite); }
515 caretpose = 0;
516 caretpos = 0;
517 focused = false;
518 messageid = msgid;
521 void TextEntryPart::partRender(Surface *renderer, int xoffset, int yoffset) {
522 TextPart::partRender(renderer, xoffset, yoffset, (focused ? this : 0));
525 void TextEntryPart::renderCaret(Surface *renderer, int xoffset, int yoffset) {
526 // TODO: fudge xoffset/yoffset as required
527 renderer->render(caretsprite, caretpose, xoffset, yoffset, has_alpha, alpha);
530 void TextEntryPart::tick() {
531 SpritePart::tick();
533 if (focused) {
534 caretpose++;
535 if (caretpose == caretsprite->numframes())
536 caretpose = 0;
540 void SpritePart::tick() {
541 if (!animation.empty()) {
542 if (framerate > 1) {
543 framedelay++;
544 if (framedelay == (unsigned int)framerate + 1)
545 framedelay = 0;
548 if (framedelay == 0) {
549 unsigned int f = frameno + 1;
550 if (f == animation.size()) return;
551 if (animation[f] == 255) {
552 if (f == (animation.size() - 1)) f = 0;
553 else f = animation[f + 1];
555 // TODO: check f is valid..
556 setFrameNo(f);
561 CameraPart::CameraPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y,
562 unsigned int _z, unsigned int view_width, unsigned int view_height, unsigned int camera_width, unsigned int camera_height)
563 : SpritePart(p, _id, spritefile, fimg, _x, _y, _z) {
564 viewwidth = view_width;
565 viewheight = view_height;
566 camerawidth = camera_width;
567 cameraheight = camera_height;
568 camera = shared_ptr<Camera>(new PartCamera(this));
571 void CameraPart::partRender(class Surface *renderer, int xoffset, int yoffset) {
572 // TODO: hack to stop us rendering cameras inside cameras. better way?
573 if (renderer == engine.backend->getMainSurface()) {
574 // make sure we're onscreen before bothering to do any work..
575 if (xoffset + x + viewwidth > 0 && yoffset + y + viewheight > 0 &&
576 xoffset + x < (int)renderer->getWidth() && yoffset + y < (int)renderer->getHeight()) {
577 Surface *surface = engine.backend->newSurface(viewwidth, viewheight);
578 assert(surface); // TODO: good behaviour?
579 world.drawWorld(camera.get(), surface);
580 renderer->blitSurface(surface, xoffset + x, yoffset + y, camerawidth, cameraheight);
581 engine.backend->freeSurface(surface);
585 SpritePart::partRender(renderer, xoffset, yoffset);
588 void CameraPart::tick() {
589 SpritePart::tick();
591 camera->tick();
594 GraphPart::GraphPart(Agent *p, unsigned int _id, std::string spritefile, unsigned int fimg, int _x, int _y,
595 unsigned int _z, unsigned int novalues) : SpritePart(p, _id, spritefile, fimg, _x, _y, _z) {
596 // TODO: store novalues
599 /* vim: set noet: */